Fundamentos dos métodos de extensão do Dart
Conheça as considerações de design por trás dos métodos de extensão, um novo recurso que chegará em uma versão futura do SDK do Dart
Fundamentos dos métodos de extensão do Dart
#
Em uma versão futura, a linguagem Dart está adicionando um novo recurso, os métodos de extensão, que permitem (fingir) adicionar novos membros a tipos existentes. Um método de extensão pode ser invocado como um método normal,
o.extensionMethod(42), mesmo que na prática seja apenas uma função estática.
Por que estamos adicionando métodos de extensão? Para que servem? Como você os usa? E por que eu os chamo de “métodos de extensão” quando você também pode adicionar outros membros? (Esta última é fácil: eu pessoalmente penso neles como
membros de extensão, mas “métodos de extensão” era o título de trabalho, e é como um recurso semelhante se chama em outras linguagens; então, na boa tradição do Dart, optamos por um nome familiar e nada surpreendente. Não vou precisar de getters, setters ou operadores de extensão aqui, mas você pode perfeitamente adicionar um operador
% a String se quiser, independentemente do nome do recurso.)
Como sou uma das pessoas que projetaram o recurso, vou responder oportunisticamente a todas essas perguntas antes que qualquer outra pessoa tenha a chance. (E, como publiquei este artigo antes de terminarmos o recurso, ainda consegui remover as coisas que deixaram de ser verdade!)
Mas, primeiro, um desvio!
O que eu teria feito antes dos métodos de extensão
#
Vamos supor, puramente por hipótese, que eu ache a função catchError
de Future
terrível e que ela deveria ser substituída por algo mais novo, mais brilhante e melhor. Digamos que seja porque ela recebe um
Function
como argumento em vez de um tipo de função adequado, o que ela faz por razões históricas perfeitamente razoáveis, e isso significa que você não terá nenhuma verificação estática de tipos. Isso é ruim, e o método deveria se sentir mal por isso.
Obviamente, não posso remover a função; isso quebraria, tipo, todo programa Dart sério que já existiu.
Então eu gostaria de pelo menos adicionar um novo método a Future<T> para que os usuários possam usar esse em vez do antigo, digamos algo 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...);
}
que você pode chamar assim:
Future<int> eventualInteger = ...;
eventualInteger.onError((FormatException e, s) => ...).then(...);
Infelizmente, não posso simplesmente adicionar isso à classe Future. Se fizer isso, também o adiciono à interface
Future, e qualquer outra classe que implemente essa interface ficará incompleta e não compilará mais. Em certo ponto, contamos 76 classes implementando
Future. Isso foi há um tempo, e paramos de contar. Ainda não podemos quebrar todo mundo, então essa opção também não está na mesa.
Bem, então vou usar uma função auxiliar estática:
Future<T> onFutureError<T, E>(Future<T> source,
FutureOr<T> handleError(E error, StackTrace stack)) =>
source.catchError(...something clever...);
que você pode chamar assim:
Future<int> eventualInteger = ...;
onFutureError(eventualInteger,
(FormatException e, s) => ...).then(...);
Quase tão infelizmente, isso simplesmente não se lê bem. Nós gostamos de encadeamento de métodos baseado em
. porque ele permite ler da esquerda para a direita: “Faça isto, depois faça aquilo, depois faça mais uma coisa”. Usar a função auxiliar estática nos obriga a ler assim: “Faça aquilo com o seguinte: Faça isto. Depois disso, faça mais uma coisa”… dizer o quê? Simplesmente não tem o mesmo
fluxo, a mesma schwung. Na prática, é quase impossível de ler.
Ok, então, sem me deixar abater na minha busca, em vez de melhorar a classe Future, vou introduzir uma nova interface aprimorada e dar aos usuários uma forma de envolver a interface antiga:
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...);
}
que você pode usar assim:
Future<int> eventualInteger = ...;
MyFuture(eventualInteger).onError(
(FormatException e, s) => ...).then(...);
Provavelmente eu até deixaria MyFuture implementar Future e encaminhar todos os membros de
Future para o future _wrappee, e então faria todos os métodos retornarem um wrapper
MyFuture de novo, para que eu possa continuar encadeando.
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 e fluido, se me permitem dizer!
Isso era praticamente o melhor que se conseguia, antes dos métodos de extensão… o que significava adicionar o wrapper manualmente e aceitar o custo de desempenho dos objetos wrapper extras e das funções de encaminhamento intermediárias.
O que eu vou fazer com os métodos de extensão
#Quando sairmos da era das trevas sem extensões, posso usar métodos de extensão para conseguir exatamente o que eu realmente, realmente quero. Vou escrever:
extension MyFuture<T> on Future<T> {
Future<T> onError<E>(
FutureOr<T> handleError(E error, StackTrace stack)) =>
this.catchError(...something clever...);
}
e aí você pode chamá-lo assim:
Future<int> eventualInteger = ...;
eventualInteger.onError((FormatException e, s) => ...).then(...);
e é isso. Missão cumprida em cinco linhas!
“Mas como isso funciona?”, você pode perguntar. Funciona muito bem, obrigado.
Na verdade, ele se comporta quase exatamente da mesma forma que a classe wrapper, mesmo sendo apenas uma função auxiliar estática. Você pode até escrever
MyFuture(eventualInteger).onError(...) explicitamente, como se a extensão fosse uma classe wrapper. Não é, mas parece e age
quase como se fosse. E você pode omitir o wrapping explícito e fazer com que ele seja aplicado
implicitamente quando os tipos forem compatíveis.
É (ou não é) uma classe wrapper
#
O design da declaração extension foi feito deliberadamente para parecer com uma declaração de
class ou mixin, e ela age como se fosse uma classe wrapper com um _wrappee
oculto. Você pode até ter membros estáticos na declaração, e eles funcionam exatamente como membros estáticos em uma declaração de
class ou mixin.
Há uma melhoria em relação à classe wrapper: você pode escrever this dentro de membros de instância para se referir ao
_wrappee em vez do objeto wrapper.
Mudar o significado de this não foi apenas uma melhoria. Estes são métodos de extensão estáticos
e, como eu disse antes, eles são na verdade apenas uma forma mais conveniente de chamar funções estáticas. Isso significa que não há um
objeto wrapper. Ele nunca existiu; apenas fingimos que existia, mas isso significa que não podemos deixar
this se referir a um objeto que não existe.
Também não podemos permitir que você use MyFuture(eventualInteger) como um valor; então, se você tentar fazer
var myFuture = MyFuture(eventualInteger), não vamos permitir. A única forma de usar MyFuture(eventualInteger)
é como alvo de uma invocação de membro de extensão.
MyFuture(eventualInteger).onError(...); // GOOD: Use to call method.
var x = MyFuture(eventualInteger); // BAD: Use as stand-alone value.
É o mesmo que acontece quando você usa super para chamar métodos, mas não pelo seu valor. Ou como um prefixo de biblioteca. Tudo o que você pode fazer é acessar um membro; você não pode tratá-lo como um valor porque ele não tem valor, e não há valor para ele ter.
Como não há objeto, você não pode declarar campos de instância em uma declaração extension. Você
pode declarar getters e setters, e talvez até sustentá-los com um Expando. Uma extensão também não pode declarar nenhum construtor, já que nada está sendo construído; ela apenas finge ter um construtor que recebe o objeto wrappee.
Ela (não) estende o tipo
#
Se você tivesse que escrever o wrapping MyFuture(...) toda vez que usasse um membro de extensão, isso não seria uma grande melhoria. Provavelmente poderíamos simplesmente escrever as classes wrapper diretamente e gastar algumas horas de engenheiros de compilador garantindo que eliminaríamos o objeto intermediário na otimização.
Eu disse acima que você pode escrever eventualInteger.onError(...). Isso funciona porque nós
envolvemos implicitamente expressões com base no tipo estático delas e no nome do membro que elas chamam. Envolvemos automaticamente
expr.method() como Ext(expr).method() quando todas as condições a seguir são verdadeiras:
-
O tipo estático de
exprnão tem um membro com nome basemethod(a interface sempre vence). -
A extensão
Exté importada ou declarada no escopo da biblioteca atual (a extensão é acessível). -
A extensão declara um membro com nome base
method, e o tipo estático deexpré um subtipo do tipoonda declaraçãoExt(a extensão é aplicável).
Se houver mais de uma extensão acessível e aplicável para uma invocação de membro, há regras sobre qual delas vence o conflito. Em alguns casos, não há como escolher um vencedor, e então isso é apenas um erro de compilação. Essas regras dependem apenas do tipo
on da declaração da extensão, não da declaração do membro. (O Dart não tem “sobrecarga”, ou seja, múltiplos métodos com o mesmo nome e assinaturas diferentes, entre os quais você escolhe com base na estrutura ou nos tipos dos argumentos; e os métodos de extensão não oferecem uma porta dos fundos para conseguir sobrecarga.)
Tudo é estático
#Eu disse “métodos de extensão estáticos” acima, e fiz isso por um motivo!
O Dart é estaticamente tipado. O compilador conhece o tipo de cada expressão em tempo de compilação, então, se você escrever
target.member(42), e member for um membro de extensão, o compilador precisa descobrir
qual extensão usar para envolver target implicitamente, a fim de encontrar o tipo de toda a invocação do membro.
Se o wrapping implícito de extensão precisa acontecer entre descobrir o tipo da expressão alvo e descobrir o tipo da invocação do membro, parece óbvio que a “inferência de extensão” precisa acontecer durante a fase de “inferência de tipos”, cujo nome é cada vez mais impreciso. Essa é a fase mais conhecida por preencher genéricos ausentes.
Eu escrevi eventualInteger.onError((FormatException e, s) {...}), mesmo que tanto a extensão
MyFuture quanto o método onError sejam genéricos. Durante a inferência de tipos, o compilador do Dart seleciona a extensão e infere os argumentos de tipo ausentes. Aqui, ele primeiro decide usar a extensão
MyFuture, depois insere o wrapper implícito e, por fim, realiza a inferência de tipos para a aplicação da extensão
MyFuture(eventualInteger).onError((FormatException e, s) {...}) exatamente da mesma forma
como faria para uma classe wrapper correspondente:
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...);
}
Nesse caso, a inferência de tipos deduziria a seguinte aplicação da extensão e os tipos completos da invocação:
MyFuture<int>(eventualInteger).onError<FormatException>(
(FormatException e, StackTrace s) {...});
Isso significa que os argumentos de tipo da extensão são baseados no tipo estático da expressão envolvida. Se você tiver um
Future<num> fut = Future<int>.value(42);, então fut.onError(...)
vai vincular o parâmetro de tipo T de MyFuture a num em tempo de compilação, e não a
int. É tudo estático, assim como para quaisquer outros argumentos de tipo inferidos.
Isso também significa que você nunca poderá chamar um membro de extensão em um alvo tipado como dynamic.
Resolução de conflitos
#
Como foi dito acima, há regras sobre qual extensão vence quando há mais de uma no escopo que se aplica. Basicamente, o vencedor é a extensão com um tipo
on mais próximo do tipo real da expressão na qual você está chamando um membro, com algumas ressalvas e critérios de desempate. Normalmente, ela simplesmente “funciona” para extensões escritas juntas. Em vez de entrar nesses detalhes, vou dizer o que fazer quando ela
não funciona simplesmente.
Você pode ter um problema quando dois autores diferentes escreveram extensões conflitantes para o mesmo tipo e nome de membro. Digamos que as extensões
Ext1 e Ext2 definam ambas um método bubbleSort que se aplica ao seu objeto
List, e ou não há um vencedor claro para o conflito, ou o que vence não é o que você realmente quer chamar (digamos que
Ext2 vença e você queira chamar Ext1.bubbleSort). Aí você precisa fazer alguma coisa.
A solução mais fácil é usar uma aplicação explícita de extensão: Ext1(list).bubbleSort(). Isso evita a resolução automática e simplesmente escolhe aquela que você quer. Se você tiver apenas alguns conflitos, isso é ao mesmo tempo fácil e legível.
Porém, se você tiver trezentos conflitos no mesmo arquivo, talvez queira evitar a digitação extra. É difícil mudar se uma extensão é aplicável a uma invocação, mas você pode mudar se ela é acessível.
Você faz isso ocultando a extensão conflitante (ou extensões, se tiver muito azar) onde a importa:
import "ext2lib.dart" hide Ext2;. Isso impede que a extensão Ext2 seja importada para o escopo da biblioteca atual, o que a torna inacessível. Obviamente, não importar
ext2lib.dart também faria isso, mas, a menos que a extensão seja a única coisa que você usa dessa biblioteca, isso não é prático.
(Editado em 11 de dezembro) Aqui eu costumava dizer que você podia importar uma das extensões conflitantes com um prefixo e que, assim, ela não ficaria disponível para uso implícito. Acontece que algumas pessoas declaram métodos de extensão na mesma biblioteca da classe que estendem, e é muito irritante se essa biblioteca não funciona quando importada com um prefixo. Então corrigimos isso. Extensões importadas com um prefixo também funcionam implicitamente. Se você realmente precisar usar duas extensões conflitantes na mesma biblioteca, vai precisar usar aplicação explícita de extensão em todos os lugares onde houver conflito. Podemos considerar adicionar uma forma diferente de desabilitar extensões implícitas no futuro, pelo menos se extensões conflitantes se mostrarem um problema recorrente.
Resumo
#O Dart terá métodos de extensão em uma próxima versão — uma forma bonita de chamar funções estáticas.
Você pode definir membros de extensão para métodos de instância, operadores, setters e getters, mas não campos.
Você pode invocar métodos de extensão de forma explícita ou, quando não há conflito com um membro da interface ou com outra extensão, de forma implícita:
Ext1(list).bubbleSort() // Explicit, like it's a wrapper class.
list.bubbleSort() // Implicitly, like it extends the type.
Invocações implícitas funcionam da mesma forma que as explícitas, mas primeiro inferem qual extensão está sendo aplicada. Se a inferência de extensão falhar por causa de extensões conflitantes, você pode fazer qualquer uma das seguintes coisas:
Aplicar a extensão explicitamente.
Não importar a extensão conflitante de forma alguma (remover o import ou ocultar a extensão).
(Editado em 11 de dezembro): E é isso (por enquanto).
Extensões são estáticas. Tudo sobre elas é decidido com base em tipos estáticos.
Aproveite com responsabilidade!
Mais do Dart
Resultados do Google Summer of Code 2026
Hoje, destacamos os projetos e os resultados dos colaboradores orientados pelo Dart e pelo Flutter no Google Summer of Code 2026.
Dart no Google Summer of Code 2026
Estamos animados para anunciar que o projeto Dart será mentor de contribuidores pela sétima vez no Google Summer of Code 2026!