Fundamentos de los métodos de extensión en Dart
Lee sobre las consideraciones de diseño detrás de los métodos de extensión, una nueva característica que llegará en un futuro lanzamiento del SDK de Dart
Fundamentos de los métodos de extensión de Dart
#
En una futura versión, el lenguaje Dart está añadiendo una nueva característica, los métodos de extensión, que te permiten (fingir) añadir nuevos miembros a tipos existentes. Un método de extensión se puede invocar igual que un método normal,
o.extensionMethod(42), aunque en realidad es solo una función estática.
¿Por qué estamos añadiendo métodos de extensión? ¿Para qué sirven? ¿Cómo se usan? ¿Y por qué los llamo “métodos de
extensión” cuando también puedes añadir otros miembros? (El último es fácil: personalmente pienso en ellos como
miembros de extensión, pero “métodos de extensión” fue el título provisional, y es como se llama la característica similar en otros lenguajes, así que en la buena tradición de Dart, fuimos por el nombre familiar y poco sorprendente. No voy a necesitar getters, setters u operadores de extensión aquí, pero totalmente puedes añadir un operador
% a String si quieres, sin importar cómo llamemos a la característica.)
Como soy una de las personas que diseñó la característica, responderé oportunamente todas estas preguntas antes de que nadie tenga la oportunidad. (Y, como publiqué este artículo antes de que termináramos la característica, ¡hasta he podido editar las cosas que ya no son ciertas!)
Pero primero, un desvío.
Lo que habría hecho antes de los métodos de extensión
#Asume, puramente de forma hipotética, que pienso que la función catchError
en Future
es terrible y debería ser reemplazada por algo más nuevo, más brillante y mejor. Digamos, porque toma un Function
como argumento en lugar de un tipo de función adecuado, lo cual hace por razones históricas
perfectamente razonables, y eso significa que no obtendrás ninguna verificación de tipos estática. Eso es malo, y el método debería sentirse mal.
Obviamente no puedo eliminar la función, eso rompería, como, todos los programas serios de Dart jamás.
Entonces querría al menos añadir un nuevo método a Future<T> para que los usuarios puedan usar ese en su lugar, digamos uno como:
abstract class Future<T> {
...
/// Catches any [error] of type [E].
Future<T> onError<E>(FutureOr<T> handleError(E error, StackTrace stack)) =>
this.catchError(...something clever...);
}
la cual puedes llamar como:
Future<int> eventualInteger = ...;
eventualInteger.onError((FormatException e, s) => ...).then(...);
Lamentablemente, no puedo simplemente añadir eso a la clase Future. Si lo hago, también lo añado a la interfaz de Future
y cualquier otra clase que implemente esa interfaz estará incompleta y ya no compilará. En algún momento contamos 76 clases que implementaban
Future. Eso fue hace tiempo, y dejamos de contar. Aún no podemos romper a todos, así que esa opción tampoco está sobre la mesa.
Bueno, entonces usaré una función helper estática:
Future<T> onFutureError<T, E>(Future<T> source,
FutureOr<T> handleError(E error, StackTrace stack)) =>
source.catchError(...something clever...);
la cual puedes llamar como:
Future<int> eventualInteger = ...;
onFutureError(eventualInteger,
(FormatException e, s) => ...).then(...);
Casi igual de lamentable, simplemente no se lee bien. Nos gusta usar el encadenamiento de métodos basado en . porque nos permite leer de izquierda a derecha: “Haz esto, luego haz aquello, luego haz algo más”. Usar la función helper estática nos fuerza a leerlo como: “Haz aquello a lo siguiente: Haz esto. Después de eso, haz algo más” … ¿qué? Simplemente no tiene el mismo
flujo, el mismo schwung. Es prácticamente ilegible en la práctica.
Bien entonces, no me dejo disuadir en mi búsqueda, así que en lugar de mejorar la clase Future, introduciré una interfaz nueva y mejorada y daré a los usuarios una forma de envolver la interfaz antigua:
class MyFuture<T> {
Future<T> _wrappee;
MyFuture(Future<T> future) : _wrappee = future;
Future<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
_wrappee.catchError(...something clever...);
}
la cual puedes usar como:
Future<int> eventualInteger = ...;
MyFuture(eventualInteger).onError(
(FormatException e, s) => ...).then(...);
Probablemente incluso dejaría que MyFuture implemente Future y reenvíe todos los miembros de
Future al Future _wrappee, y también dejaría que todos los métodos devuelvan un wrapper de
MyFuture de nuevo, para poder seguir encadenando.
class MyFuture<T> {
Future<T> _wrappee;
MyFuture(Future<T> future) : _wrappee = future;
MyFuture<R> then<R>(...) => MyFuture(_wrappee.then<R>(...));
/// Forward other `Future` methods too.
MyFuture<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
MyFuture(_wrappee.catchError(...something clever...));
}
¡Bonito y fluido, si lo digo yo mismo!
Esto era básicamente lo mejor que podíamos obtener, antes de los métodos de extensión … lo que significaba añadir manualmente el wrapper y asumir el costo de rendimiento de los objetos wrapper adicionales y las funciones de reenvío intermedias.
Lo que haré con los métodos de extensión
#Una vez que salgamos de la oscura era sin extensiones, puedo usar métodos de extensión para obtener lo que realmente, realmente quiero. Escribiré:
extension MyFuture<T> on Future<T> {
Future<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
this.catchError(...something clever...);
}
y luego puedes llamarlo como:
Future<int> eventualInteger = ...;
eventualInteger.onError((FormatException e, s) => ...).then(...);
y eso es todo. ¡Misión cumplida en cinco líneas!
“Pero ¿cómo funciona?”, podrías preguntar. Funciona muy bien, gracias.
En realidad, se comporta casi exactamente igual que la clase wrapper, aunque en realidad es solo una función helper estática. Incluso puedes escribir
MyFuture(eventualInteger).onError(...) explícitamente, como si la extensión fuera una clase wrapper. No lo es, pero se ve y actúa
casi como si lo fuera. Y puedes omitir el envoltorio explícito y que se aplique
implícitamente cuando los tipos son correctos.
Es (no) una clase wrapper
#El diseño de la declaración de extension está hecho deliberadamente para que parezca una declaración de class
o mixin, y actúa igual que si fuera una clase wrapper con un _wrappee oculto. Incluso puedes tener miembros estáticos en la declaración, y funcionan igual que los miembros estáticos en una declaración de
class o mixin.
Hay una mejora respecto a la clase wrapper: puedes escribir this dentro de los miembros de instancia para referirte al
_wrappee en lugar del objeto wrapper.
Cambiar el significado de this no fue solo una mejora. Estos son métodos de extensión estáticos, y como dije antes, son realmente solo una forma más conveniente de llamar a funciones estáticas. Eso significa que no hay ningún objeto
wrapper. Nunca existió, solo fingimos que existía, pero eso significa que no podemos dejar que this
se refiera al objeto inexistente.
Tampoco podemos permitirte usar MyFuture(eventualInteger) como un valor, así que si intentas hacer
var myFuture = MyFuture(eventualInteger), no te lo permitiremos. La única forma de usar MyFuture(eventualInteger)
es como el objetivo de una invocación de un miembro de extensión.
MyFuture(eventualInteger).onError(...); // GOOD: Use to call method.
var x = MyFuture(eventualInteger); // BAD: Use as stand-alone value.
Es igual a cómo puedes usar super para llamar a métodos, pero no por su valor. O igual que un prefijo de librería. Todo lo que puedes hacer es acceder a un miembro; no puedes tratarlo como un valor porque no tiene valor, y no hay valor para que tenga.
Como no hay objeto, no puedes declarar campos de instancia en una declaración de extension. Sí
puedes declarar getters y setters, y quizás incluso respaldarlos con un Expando. Una extensión tampoco puede declarar constructores ya que nada se está construyendo; solo finge tener un constructor que toma el objeto wrappee.
No extiende el tipo
#Si tuvieras que escribir el envoltorio MyFuture(...) cada vez que usaras un miembro de extensión, entonces no sería mucha mejora. Probablemente podríamos simplemente escribir las clases wrapper directamente y dedicar algunas horas de ingeniería de compiladores a asegurar que optimizamos el objeto intermedio.
Dije arriba que puedes escribir eventualInteger.onError(...). Esto funciona porque envolvemos implícitamente
expresiones basándonos en su tipo estático y el nombre del miembro que llaman. Automáticamente envolvemos expr.method()
como Ext(expr).method() cuando se cumplen todas las siguientes condiciones:
-
El tipo estático de
exprno tiene un miembro con (nombre base)method(la interfaz siempre gana). -
La extensión
Extestá importada o declarada en el ámbito de la librería actual (la extensión está accesible). -
La extensión declara un miembro con nombre base
method, y el tipo estático deexpres un subtipo del tipoonde la declaraciónExt(la extensión es aplicable).
Si hay más de una extensión accesible y aplicable para una invocación de miembro, hay reglas sobre cuál ganará el conflicto. En algunos casos, no hay forma de elegir un ganador, y entonces es simplemente un error de compilación. Estas reglas dependen solo del tipo on de la declaración de extensión, no de la declaración del miembro. (Dart no tiene “sobrecarga” — múltiples métodos con el mismo nombre y diferentes firmas, entre las que eliges basándote en la estructura o tipos de los argumentos — y los métodos de extensión no proporcionan una puerta trasera para obtener sobrecarga.)
Es todo estático
#Dije “métodos de extensión estáticos” arriba, ¡y lo dije por una razón!
Dart tiene tipado estático. El compilador conoce el tipo de cada expresión en tiempo de compilación, así que si escribes
target.member(42), y member es un miembro de extensión, entonces el compilador necesita averiguar
qué extensión usar para envolver implícitamente a target, para encontrar el tipo de toda la invocación del miembro.
Si el envoltorio implícito de extensión tiene que ocurrir entre encontrar el tipo de la expresión objetivo y encontrar el tipo de la invocación del miembro, parece obvio que la “inferencia de extensión” tiene que ocurrir durante la cada vez más inexactamente llamada fase de “inferencia de tipos”. Esa es la fase que se conoce principalmente por rellenar los genéricos faltantes.
Sí escribí eventualInteger.onError((FormatException e, s) {...}), aunque tanto la extensión
MyFuture como el método onError son genéricos. Al hacer la inferencia de tipos, el compilador de Dart tanto selecciona la extensión como infiere los argumentos de tipo faltantes. Aquí primero decide usar la extensión
MyFuture, luego inserta el wrapper implícito, y finalmente realiza la inferencia de tipos para la aplicación de la extensión
MyFuture(eventualInteger).onError((FormatException e, s) {...}) de exactamente la misma manera
que lo haría para una clase wrapper correspondiente:
class MyFuture<T> {
Future<T> _wrappee;
MyFuture(Future<T> future) : _wrappee = future;
MyFuture<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
_wrappee.catchError(...something clever...);
}
En este caso, la inferencia de tipos inferiría la siguiente aplicación de extensión y los tipos completos para la invocación:
MyFuture<int>(eventualInteger).onError<FormatException>(
(FormatException e, StackTrace s) {...});
Esto significa que los argumentos de tipo de la extensión se basan en el tipo estático de la expresión envuelta. Si tienes un
Future<num> fut = Future<int>.value(42); entonces fut.onError(...)
vinculará el parámetro de tipo T de MyFuture a num en tiempo de compilación, no a
int. Es todo estático, igual que para cualquier otro argumento de tipo inferido.
Eso también significa que nunca podrás llamar a un miembro de extensión sobre un objetivo tipado como dynamic.
Resolución de conflictos
#Como se indicó arriba, hay reglas sobre qué extensión gana cuando hay más de una en el ámbito que aplica. Básicamente, el ganador es la extensión con un tipo
on más cercano al tipo real de la expresión sobre la que estás llamando a un miembro, con algunas advertencias y desempates. Normalmente “simplemente funciona” para extensiones que se escriben juntas. En lugar de entrar en esos detalles, te diré qué hacer cuando
no funciona.
Puedes tener un problema cuando dos autores diferentes han escrito extensiones conflictivas para el mismo tipo y nombre de miembro. Digamos que las extensiones
Ext1 y Ext2 definen ambas un método bubbleSort que aplica a tu objeto
List, y o no hay un ganador claro del conflicto, o el que gana no es el que realmente quieres llamar (digamos que
Ext2 gana y quieres llamar a Ext1.bubbleSort). Entonces tienes que hacer algo.
La solución más fácil es usar una aplicación de extensión explícita: Ext1(list).bubbleSort(). Esto evita la resolución automática y simplemente elige la que quieres. Si solo tienes unos pocos conflictos, entonces eso es fácil y legible.
Sin embargo, si tienes trescientos conflictos en el mismo archivo, quizás quieras evitar la escritura extra. Es difícil cambiar si una extensión es aplicable a una invocación, pero puedes cambiar si es accesible.
Lo haces ocultando la extensión conflictiva (o extensiones, si tienes mala suerte) donde la importas:
import "ext2lib.dart" hide Ext2;. Hacerlo evitará que la extensión Ext2 se importe al ámbito de la librería actual, lo que la hace no accesible. Obviamente, también lo hará no importar
ext2lib.dart en absoluto, pero a menos que la extensión sea lo único que usas de esa librería, eso no es práctico.
(Editado 11 de dic.) Aquí solía decir que podías importar una de las extensiones conflictivas con un prefijo y que entonces no estaría disponible para uso implícito. Resulta que algunas personas declaran métodos de extensión en la misma librería que la clase que extienden, y es realmente molesto si esa librería no funciona cuando se importa con un prefijo. Así que lo arreglamos. Las extensiones importadas con un prefijo sí funcionan implícitamente también. Si realmente necesitas usar dos extensiones conflictivas en la misma librería, tendrás que usar la aplicación de extensión explícita en todos los lugares donde haya un conflicto. Podemos considerar añadir una forma diferente de deshabilitar las extensiones implícitas en el futuro, al menos si las extensiones conflictivas resultan ser un problema recurrente.
Resumen
#Dart tendrá métodos de extensión en una próxima versión — una forma bonita de llamar a funciones estáticas.
Puedes definir miembros de extensión para métodos de instancia, operadores, setters y getters, pero no campos.
Puedes invocar métodos de extensión tanto explícitamente como — cuando no hay conflicto con un miembro de la interfaz u otra extensión — implícitamente:
Ext1(list).bubbleSort() // Explicit, like it's a wrapper class.
list.bubbleSort() // Implicitly, like it extends the type.
Las invocaciones implícitas funcionan igual que las invocaciones explícitas, pero primero infieren qué extensión se está aplicando. Si la inferencia de extensión falla debido a extensiones conflictivas, entonces puedes hacer cualquiera de las siguientes:
Aplica la extensión explícitamente.
No importes la extensión conflictiva en absoluto (elimina el import u oculta la extensión).
(Editado 11 de dic): Y eso es todo (por ahora).
Las extensiones son estáticas. Todo sobre ellas se decide basándose en tipos estáticos.
¡Disfruta con responsabilidad!
Más de Dart
Dart en Google Summer of Code 2026
¡Estamos emocionados de anunciar que el proyecto Dart será mentor de contribuidores por séptima vez en Google Summer of Code 2026!
Resultados de Google Summer of Code 2025
El programa Google Summer of Code (GSoC) se enfoca en involucrar a nuevos desarrolladores de todo el mundo con el desarrollo de software de código abierto. Google…