Dart eficaz: Diseño
Diseña librerías consistentes y usables.
Aquí tienes algunas pautas para escribir APIs consistentes y usables para tus librerías.
Nombres
#El nombramiento es una parte importante al escribir código legible y mantenible. Las siguientes mejores prácticas pueden ayudarte a lograr ese objetivo.
USA los términos de manera consistente
#Usa el mismo nombre para la misma cosa a lo largo de tu código. Si ya existe un precedente fuera de tu API que los usuarios probablemente conozcan, sigue ese precedente.
pageCount // A field.
updatePageCount() // Consistent with pageCount.
toSomething() // Consistent with Iterable's toList().
asSomething() // Consistent with List's asMap().
Point // A familiar concept.
renumberPages() // Confusingly different from pageCount.
convertToSomething() // Inconsistent with toX() precedent.
wrappedAsSomething() // Inconsistent with asX() precedent.
Cartesian // Unfamiliar to most users.
El objetivo es aprovechar lo que el usuario ya sabe. Esto incluye su conocimiento del dominio del problema en sí, las convenciones de las librerías principales y otras partes de tu propia API. Al construir sobre eso, reduces la cantidad de conocimiento nuevo que tienen que adquirir antes de poder ser productivos.
EVITA las abreviaturas
#A menos que la abreviatura sea más común que el término sin abreviar, no abrevies. Si abrevias, escríbela con mayúsculas correctamente.
pageCount
buildRectangles
IOStream
HttpRequest
numPages // "Num" is an abbreviation of "number (of)".
buildRects
InputOutputStream
HypertextTransferProtocolRequest
PREFIERE colocar el sustantivo más descriptivo al final
#La última palabra debe ser la más descriptiva de lo que es la cosa. Puedes anteponerle otras palabras, como adjetivos, para describir mejor la cosa.
pageCount // A count (of pages).
ConversionSink // A sink for doing conversions.
ChunkedConversionSink // A ConversionSink that's chunked.
CssFontFaceRule // A rule for font faces in CSS.
numPages // Not a collection of pages.
CanvasRenderingContext2D // Not a "2D".
RuleFontFaceCss // Not a CSS.
CONSIDERA hacer que el código se lea como una oración
#Cuando tengas dudas sobre el nombramiento, escribe algo de código que use tu API e intenta leerlo como una oración.
// "If errors is empty..."
if (errors.isEmpty) {
// ...
}
// "Hey, subscription, cancel!"
subscription.cancel();
// "Get the monsters where the monster has claws."
monsters.where((monster) => monster.hasClaws);
// Telling errors to empty itself, or asking if it is?
if (errors.empty) {
// ...
}
// Toggle what? To what?
subscription.toggle();
// Filter the monsters with claws *out* or include *only* those?
monsters.filter((monster) => monster.hasClaws);
Es útil probar tu API y ver cómo se "lee" cuando se usa en el código, pero tampoco hay que exagerar. No es útil agregar artículos y otras partes del discurso para forzar a que tus nombres se lean literalmente como una oración gramaticalmente correcta.
if (theCollectionOfErrors.isEmpty) {
// ...
}
monsters.producesANewSequenceWhereEach((monster) => monster.hasClaws);
PREFIERE una frase nominal para una propiedad o variable no booleana
#El enfoque del lector está en qué es la propiedad. Si al usuario le importa más cómo se determina una propiedad, entonces probablemente debería ser un método con un nombre de frase verbal.
list.length
context.lineWidth
quest.rampagingSwampBeast
list.deleteItems
PREFIERE una frase verbal no imperativa para una propiedad o variable booleana
#Los nombres booleanos se usan a menudo como condiciones en el flujo de control, por lo que querrás un nombre que se lea bien allí. Compara:
if (window.closeable) ... // Adjective.
if (window.canClose) ... // Verb.
Los buenos nombres tienden a comenzar con alguno de estos tipos de verbos:
-
una forma de "ser/estar":
isEnabled,wasShown,willFire. Estos son, por mucho, los más comunes. -
un verbo auxiliar:
hasElements,canClose,shouldConsume,mustSave. -
un verbo activo:
ignoresInput,wroteFile. Estos son raros porque generalmente son ambiguos.loggedResultes un mal nombre porque podría significar "si se registró o no un resultado" o "el resultado que se registró". Del mismo modo,closingConnectionpodría ser "si la conexión se está cerrando" o "la conexión que se está cerrando". Los verbos activos están permitidos cuando el nombre solo puede leerse como un predicado.
Lo que separa todas estas frases verbales de los nombres de métodos es que no son imperativas. Un nombre booleano nunca debería sonar como un comando para decirle al objeto que haga algo, porque acceder a una propiedad no cambia el objeto. (Si la propiedad sí modifica el objeto de una manera significativa, debería ser un método).
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
empty // Adjective or verb?
withElements // Sounds like it might hold elements.
closeable // Sounds like an interface.
// "canClose" reads better as a sentence.
closingWindow // Returns a bool or a window?
showPopup // Sounds like it shows the popup.
CONSIDERA omitir el verbo para un parámetro booleano nombrado
#Esto perfecciona la regla anterior. Para los parámetros nombrados que son booleanos, el nombre suele ser igual de claro sin el verbo, y el código se lee mejor en el lugar de la llamada.
Isolate.spawn(entryPoint, message, paused: false);
var copy = List.from(elements, growable: true);
var regExp = RegExp(pattern, caseSensitive: false);
PREFIERE el nombre "positivo" para una propiedad o variable booleana
#La mayoría de los nombres booleanos tienen formas conceptualmente "positivas" y "negativas" donde la primera se siente como el concepto fundamental y la segunda es su negación: "abierto" y "cerrado", "habilitado" y "deshabilitado", etc. A menudo, el segundo nombre tiene literalmente un prefijo que niega al primero: "visible" e "in-visible", "conectado" y "des-conectado", "cero" y "no-cero".
Al elegir cuál de los dos casos representa true —y, por lo tanto,
por cuál caso se nombra la propiedad— prefiere el positivo o más
fundamental. Los miembros booleanos a menudo se anidan dentro de expresiones lógicas,
incluyendo operadores de negación. Si tu propiedad en sí se lee como una negación,
es más difícil para el lector realizar mentalmente la doble negación y
entender lo que significa el código.
if (socket.isConnected && database.hasData) {
socket.write(database.read());
}
if (!socket.isDisconnected && !database.isEmpty) {
socket.write(database.read());
}
Para algunas propiedades, no hay una forma positiva obvia. ¿Un documento que ha sido guardado en el disco está "guardado" o "sin-cambios"? ¿Un documento que no ha sido guardado está "sin-guardar" o "cambiado"? En casos ambiguos, inclínate por la opción que tenga menos probabilidades de ser negada por los usuarios o que tenga el nombre más corto.
Excepción: Con algunas propiedades, la forma negativa es lo que los usuarios
necesitan usar de manera abrumadora. Elegir el caso positivo los obligaría a
negar la propiedad con ! en todas partes. En su lugar, puede ser mejor usar el
caso negativo para esa propiedad.
PREFIERE una frase verbal imperativa para una función o método cuyo propósito principal sea un efecto secundario
#Los miembros invocables pueden retornar un resultado al invocador y realizar otro trabajo o efectos secundarios. En un lenguaje imperativo como Dart, los miembros suelen llamarse principalmente por su efecto secundario: pueden cambiar el estado interno de un objeto, producir alguna salida o comunicarse con el mundo exterior.
Esos tipos de miembros deben nombrarse usando una frase verbal imperativa que aclare el trabajo que realiza el miembro.
list.add('element');
queue.removeFirst();
window.refresh();
De esta manera, una invocación se lee como una orden para realizar ese trabajo.
PREFIERE una frase nominal o una frase verbal no imperativa para una función o método si retornar un valor es su propósito principal
#Otros miembros invocables tienen pocos efectos secundarios pero retornan un resultado útil al
invocador. Si el miembro no necesita parámetros para hacer eso, generalmente debería ser un
getter. Pero a veces una "propiedad" lógica necesita algunos parámetros. Por ejemplo,
elementAt() retorna un fragmento de datos de una colección, pero necesita un
parámetro para saber qué fragmento de datos retornar.
Esto significa que el miembro es sintácticamente un método, pero conceptualmente es una propiedad, y debe nombrarse como tal usando una frase que describa qué retorna el miembro.
var element = list.elementAt(3);
var first = list.firstWhere(test);
var char = string.codeUnitAt(4);
Esta pauta es deliberadamente más flexible que la anterior. A veces un método
no tiene efectos secundarios pero aun así es más simple de nombrar con una frase verbal como
list.take() o string.split().
CONSIDERA una frase verbal imperativa para una función o método si quieres llamar la atención sobre el trabajo que realiza
#Cuando un miembro produce un resultado sin efectos secundarios, generalmente debería ser un getter o un método con un nombre de frase nominal que describa el resultado que retorna. Sin embargo, a veces el trabajo requerido para producir ese resultado es importante. Puede ser propenso a fallas en tiempo de ejecución o usar recursos pesados como la red o la E/S de archivos. En casos como este, donde quieres que el invocador piense en el trabajo que realiza el miembro, dale al miembro un nombre de frase verbal que describa ese trabajo.
var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();
Ten en cuenta, sin embargo, que esta pauta es más flexible que las dos anteriores. El trabajo que realiza una operación suele ser un detalle de implementación que no es relevante para el invocador, y los límites de rendimiento y robustez cambian con el tiempo. La mayoría de las veces, nombra tus miembros en función de qué hacen por el invocador, no de cómo lo hacen.
EVITA comenzar el nombre de una función o método con get
#
En la mayoría de los casos, el método o función debería ser
un getter al que se le haya quitado get del nombre.
Por ejemplo, en lugar de un método llamado getBreakfastOrder(),
define un getter llamado breakfastOrder.
Incluso si el miembro necesita ser un método porque recibe argumentos o
de lo contrario no encaja bien como getter, deberías evitar get. Como establecen las
pautas anteriores, puedes:
-
Simplemente quitar
gety usar un nombre de frase nominal comobreakfastOrder()si al invocador le importa principalmente el valor que retorna el método. -
Usar un nombre de frase verbal si al invocador le importa el trabajo que se realiza, pero elige un verbo que describa el trabajo con más precisión que
get, comocreate,download,fetch,calculate,request,aggregate, etc.
PREFIERE nombrar un método to___() si copia el estado del objeto a un nuevo objeto
#
Regla de linter: use_to_and_as_if_applicable
Un método de conversión es aquel que retorna un nuevo objeto que contiene una copia de
casi todo el estado del receptor, pero generalmente en alguna forma o
representación diferente. Las librerías principales tienen la convención de que estos métodos se
nombran comenzando con to seguido del tipo de resultado.
Si defines un método de conversión, es útil seguir esa convención.
list.toSet();
stackTrace.toString();
dateTime.toLocal();
PREFIERE nombrar un método as___() si retorna una representación diferente respaldada por el objeto original
#
Regla de linter: use_to_and_as_if_applicable
Los métodos de conversión son "instantáneas". El objeto resultante tiene su propia copia del estado del objeto original. Hay otros métodos similares a conversiones que retornan vistas: proporcionan un nuevo objeto, pero ese objeto hace referencia al original. Los cambios posteriores en el objeto original se reflejan en la vista.
La convención de la librería principal que debes seguir es as___().
var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();
EVITA describir los parámetros en el nombre de la función o método
#El usuario verá el argumento en el lugar de la llamada, por lo que no suele ayudar a la legibilidad referirse a él también en el nombre mismo.
list.add(element);
map.remove(key);
list.addElement(element)
map.removeKey(key)
Sin embargo, puede ser útil mencionar un parámetro para eliminar la ambigüedad con respecto a otros métodos con nombres similares que reciben tipos diferentes:
map.containsKey(key);
map.containsValue(value);
SIGUE las convenciones mnemotécnicas existentes al nombrar parámetros de tipo
#Los nombres de una sola letra no son exactamente esclarecedores, pero casi todos los tipos genéricos los usan. Afortunadamente, la mayoría los usa de una manera mnemotécnica y consistente. Las convenciones son:
-
Epara el tipo de elemento en una colección:biendartclass IterableBase<E> {} class List<E> {} class HashSet<E> {} class RedBlackTree<E> {} -
KyVpara los tipos de clave (key) y valor (value) en una colección asociativa:biendartclass Map<K, V> {} class Multimap<K, V> {} class MapEntry<K, V> {} -
Rpara un tipo utilizado como el tipo de retorno (return) de una función o de los métodos de una clase. Esto no es común, pero aparece en typedefs a veces y en clases que implementan el patrón visitor:biendartabstract class ExpressionVisitor<R> { R visitBinary(BinaryExpression node); R visitLiteral(LiteralExpression node); R visitUnary(UnaryExpression node); } -
De lo contrario, usa
T,SyUpara genéricos que tienen un solo parámetro de tipo y donde el tipo que los rodea hace que su significado sea obvio. Hay múltiples letras aquí para permitir la anidación sin ocultar un nombre circundante. Por ejemplo:biendartclass Future<T> { Future<S> then<S>(FutureOr<S> onValue(T value)) => ... }Aquí, el método genérico
then<S>()usaSpara evitar ocultar laTenFuture<T>.
Si ninguno de los casos anteriores encaja bien, entonces otra letra única como nombre mnemotécnico o un nombre descriptivo está bien:
class Graph<N, E> {
final List<N> nodes = [];
final List<E> edges = [];
}
class Graph<Node, Edge> {
final List<Node> nodes = [];
final List<Edge> edges = [];
}
En la práctica, las convenciones existentes cubren la mayoría de los parámetros de tipo.
Bibliotecas
#Un carácter de subrayado inicial ( _ ) indica que un miembro es privado para su
librería. Esto no es una mera convención, sino que está integrado en el propio lenguaje.
PREFIERE hacer las declaraciones privadas
#Una declaración pública en una librería (ya sea a nivel superior o en una clase) es una señal de que otras librerías pueden y deben acceder a ese miembro. También es un compromiso por parte de tu librería de dar soporte a eso y comportarse adecuadamente cuando suceda.
Si eso no es lo que pretendes, agrega el pequeño _ y sé feliz. Las interfaces públicas estrechas
son más fáciles de mantener para ti y más fáciles de aprender para los usuarios. Como una
agradable ventaja, el analyzer te informará sobre declaraciones privadas no utilizadas para que
puedas eliminar código muerto. No puede hacer eso si el miembro es público porque
no sabe si algún código fuera de su alcance lo está utilizando.
CONSIDERA declarar múltiples clases en la misma librería
#Algunos lenguajes, como Java, vinculan la organización de archivos a la organización de clases: cada archivo solo puede definir una única clase de nivel superior. Dart no tiene esa limitación. Las librerías son entidades distintas separadas de las clases. Está perfectamente bien que una sola librería contenga múltiples clases, variables de nivel superior y funciones si todas pertenecen lógicamente al mismo grupo.
Colocar múltiples clases juntas en una librería puede habilitar algunos patrones útiles. Dado que la privacidad en Dart funciona a nivel de librería, no a nivel de clase, esta es una forma de definir clases "amigas" (friend) como lo harías en C++. Cada clase declarada en la misma librería puede acceder a los miembros privados de las demás, pero el código fuera de esa librería no puede hacerlo.
Por supuesto, esta pauta no significa que debas colocar todas tus clases en una enorme librería monolítica, solo que se te permite colocar más de una clase en una sola librería.
Clases y mixins
#Dart es un lenguaje orientado a objetos "puro" en el sentido de que todos los objetos son instancias de clases. Pero Dart no requiere que todo el código se defina dentro de una clase: puedes definir variables, constantes y funciones de nivel superior al igual que puedes hacerlo en un lenguaje procedimental o funcional.
EVITA definir una clase abstracta de un solo miembro cuando una función simple sea suficiente
#Regla de linter: one_member_abstracts
A diferencia de Java, Dart tiene funciones de primera clase, closures y una sintaxis bastante
ligera para usarlos. Si todo lo que necesitas es algo como un callback, simplemente usa una
función. Si estás definiendo una clase y esta solo tiene un único miembro abstracto
con un nombre sin sentido como call o invoke, es muy probable que
solo quieras una función.
typedef Predicate<E> = bool Function(E element);
abstract class Predicate<E> {
bool test(E element);
}
EVITA definir una clase que contenga solo miembros estáticos
#Regla de linter: avoid_classes_with_only_static_members
In Java y C#, cada definición debe estar dentro de una clase, por lo que es común ver "clases" que existen solo como un lugar para meter miembros estáticos. Otras clases se usan como espacios de nombres: una forma de dar un prefijo compartido a un grupo de miembros para relacionarlos entre sí o evitar una colisión de nombres.
Dart tiene funciones, variables y constantes de nivel superior, por lo que no necesitas una clase solo para definir algo. Si lo que quieres es un espacio de nombres, una librería se adapta mejor. Las librerías admiten prefijos de importación y combinadores de mostrar/ocultar (show/hide). Esas son herramientas poderosas que permiten al consumidor de tu código manejar las colisiones de nombres de la manera que mejor funcione para ellos.
Si una función o variable no está vinculada lógicamente a una clase, colócala en el nivel superior. Si te preocupan las colisiones de nombres, dale un nombre más preciso o muévela a una librería separada que se pueda importar con un prefijo.
DateTime mostRecent(List<DateTime> dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
const _favoriteMammal = 'weasel';
class DateUtils {
static DateTime mostRecent(List<DateTime> dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
}
class _Favorites {
static const mammal = 'weasel';
}
En Dart idóneo, las clases definen tipos de objetos. Un tipo que nunca se instancia es un code smell.
Sin embargo, esta no es una regla estricta. Por ejemplo, con constantes y tipos similares a enums, puede ser natural agruparlos en una clase.
class Color {
static const red = '#f00';
static const green = '#0f0';
static const blue = '#00f';
static const black = '#000';
static const white = '#fff';
}
EVITA extender una clase que no esté diseñada para ser subclaseada
#Si un constructor se cambia de constructor generativo a constructor
factory, cualquier constructor de subclase que llame a ese constructor fallará.
Además, si una clase cambia cuál de sus propios métodos invoca sobre this, eso
puede romper las subclases que sobrescriben esos métodos y esperan que sean llamados
en ciertos puntos.
Ambas cosas significan que una clase debe ser deliberada sobre si desea o no
permitir la creación de subclases. Esto se puede comunicar en un comentario de documentación, o
dando a la clase un nombre obvio como IterableBase. Si el autor de la clase
no hace eso, es mejor asumir que no debes extender la clase.
De lo contrario, los cambios posteriores en ella podrían romper tu código.
USA modificadores de clase para controlar si tu clase se puede extender
#Los modificadores de clase como final, interface o sealed
restringen cómo se puede extender una clase.
Por ejemplo, usa final class A {} o interface class B {}
para evitar
la extensión fuera de la librería actual.
Usa estos modificadores para comunicar tu intención, en lugar de confiar en la documentación.
EVITA implementar una clase que no esté diseñada para ser una interfaz
#Las interfaces implícitas son una herramienta poderosa en Dart para evitar tener que repetir el contrato de una clase cuando este se puede inferir trivialmente de las firmas de una implementación de ese contrato.
Pero implementar la interfaz de una clase representa un acoplamiento muy fuerte con esa clase. Significa que prácticamente cualquier cambio en la clase cuya interfaz estás implementando romperá tu implementación. Por ejemplo, agregar un nuevo miembro a una clase suele ser un cambio seguro y no destructivo. Pero si estás implementando la interfaz de esa clase, ahora tu clase tiene un error estático porque carece de una implementación de ese nuevo método.
Los mantenedores de librerías necesitan la capacidad de evolucionar las clases existentes sin perjudicar a los usuarios. Si tratas cada clase como si expusiera una interfaz que los usuarios son libres de implementar, entonces cambiar esas clases se vuelve muy difícil. Esa dificultad, a su vez, significa que las librerías en las que confías tardan más en crecer y adaptarse a las nuevas necesidades.
Para dar más margen de maniobra a los autores de las clases que utilizas, evita implementar interfaces implícitas, excepto para clases que estén claramente diseñadas para ser implementadas. De lo contrario, puedes introducir un acoplamiento que el autor no pretende y podrían romper tu código sin darse cuenta.
USA modificadores de clase para controlar si tu clase puede ser una interfaz
#Al diseñar una librería, usa modificadores de clase como final, base o sealed
para forzar el uso
previsto. Por ejemplo, usa final class C {} o base class D {}
para evitar la
implementación fuera de la librería actual.
Aunque es ideal que todas las librerías usen estos modificadores para hacer cumplir la intención del diseño,
los desarrolladores pueden encontrar casos en los que no se aplican. En tales casos, ten cuidado con
problemas de implementación no deseados.
PREFIERE definir un mixin puro o una class pura a una mixin class
#
Regla de linter: prefer_mixin
Dart anteriormente (en las versiones del lenguaje 2.12 a la 2.19) permitía que cualquier clase que cumpliera ciertas restricciones (sin constructor no predeterminado, sin superclase, etc.) se mezclara (mix into) en otras clases. Esto era confuso porque el autor de la clase podría no haber tenido la intención de que se usara como mezcla.
Dart 3.0.0 ahora requiere que cualquier tipo que se pretenda mezclar en otras clases,
además de ser tratado como una clase normal, deba declararse explícitamente como tal con
la declaración mixin class.
Sin embargo, los tipos que necesitan ser tanto un mixin como una clase deberían ser un caso raro.
La declaración mixin class está pensada principalmente para ayudar a migrar clases anteriores a la 3.0.0
que se utilizaban como mixins a una declaración más explícita. El nuevo código debería definir
claramente el comportamiento y la intención de sus declaraciones usando únicamente declaraciones de mixin puro
o de class pura, y evitar la ambigüedad de las clases mixin.
Lee Migración de clases como mixins
para obtener más orientación sobre las declaraciones mixin y mixin class.
Constructores
#Los constructores de Dart se crean declarando una función con el mismo nombre que la clase y, opcionalmente, un identificador adicional. Estos últimos se denominan constructores nombrados.
CONSIDERA hacer tu constructor const si la clase lo admite
#
Si tienes una clase donde todos los campos son final y el constructor no hace
nada más que inicializarlos, puedes hacer que ese constructor sea const. Eso permite
a los usuarios crear instancias de tu clase en lugares donde se requieren constantes:
dentro de otras constantes más grandes, casos de switch, valores de parámetros por defecto,
etc.
Si no lo haces explícitamente const, no podrán hacer eso.
Ten en cuenta, sin embargo, que un constructor const es un compromiso en tu API pública. Si
más tarde cambias el constructor a no-const, romperás a los usuarios que lo estén
llamando en expresiones constantes. Si no quieres comprometerte a eso, no
lo hagas const. En la práctica, los constructores const son más útiles para tipos simples,
inmutables y similares a valores.
Miembros
#Un miembro pertenece a un objeto y puede constar de métodos o variables de instancia.
PREFIERE hacer los campos y variables de nivel superior final
#
Regla de linter: prefer_final_fields
El estado que no es mutable (que no cambia con el tiempo) es
más fácil de razonar para los programadores. Las clases y librerías que minimizan la
cantidad de estado mutable con el que trabajan tienden a ser más fáciles de mantener.
Por supuesto, a menudo es útil tener datos mutables. Pero si no los necesitas,
tu opción por defecto debería ser hacer los campos y las variables de nivel superior final
cuando puedas.
A veces, un campo de instancia no cambia después de haber sido inicializado, pero
no puede inicializarse hasta después de que se construya la instancia. Por ejemplo, puede
necesitar hacer referencia a this o a algún otro campo de la instancia. En casos como
ese, considera hacer el campo late final. Cuando lo hagas, es posible que también puedas
inicializar el campo en su declaración.
USA getters para operaciones que conceptualmente accedan a propiedades
#Decidir cuándo un miembro debe ser un getter frente a un método es una parte sutil pero
importante de un buen diseño de API, de ahí esta pauta tan larga.
Las culturas de algunos otros lenguajes evitan los getters. Solo los usan cuando
la operación es casi exactamente como un campo: realiza una cantidad minúscula de
cálculo sobre el estado que reside completamente en el objeto. Cualquier cosa más compleja o
pesada que eso obtiene () después del nombre para indicar "¡se está realizando un cálculo
aquí!" porque un nombre solo después de un . significa "campo".
En Dart no es así. En Dart, todos los nombres con punto son invocaciones de miembros que pueden realizar cálculos. Los campos son especiales: son getters cuya implementación es provista por el lenguaje. En otras palabras, los getters no son "campos particularmente lentos" en Dart; los campos son "getters particularmente rápidos".
Aun así, elegir un getter en lugar de un método envía una señal importante al invocador. La señal, a grandes rasgos, es que la operación es "similar a un campo". La operación, al menos en principio, podría implementarse usando un campo, por lo que sabe el invocador. Eso implica:
La operación no recibe ningún argumento y retorna un resultado.
-
Al invocador le importa principalmente el resultado. Si quieres que el invocador se preocupe por cómo la operación produce su resultado más de lo que lo hace por el resultado que se produce, dale a la operación un nombre de verbo que describa el trabajo y conviértela en un método.
Esto no significa que la operación deba ser particularmente rápida para ser un getter.
IterableBase.lengthesO(n), y eso está bien. Está bien que un getter realice un cálculo significativo. Pero si realiza una cantidad de trabajo sorprendente, es posible que desees llamar su atención sobre eso convirtiéndola en un método cuyo nombre sea un verbo que describa lo que hace.maldartconnection.nextIncomingMessage; // Does network I/O. expression.normalForm; // Could be exponential to calculate. -
La operación no tiene efectos secundarios visibles para el usuario. Acceder a un campo real no altera el objeto ni ningún otro estado del programa. No produce salida, ni escribe archivos, etc. Un getter tampoco debería hacer esas cosas.
La parte "visible para el usuario" es importante. Está bien que los getters modifiquen un estado oculto o produzcan efectos secundarios fuera de banda. Los getters pueden calcular de forma diferida y almacenar su resultado, escribir en una caché, registrar datos (logs), etc. Siempre y cuando al invocador no le importe el efecto secundario, probablemente esté bien.
maldartstdout.newline; // Produces output. list.clear; // Modifies object. -
La operación es idempotente. "Idempotente" es una palabra extraña que, en este contexto, básicamente significa que llamar a la operación varias veces produce el mismo resultado cada vez, a menos que algún estado se modifique explícitamente entre esas llamadas. (Obviamente,
list.lengthproduce resultados diferentes si agregas un elemento a la lista entre llamadas)."Mismo resultado" aquí no significa que un getter deba producir literalmente un objeto idéntico en llamadas sucesivas. Requerir eso obligaría a muchos getters a tener un almacenamiento en caché frágil, lo que niega todo el propósito de usar un getter. Es común, y está perfectamente bien, que un getter retorne un nuevo Future o lista cada vez que lo llamas. La parte importante es que el Future se complete con el mismo valor y la lista contenga los mismos elementos.
En otras palabras, el valor del resultado debería ser el mismo en los aspectos que al invocador le importan.
maldartDateTime.now; // New result each time. -
El objeto resultante no expone todo el estado del objeto original. Un campo expone solo una parte de un objeto. Si tu operación retorna un resultado que expone todo el estado del objeto original, probablemente sea mejor que sea un método
to___()oas___().
Si todo lo anterior describe tu operación, debería ser un getter. Parece que pocos miembros sobrevivirían a esa prueba, pero sorprendentemente muchos lo hacen. Muchas operaciones simplemente realizan algún cálculo sobre algún estado y la mayoría de ellas pueden y deben ser getters.
rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;
USA setters para operaciones que conceptualmente cambien propiedades
#Regla de linter: use_setters_to_change_properties
Decidir entre un setter frente a un método es similar a decidir entre un getter frente a un método. In ambos casos, la operación debe ser "similar a un campo".
Para un setter, "similar a un campo" significa:
-
La operación toma un solo argumento y no produce un valor de resultado.
La operación cambia algún estado en el objeto.
-
La operación es idempotente. Llamar al mismo setter dos veces con el mismo valor no debería hacer nada la segunda vez en lo que respecta al invocador. Internalmente, tal vez tengas alguna invalidación de caché o registro de logs. Eso está bien. Pero desde la perspectiva del invocador, parece que la segunda llamada no hace nada.
rectangle.width = 3;
button.visible = false;
NO definas un setter sin un getter correspondiente
#Regla de linter: avoid_setters_without_getters
Los usuarios piensan en los getters y setters como propiedades visibles de un objeto. Una
propiedad tipo "buzón" (dropbox) en la que se puede escribir pero que no se puede ver es confusa y
contradice su intuición sobre cómo funcionan las propiedades. Por ejemplo, un setter
sin un getter significa que puedes usar = para modificarlo, pero no +=.
Esta pauta no significa que debas agregar un getter solo para permitir el setter que deseas agregar. Por lo general, los objetos no deberían exponer más estado del que necesitan. Si tienes alguna parte del estado de un objeto que se puede modificar pero no se puede exponer de la misma manera, usa un método en su lugar.
EVITA usar pruebas de tipo en tiempo de ejecución para simular la sobrecarga
#Es común que una API admita operaciones similares en diferentes tipos de parámetros. Para enfatizar la similitud, algunos lenguajes admiten la sobrecarga (overloading), que te permite definir múltiples métodos que tienen el mismo nombre pero diferentes listas de parámetros. En el tiempo de compilación, el compilador analiza los tipos de argumentos reales para determinar qué método llamar.
Dart no tiene sobrecarga.
Puedes definir una API que parezca sobrecarga
definiendo un solo método y luego usando pruebas de tipo is
dentro del cuerpo para analizar los tipos en tiempo de ejecución de los argumentos y realizar el
comportamiento adecuado.
Sin embargo, simular la sobrecarga de esta manera convierte una selección de método en tiempo de compilación
en una elección que ocurre en tiempo de ejecución.
Si los invocadores suelen saber qué tipo tienen y qué operación específica desean, es mejor definir métodos separados con nombres diferentes para permitir que los invocadores seleccionen la operación correcta. Esto proporciona una mejor comprobación de tipos estática y un rendimiento más rápido, ya que evita cualquier prueba de tipo en tiempo de ejecución.
Sin embargo, si los usuarios pudieran tener un objeto de un tipo desconocido
y desean que la API use internamente is para elegir la operación correcta,
entonces un solo método donde el parámetro sea un supertipo
de todos los tipos admitidos podría ser razonable.
EVITA campos late final públicos sin inicializadores
#
A diferencia de otros campos final, un campo late final sin un inicializador sí
define un setter. Si ese campo es público, entonces el setter es público. Esto es
rara vez lo que deseas. Los campos generalmente se marcan como late para que puedan inicializarse
internamente en algún momento del ciclo de vida de la instancia, a menudo dentro
del cuerpo del constructor.
A menos que sí desees que los usuarios llamen al setter, es mejor elegir una de las siguientes soluciones:
- No uses
late. - Usa un constructor factory para calcular los valores de los campos
final. - Usa
late, pero inicializa el campolateen su declaración. - Usa
late, pero haz que el campolatesea privado y define un getter público para él.
EVITA retornar tipos Future, Stream y de colección anulables
#
Cuando una API retorna un tipo contenedor, tiene dos formas de indicar la ausencia de
datos: puede retornar un contenedor vacío o puede retornar null. Los usuarios generalmente
asumen y prefieren que uses un contenedor vacío para indicar "sin datos". De
esa manera, tienen un objeto real sobre el cual pueden llamar a métodos como isEmpty.
Para indicar que tu API no tiene datos que proporcionar, prefiere retornar una colección vacía, un Future no anulable de un tipo anulable, o un Stream que no emita ningún valor.
Excepción: Si retornar null significa algo diferente a producir
un contenedor vacío, podría tener sentido usar un tipo anulable.
EVITA retornar this desde los métodos solo para habilitar una interfaz fluida
#
Regla de linter: avoid_returning_this
Las cascadas de métodos son una mejor solución para encadenar llamadas a métodos.
var buffer =
StringBuffer()
..write('one')
..write('two')
..write('three');
var buffer =
StringBuffer()
.write('one')
.write('two')
.write('three');
Tipos
#Cuando escribes un tipo en tu programa, restringes los tipos de valores que fluyen hacia diferentes partes de tu código. Los tipos pueden aparecer en dos tipos de lugares: anotaciones de tipo en las declaraciones y argumentos de tipo en invocaciones genéricas.
Las anotaciones de tipo son en lo que normalmente piensas cuando piensas en "tipos estáticos".
Puedes anotar el tipo de una variable, parámetro, campo o tipo de retorno. En
el siguiente ejemplo, bool y String son anotaciones de tipo. Dependen de
la estructura declarativa estática del código y no se "ejecutan" en tiempo de ejecución.
bool isEmpty(String parameter) {
bool result = parameter.isEmpty;
return result;
}
Una invocación genérica es un literal de colección, una llamada al constructor de una clase
genérica o una invocación de un método genérico. En el siguiente ejemplo, num
e int son argumentos de tipo en invocaciones genéricas. Aunque son tipos,
son entidades de primera clase que se reifican y se pasan a la invocación en tiempo de
ejecución.
var lists = <num>[1, 2];
lists.addAll(List<num>.filled(3, 4));
lists.cast<int>();
Hacemos hincapié en la parte de "invocación genérica" aquí, porque los argumentos de tipo también pueden aparecer en las anotaciones de tipo:
List<int> ints = [1, 2];
Aquí, int es un argumento de tipo, pero aparece dentro de una anotación de tipo, no en una
invocación genérica. Por lo general, no tienes que preocuparte por esta distinción, pero
en un par de lugares tenemos pautas diferentes para cuando se usa un tipo en una
invocación genérica en lugar de una anotación de tipo.
Inferencia de tipos
#Las anotaciones de tipo son opcionales en Dart.
Si omites una, Dart intenta inferir un tipo
basándose en el contexto cercano. A veces no tiene suficiente información para
inferir un tipo completo. Cuando eso sucede, Dart a veces informa un error, pero
por lo general completa silenciosamente las partes faltantes con dynamic. El
dynamic implícito conduce a código que parece inferido y seguro, pero que en realidad deshabilita
la comprobación de tipos por completo. Las reglas siguientes evitan eso al requerir tipos cuando
la inferencia falla.
El hecho de que Dart tenga tanto inferencia de tipos como un tipo dynamic genera cierta
confusión sobre lo que significa decir que el código es "sin tipo". ¿Significa eso que el código
está tipado dinámicamente o que no escribiste el tipo? Para evitar esa
confusión, evitamos decir "sin tipo" y en su lugar usamos la siguiente terminología:
-
Si el código está anotado con tipo, el tipo se escribió explícitamente en el código.
-
Si el código es inferido, no se escribió ninguna anotación de tipo y Dart descubrió con éxito el tipo por sí mismo. La inferencia puede fallar, en cuyo caso las pautas no consideran eso como inferido.
-
Si el código es dynamic, entonces su tipo estático es el tipo especial
dynamic. El código se puede anotar explícitamente comodynamico se puede inferir.
En otras palabras, el hecho de que un código esté anotado o sea inferido es ortogonal a
si es dynamic o algún otro tipo.
La inferencia es una herramienta poderosa que te ahorra el esfuerzo de escribir y leer tipos que son obvios o poco interesantes. Mantiene la atención del lector enfocada en el comportamiento del código en sí. Los tipos explícitos también son una parte clave para un código robusto y mantenible. Definen la forma estática de una API y crean límites para documentar y hacer cumplir qué tipos de valores se permiten llegar a diferentes partes del programa.
Por supuesto, la inferencia no es magia. A veces la inferencia tiene éxito y selecciona un tipo, pero no es el tipo que deseas. El caso común es inferir un tipo demasiado preciso a partir del inicializador de una variable cuando pretendes asignar valores de otros tipos a la variable más adelante. En esos casos, tienes que escribir el tipo explícitamente.
Las pautas aquí logran el mejor equilibrio que hemos encontrado entre brevedad y control, flexibilidad y seguridad. Hay pautas específicas para cubrir todos los diversos casos, pero el resumen aproximado es:
-
Anota cuando la inferencia no tenga suficiente contexto, incluso cuando
dynamicsea el tipo que deseas. No anotes variables locales e invocaciones genéricas a menos que lo necesites.
-
Prefiere anotar las variables y los campos de nivel superior a menos que el inicializador haga que el tipo sea obvio.
ANOTA el tipo en variables sin inicializadores
#Regla de linter: prefer_typing_uninitialized_variables
El tipo de una variable (de nivel superior, local, campo estático o campo de instancia) a menudo puede inferirse de su inicializador. Sin embargo, si no hay inicializador, la inferencia falla.
List<AstNode> parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
var parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
ANOTA el tipo en campos y variables de nivel superior si el tipo no es obvio
#Regla de linter: type_annotate_public_apis
Las anotaciones de tipo son documentación importante sobre cómo se debe usar una librería. Forman límites entre regiones de un programa para aislar el origen de un error de tipo. Considera:
install(id, destination) => ...
Aquí, no está claro qué es id. ¿Una cadena (string)? ¿Y qué es destination? ¿Una cadena
o un objeto File? ¿Es este método síncrono o asíncrono? Esto es más claro:
Future<bool> install(PackageId id, String destination) => ...
Sin embargo, en algunos casos el tipo es tan obvio que escribirlo no tiene sentido:
const screenWidth = 640; // Inferred as int.
"Obvio" no está definido con precisión, pero todos estos son buenos candidatos:
- Literales.
- Invocaciones de constructores.
- Referencias a otras constantes que están tipadas explícitamente.
- Expresiones simples en números y cadenas.
- Métodos factory como
int.parse(),Future.wait(), etc. con los que se espera que los lectores estén familiarizados.
Si crees que la expresión del inicializador (sea cual sea) es suficientemente clara, puedes omitir la anotación. Pero si crees que anotar ayuda a que el código sea más claro, agrégala.
Cuando tengas dudas, añade una anotación de tipo. También puedes anotar explícitamente tipos obvios. Si el tipo inferido depende de valores o declaraciones de otras librerías, quizás desees anotar el tipo en tu declaración para que un cambio en esa otra librería no cambie silenciosamente el tipo de tu propia API sin que te des cuenta.
Esta regla se aplica tanto a las declaraciones públicas como a las privadas. Así como las anotaciones de tipo en las APIs ayudan a los usuarios de tu código, los tipos en los miembros privados ayudan a los mantenedores.
NO anotes el tipo de forma redundante en variables locales inicializadas
#Regla de linter: omit_local_variable_types
Las variables locales, especialmente en el código moderno donde las funciones tienden a ser pequeñas, tienen muy poco alcance. Omitir el tipo enfoca la atención del lector en el nombre más importante de la variable y su valor inicializado.
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
var desserts = <List<Ingredient>>[];
for (final recipe in cookbook) {
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
List<List<Ingredient>> desserts = <List<Ingredient>>[];
for (final List<Ingredient> recipe in cookbook) {
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
A veces, el tipo inferido no es el tipo que deseas que tenga la variable. Por ejemplo, puedes tener la intención de asignar valores de otros tipos más adelante. En ese caso, anota la variable con el tipo que deseas.
Widget build(BuildContext context) {
Widget result = Text('You won!');
if (applyPadding) {
result = Padding(padding: EdgeInsets.all(8.0), child: result);
}
return result;
}
ANOTA los tipos de retorno en las declaraciones de funciones
#Dart generalmente no infiere el tipo de retorno de una declaración de función a partir de su cuerpo, a diferencia de otros lenguajes. Eso significa que debes escribir una anotación de tipo para el tipo de retorno tú mismo.
String makeGreeting(String who) {
return 'Hello, $who!';
}
makeGreeting(String who) {
return 'Hello, $who!';
}
Ten en cuenta que esta pauta solo se aplica a declaraciones de funciones no locales: métodos y getters de nivel superior, estáticos y de instancia. Las funciones locales y las expresiones de funciones anónimas infieren un tipo de retorno de su cuerpo. De hecho, la sintaxis de funciones anónimas ni siquiera permite una anotación de tipo de retorno.
ANOTA los tipos de parámetros en las declaraciones de funciones
#La lista de parámetros de una función determina su límite con el mundo exterior. Anotar los tipos de los parámetros hace que ese límite esté bien definido. Ten en cuenta que aunque los valores por defecto de los parámetros parecen inicializadores de variables, Dart no infiere el tipo de un parámetro opcional a partir de su valor por defecto.
void sayRepeatedly(String message, {int count = 2}) {
for (var i = 0; i < count; i++) {
print(message);
}
}
void sayRepeatedly(message, {count = 2}) {
for (var i = 0; i < count; i++) {
print(message);
}
}
Excepción: Las expresiones de función y los parámetros formales de inicialización tienen diferentes convenciones de anotación de tipo, como se describe en las dos pautas siguientes.
NO anotes los tipos de parámetros inferidos en expresiones de funciones
#Regla de linter: avoid_types_on_closure_parameters
Las funciones anónimas casi siempre se pasan inmediatamente a un método que recibe un
callback de algún tipo.
Cuando se crea una expresión de función en un contexto tipado,
Dart intenta inferir los tipos de parámetros de la función basándose en el tipo esperado.
Por ejemplo, cuando pasas una expresión de función a Iterable.map(), el
tipo de parámetro de tu función se infiere basándose en el tipo de callback que espera map():
var names = people.map((person) => person.name);
var names = people.map((Person person) => person.name);
Si el lenguaje puede inferir el tipo que deseas para un parámetro en una expresión de función, entonces no lo anotes. En casos raros, el contexto circundante no es lo suficientemente preciso como para proporcionar un tipo para uno o más de los parámetros de la función. En esos casos, es posible que necesites anotar el tipo. (Si la función no se usa de inmediato, generalmente es mejor convertirla en una declaración nombrada).
NO anotes el tipo en parámetros formales de inicialización
#Regla de linter: type_init_formals
Si el parámetro de un constructor usa this. para inicializar un campo,
o super. para reenviar un parámetro de la superclase,
entonces se infiere que el tipo del parámetro
tiene el mismo tipo que
el campo o el parámetro del superconstructor respectivamente.
class Point {
double x, y;
Point(this.x, this.y);
}
class MyWidget extends StatelessWidget {
MyWidget({super.key});
}
class Point {
double x, y;
Point(double this.x, double this.y);
}
class MyWidget extends StatelessWidget {
MyWidget({Key? super.key});
}
ESCRIBE argumentos de tipo en invocaciones genéricas que no se infieran
#Dart es bastante inteligente a la hora de inferir argumentos de tipo en las invocaciones genéricas. Analiza el tipo esperado donde ocurre la expresión y los tipos de los valores que se pasan a la invocación. Sin embargo, a veces eso no es suficiente para determinar completamente un argumento de tipo. En ese caso, escribe la lista completa de argumentos de tipo explícitamente.
var playerScores = <String, int>{};
final events = StreamController<Event>();
var playerScores = {};
final events = StreamController();
A veces, la invocación ocurre como el inicializador de una declaración de variable. Si la variable no es local, en lugar de escribir la lista de argumentos de tipo en la invocación misma, puedes colocar una anotación de tipo en la declaración:
class Downloader {
final Completer<String> response = Completer();
}
class Downloader {
final response = Completer();
}
Anotar la variable también aborda esta pauta porque ahora los argumentos de tipo sí se infieren.
NO escribas argumentos de tipo en invocaciones genéricas que se infieran
#Esto es lo inverso de la regla anterior. Si la lista de argumentos de tipo de una invocación se infiere correctamente con los tipos que deseas, entonces omite los tipos y deja que Dart haga el trabajo por ti.
class Downloader {
final Completer<String> response = Completer();
}
class Downloader {
final Completer<String> response = Completer<String>();
}
Aquí, la anotación de tipo en el campo proporciona un contexto circundante para inferir el argumento de tipo de la llamada al constructor en el inicializador.
var items = Future.value([1, 2, 3]);
var items = Future<List<int>>.value(<int>[1, 2, 3]);
Aquí, los tipos de la colección y de la instancia pueden inferirse de abajo hacia arriba a partir de sus elementos y argumentos.
EVITA escribir tipos genéricos incompletos
#El objetivo de escribir una anotación de tipo o un argumento de tipo es fijar un tipo completo. Sin embargo, si escribes el nombre de un tipo genérico pero omites sus argumentos de tipo, no has especificado completamente el tipo. En Java, estos se denominan "tipos crudos" (raw types). Por ejemplo:
List numbers = [1, 2, 3];
var completer = Completer<Map>();
Aquí, numbers tiene una anotación de tipo, pero la anotación no proporciona un argumento de
tipo para la clase genérica List. Del mismo modo, el argumento de tipo Map para
Completer
no está completamente especificado. En casos como este, Dart no intentará "completar" el
resto del tipo por ti utilizando el contexto circundante. En su lugar, completa silenciosamente
cualquier argumento de tipo que falte con dynamic (o con el límite (bound) si la
clase tiene uno). Eso rara vez es lo que deseas.
En su lugar, si estás escribiendo un tipo genérico ya sea en una anotación de tipo o como un argumento de tipo dentro de alguna invocación, asegúrate de escribir un tipo completo:
List<num> numbers = [1, 2, 3];
var completer = Completer<Map<String, int>>();
ANOTA con dynamic en lugar de permitir que la inferencia falle
#
Cuando la inferencia no completa un tipo, generalmente se establece por defecto en dynamic. Si
dynamic es el tipo que deseas, esta es técnicamente la forma más concisa de obtenerlo.
Sin embargo, no es la forma más clara. Un lector casual de tu código que
vea que falta una anotación no tiene forma de saber si tenías la intención de que fuera
dynamic, si esperabas que la inferencia completara algún otro tipo, o si simplemente olvidaste
escribir la anotación.
Cuando dynamic sea el tipo que deseas, escríbelo explícitamente para que tu intención sea clara
y para resaltar que este código tiene menos seguridad estática.
dynamic mergeJson(dynamic original, dynamic changes) => ...
mergeJson(original, changes) => ...
Ten en cuenta que está bien omitir el tipo cuando Dart infiere con éxito dynamic.
Map<String, dynamic> readJson() => ...
void printUsers() {
var json = readJson();
var users = json['users'];
print(users);
}
Aquí, Dart infiere Map<String, dynamic> para json y luego a partir de eso infiere
dynamic para users. Está bien dejar users
sin una anotación de tipo. La
distinción es un poco sutil. Está bien permitir que la inferencia propague
dynamic a través de tu código a partir de una anotación de tipo dynamic en otro lugar, pero
no deseas que inyecte una anotación de tipo dynamic en un lugar donde tu
código no especificó una.
Excepción: Las anotaciones de tipo en parámetros no utilizados (_) se pueden omitir.
PREFIERE firmas en anotaciones de tipo de función
#El identificador Function por sí solo, sin ningún tipo de retorno ni firma de
parámetro, hace referencia al tipo especial Function. Este tipo es solo
marginalmente más útil que usar dynamic. Si vas a realizar una anotación, prefiere
un tipo de función completo que incluya los parámetros y el tipo de retorno de la
función.
bool isValid(String value, bool Function(String) test) => ...
bool isValid(String value, Function test) => ...
Excepción: A veces, deseas un tipo que represente la unión de múltiples
tipos de funciones diferentes. Por ejemplo, puedes aceptar una función que toma un
parámetro o una función que toma dos. Como no tenemos tipos de unión, no hay
forma de tipar eso con precisión y normalmente tendrías que usar dynamic.
Function es, al menos, un poco más útil que eso:
void handleError(void Function() operation, Function errorHandler) {
try {
operation();
} catch (err, stack) {
if (errorHandler is Function(Object)) {
errorHandler(err);
} else if (errorHandler is Function(Object, StackTrace)) {
errorHandler(err, stack);
} else {
throw ArgumentError('errorHandler has wrong signature.');
}
}
}
NO especifiques un tipo de retorno para un setter
#Regla de linter: avoid_return_types_on_setters
Los setters siempre retornan void en Dart. Escribir esa palabra no tiene sentido.
void set foo(Foo value) {
...
}
set foo(Foo value) {
...
}
NO uses la sintaxis heredada de typedef
#Regla de linter: prefer_generic_function_type_aliases
Dart tiene dos notaciones para definir un typedef nombrado para un tipo de función. La sintaxis original se ve así:
typedef int Comparison<T>(T a, T b);
Esa sintaxis tiene un par de problemas:
-
No hay forma de asignar un nombre a un tipo de función genérico. En el ejemplo anterior, el propio typedef es genérico. Si haces referencia a
Comparisonen tu código, sin un argumento de tipo, obtienes implícitamente el tipo de funciónint Function(dynamic, dynamic), noint Function<T>(T, T). Esto no ocurre a menudo en la práctica, pero importa en ciertos casos extremos (corner cases). -
Un único identificador en un parámetro se interpreta como el nombre del parámetro, no como su tipo. Dado:
maldarttypedef bool TestNumber(num);La mayoría de los usuarios esperan que este sea un tipo de función que recibe un
numy retornabool. En realidad, es un tipo de función que recibe cualquier objeto (dynamic) y retornabool. El nombre del parámetro (que no se usa para nada excepto como documentación en el typedef) es "num". Esta ha sido una fuente constante de errores en Dart durante mucho tiempo.
La nueva sintaxis se ve así:
typedef Comparison<T> = int Function(T, T);
Si quieres incluir el nombre de un parámetro, también puedes hacerlo:
typedef Comparison<T> = int Function(T a, T b);
La nueva sintaxis puede expresar todo lo que la antigua sintaxis podía expresar y más, y
carece de la característica errónea donde un único identificador se trata como el
nombre del parámetro en lugar de su tipo. La misma sintaxis de tipo de función después de la
= en el typedef también se permite en cualquier lugar donde pueda aparecer una anotación de tipo, lo que
nos brinda una forma única y consistente de escribir tipos de funciones en cualquier parte de un programa.
La antigua sintaxis de typedef todavía se admite para evitar romper el código existente, pero está obsoleta (deprecated).
PREFIERE tipos de función inline sobre typedefs
#Regla de linter: avoid_private_typedef_functions
En Dart, si deseas usar un tipo de función para un campo, variable o argumento de tipo genérico, puedes definir un typedef para el tipo de función. Sin embargo, Dart admite una sintaxis de tipo de función inline que se puede usar en cualquier lugar donde se permita una anotación de tipo:
class FilteredObservable {
final bool Function(Event) _predicate;
final List<void Function(Event)> _observers;
FilteredObservable(this._predicate, this._observers);
void Function(Event)? notify(Event event) {
if (!_predicate(event)) return null;
void Function(Event)? last;
for (final observer in _observers) {
observer(event);
last = observer;
}
return last;
}
}
Aún puede valer la pena definir un typedef si el tipo de función es particularmente largo o se usa con frecuencia. Pero en la mayoría de los casos, los usuarios quieren ver cuál es realmente el tipo de función justo donde se usa, y la sintaxis del tipo de función les brinda esa claridad.
PREFIERE usar la sintaxis de tipo de función para los parámetros
#Regla de linter: use_function_type_syntax_for_parameters
Dart tiene una sintaxis especial al definir un parámetro cuyo tipo es una función. Algo parecido a C, rodeas el nombre del parámetro con el tipo de retorno y la firma del parámetro de la función:
Iterable<T> where(bool predicate(T element)) => ...
Antes de que Dart agregara la sintaxis de tipo de función, esta era la única forma de dar a un parámetro un tipo de función sin definir un typedef. Ahora que Dart tiene una notación general para tipos de funciones, también puedes usarla para parámetros de tipo función:
Iterable<T> where(bool Function(T) predicate) => ...
La nueva sintaxis es un poco más verbosa, pero es consistente con otras ubicaciones donde debes usar la nueva sintaxis.
EVITA usar dynamic a menos que quieras deshabilitar la comprobación estática
#
Algunas operaciones funcionan con cualquier objeto posible. Por ejemplo, un método log()
podría recibir cualquier objeto y llamar a toString() en él. Dos tipos en Dart permiten todos
los valores: Object? y dynamic. Sin embargo, transmiten cosas diferentes. Si
simplemente deseas indicar que permites todos los objetos, usa Object?. Si deseas
permitir todos los objetos excepto null, usa Object.
El tipo dynamic no solo acepta todos los objetos, sino que también permite todas las
operaciones. Se permite cualquier acceso a un miembro en un valor de tipo dynamic en tiempo de
compilación, pero puede fallar y lanzar una excepción en tiempo de ejecución. Si deseas
exactamente ese despacho dinámico riesgoso pero flexible, entonces dynamic es el tipo
correcto de usar.
De lo contrario, prefiere usar Object? u Object. Confía en las comprobaciones is y en la
promoción de tipos para
asegurarte de que el tipo en tiempo de ejecución del valor admita el miembro al que deseas acceder
antes de acceder a él.
/// Returns a Boolean representation for [arg], which must
/// be a String or bool.
bool convertToBool(Object arg) {
if (arg is bool) return arg;
if (arg is String) return arg.toLowerCase() == 'true';
throw ArgumentError('Cannot convert $arg to a bool.');
}
La principal excepción a esta regla es al trabajar con APIs existentes que usan
dynamic, especialmente dentro de un tipo genérico. Por ejemplo, los objetos JSON tienen el tipo
Map<String, dynamic> y tu código deberá aceptar ese mismo tipo. Aun
así, al usar un valor de una de estas APIs, a menudo es una buena idea castearlo
a un tipo más preciso antes de acceder a los miembros.
USA Future<void> como el tipo de retorno de miembros asíncronos que no producen valores
#
Cuando tienes una función síncrona que no retorna un valor, usas void
como tipo de retorno. El equivalente asíncrono para un método que no
produce un valor, pero que el invocador podría necesitar esperar (await), es Future<void>.
Es posible que veas código que usa Future o Future<Null> en su lugar porque las versiones
anteriores de Dart no permitían void como argumento de tipo. Ahora que sí lo hace,
deberías usarlo. Hacerlo coincide más directamente con la forma en que tiparías una función
síncrona similar y te brinda una mejor comprobación de errores para los invocadores y en el
cuerpo de la función.
Para funciones asíncronas que no retornan un valor útil y donde ningún
invocador necesite esperar (await) el trabajo asíncrono o manejar un fallo asíncrono,
usa un tipo de retorno de void.
EVITA usar FutureOr<T> como un tipo de retorno
#
Si un método acepta un FutureOr<int>, es generoso en lo que
acepta. Los usuarios pueden llamar al método con un int o con un
Future<int>, por lo que no necesitan envolver un int en un
Future que de todos modos vas a desenvolver.
Si retornas un FutureOr<int>, los usuarios deben verificar si recuperan un int
o un Future<int> antes de poder hacer algo útil. (O simplemente harán await
al valor, tratándolo efectivamente siempre como un Future). Simplemente retorna un
Future<int>, es más limpio. Es más fácil para los usuarios entender que una función
es siempre asíncrona o siempre síncrona, pero una función que puede ser
ambas cosas es difícil de usar correctamente.
Future<int> triple(FutureOr<int> value) async => (await value) * 3;
FutureOr<int> triple(FutureOr<int> value) {
if (value is int) return value * 3;
return value.then((v) => v * 3);
}
La formulación más precisa de esta pauta es usar únicamente FutureOr<T> en
posiciones contravariantes. Los parámetros son contravariantes y los tipos de retorno son
covariantes. En los tipos de funciones anidadas, esto se invierte: si tienes un
parámetro cuyo tipo es en sí mismo una función, entonces el tipo de retorno del callback está
ahora en posición contravariante y los parámetros del callback son covariantes. Esto
significa que está bien que el tipo de un callback retorne FutureOr<T>:
Stream<S> asyncMap<T, S>(
Iterable<T> iterable,
FutureOr<S> Function(T) callback,
) async* {
for (final element in iterable) {
yield await callback(element);
}
}
Parámetros
#En Dart, los parámetros opcionales pueden ser posicionales o nombrados, pero no ambos.
EVITA parámetros booleanos posicionales
#Regla de linter: avoid_positional_boolean_parameters
A diferencia de otros tipos, los booleanos se usan generalmente en forma literal. Los valores como
los números suelen envolverse en constantes nombradas, pero normalmente pasamos
true y false directamente. Eso puede hacer que los lugares de llamada sean ilegibles si no está
claro qué representa el booleano:
new Task(true);
new Task(false);
new ListBox(false, true, true);
new Button(false);
En su lugar, prefiere usar argumentos nombrados, constructores nombrados o constantes nombradas para aclarar qué está haciendo la llamada.
Task.oneShot();
Task.repeating();
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);
Ten en cuenta que esto no se aplica a los setters, donde el nombre deja en claro qué representa el valor:
listBox.canScroll = true;
button.isEnabled = false;
EVITA parámetros posicionales opcionales si el usuario pudiera querer omitir parámetros anteriores
#Los parámetros posicionales opcionales deben tener una progresión lógica tal que los parámetros anteriores se pasen con más frecuencia que los posteriores. Los usuarios casi nunca deberían necesitar pasar explícitamente un "hueco" (hole) para omitir un argumento posicional anterior para pasar uno posterior. Es mejor que uses argumentos nombrados para eso.
String.fromCharCodes(Iterable<int> charCodes, [int start = 0, int? end]);
DateTime(
int year, [
int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0,
]);
Duration({
int days = 0,
int hours = 0,
int minutes = 0,
int seconds = 0,
int milliseconds = 0,
int microseconds = 0,
});
EVITA parámetros obligatorios que acepten un valor especial de "sin argumento"
#Si el usuario está omitiendo lógicamente un parámetro, prefiere permitirle omitirlo realmente
haciendo que el parámetro sea opcional en lugar de obligarlo a pasar null, una
cadena vacía u otro valor especial que signifique "no se pasó".
Omitir el parámetro es más conciso y ayuda a prevenir errores donde se pasa accidentalmente un valor
centinela como null cuando el usuario pensaba que estaba
proporcionando un valor real.
var rest = string.substring(start);
var rest = string.substring(start, null);
USA parámetros de inicio inclusivo y fin exclusivo para aceptar un rango
#Si estás definiendo un método o función que permite a un usuario seleccionar un rango de elementos o ítems de alguna secuencia indexada por enteros, recibe un índice de inicio, el cual hace referencia al primer ítem, y un índice final (probablemente opcional) que sea uno mayor que el índice del último ítem.
Esto es consistente con las librerías principales que hacen lo mismo.
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'
Es particularmente importante ser consistente aquí porque estos parámetros por lo general no tienen nombre. Si tu API recibe una longitud en lugar de un punto final, la diferencia no será visible en absoluto en el lugar de la llamada.
Igualdad
#Implementar un comportamiento de igualdad personalizado para una clase puede ser complicado. Los usuarios tienen una profunda intuición sobre cómo funciona la igualdad que tus objetos deben cumplir, y los tipos de colección como las tablas hash tienen contratos sutiles que esperan que los elementos sigan.
SOBREESCRIBE hashCode si sobreescribes ==
#
Regla de linter: hash_and_equals
La implementación predeterminada del código hash proporciona un hash de identidad: dos
objetos generalmente solo tienen el mismo código hash si son exactamente el mismo
objeto. Del mismo modo, el comportamiento predeterminado para == es la identidad.
Si estás sobreescribiendo ==, implica que puedes tener diferentes objetos que son
considerados "iguales" por tu clase. Dos objetos cualesquiera que sean iguales deben tener el
mismo código hash. De lo contrario, los mapas y otras colecciones basadas en hash no
reconocerán que los dos objetos son equivalentes.
HAZ que tu operador == obedezca las reglas matemáticas de la igualdad
#
Una relación de equivalencia debe ser:
Reflexiva:
a == asiempre debería retornartrue.-
Simétrica:
a == bdebería retornar lo mismo queb == a. -
Transitiva: Si
a == byb == cretornantrue, entoncesa == ctambién debería hacerlo.
Los usuarios y el código que utiliza == esperan que se sigan todas estas leyes. Si tu
clase no puede obedecer estas reglas, entonces == no es el nombre correcto para la operación
que estás intentando expresar.
EVITA definir una igualdad personalizada para clases mutables
#Regla de linter: avoid_equals_and_hash_code_on_mutable_classes
Cuando defines ==, también tienes que definir hashCode. Ambos deberían
tener en cuenta los campos del objeto. Si esos campos cambian, entonces eso
implica que el código hash del objeto puede cambiar.
La mayoría de las colecciones basadas en hash no anticipan eso; asumen que el código hash de un objeto será el mismo para siempre y pueden comportarse de manera impredecible si eso no es así.
NO hagas anulable el parámetro de ==
#
Regla de linter: avoid_null_checks_in_equality_operators
El lenguaje especifica que null es igual únicamente a sí mismo, y que el método ==
se llama solo si el lado derecho no es null.
class Person {
final String name;
// ···
bool operator ==(Object other) => other is Person && name == other.name;
}
class Person {
final String name;
// ···
bool operator ==(Object? other) =>
other != null && other is Person && name == other.name;
}
A menos que se indique lo contrario, la documentación en este sitio refleja Dart 3.12.2. Página actualizada por última vez el 2026-06-04. Ver fuente oreportar un problema.