Tipos de extensão
Aprenda a escrever uma interface apenas estática para um tipo existente.
Um extension type é uma abstração em tempo de compilação que "envolve" um tipo existente com uma interface diferente, apenas estática. Ele é um componente importante da interoperabilidade estática com JS, porque pode modificar facilmente a interface de um tipo existente (algo crucial para qualquer tipo de interoperabilidade) sem arcar com o custo de um wrapper de verdade.
Extension types impõem disciplina sobre o conjunto de operações (ou interface) disponíveis para objetos de um tipo subjacente, chamado de tipo de representação. Ao definir a interface de um extension type, você pode optar por reutilizar alguns membros do tipo de representação, omitir outros, substituir alguns e adicionar novas funcionalidades.
O exemplo a seguir envolve o tipo int para criar um extension type
que permite apenas operações que fazem sentido para números de ID:
extension type IdNumber(int id) {
// Wraps the 'int' type's '<' operator:
operator <(IdNumber other) => id < other.id;
// Doesn't declare the '+' operator, for example,
// because addition does not make sense for ID numbers.
}
void main() {
// Without the discipline of an extension type,
// 'int' exposes ID numbers to unsafe operations:
int myUnsafeId = 42424242;
myUnsafeId = myUnsafeId + 10; // This works, but shouldn't be allowed for IDs.
var safeId = IdNumber(42424242);
safeId + 10; // Compile-time error: No '+' operator.
myUnsafeId = safeId; // Compile-time error: Wrong type.
myUnsafeId = safeId as int; // OK: Run-time cast to representation type.
safeId < IdNumber(42424241); // OK: Uses wrapped '<' operator.
}
Sintaxe
#Declaração
#
Defina um novo extension type com a declaração extension type, um nome
e, em seguida, a declaração do tipo de representação entre parênteses:
extension type E(int i) {
// Define set of operations.
}
A declaração do tipo de representação (int i) especifica que
o tipo subjacente do extension type E é int,
e que a referência ao objeto de representação se chama i.
A declaração também introduz:
-
Um getter implícito para o objeto de representação,
com o tipo de representação como tipo de retorno:
int get i. - Um construtor implícito:
E(int i) : i = i.
O getter de representação dá acesso ao objeto de representação, tipado como o tipo subjacente. O getter fica no escopo do corpo do extension type, e você pode acessá-lo pelo nome, como qualquer outro getter:
- Dentro do corpo do extension type, usando
i(outhis.i). -
Fora dele, por meio de uma extração de propriedade, usando
e.i(em queetem o extension type como tipo estático).
As declarações de extension type também podem incluir parâmetros de tipo, assim como classes ou extensions:
extension type E<T>(List<T> elements) {
// ...
}
Construtores
#
Você pode, opcionalmente, declarar construtores no corpo de um extension type.
A própria declaração de representação é um construtor implícito,
então, por padrão, ela ocupa o lugar de um construtor sem nome do extension type.
Quaisquer construtores generativos adicionais que não redirecionam devem
inicializar a variável de instância do objeto de representação
usando this.i na lista de inicializadores ou nos parâmetros formais.
extension type E(int i) {
E.n(this.i);
E.m(int j, String foo) : i = j + foo.length;
}
void main() {
E(4); // Implicit unnamed constructor.
E.n(3); // Named constructor.
E.m(5, "Hello!"); // Named constructor with additional parameters.
}
Ou você pode nomear o construtor da declaração de representação, caso em que sobra espaço para um construtor sem nome no corpo:
extension type const E._(int it) {
E(): this._(42);
E.otherName(this.it);
}
void main2() {
E();
const E._(2);
E.otherName(3);
}
Você também pode ocultar o construtor por completo, em vez de apenas definir um novo,
usando a mesma sintaxe de construtor privado das classes, _. Por exemplo,
se você quiser que os clientes criem E apenas com uma String, mesmo que
o tipo subjacente seja int:
extension type E._(int i) {
E.fromString(String foo) : i = int.parse(foo);
}
Você também pode declarar construtores generativos de encaminhamento ou construtores factory (que também podem encaminhar para construtores de sub-extension types).
Membros
#
Declare membros no corpo de um extension type para definir a sua interface
da mesma forma que faria com os membros de uma classe.
Membros de extension type podem ser métodos, getters, setters ou operadores
(variáveis de instância e membros abstratos
que não sejam external não são permitidos):
extension type NumberE(int value) {
// Operator:
NumberE operator +(NumberE other) =>
NumberE(value + other.value);
// Getter:
NumberE get myNum => this;
// Method:
bool isValid() => !value.isNegative;
}
Membros de interface do tipo de representação não são membros de interface
do extension type por padrão.
Para tornar um único membro do tipo de representação disponível
no extension type, você precisa escrever uma declaração para ele
na definição do extension type, como o operator + em NumberE.
Você também pode definir novos membros não relacionados ao tipo de representação,
como o getter i e o método isValid.
Implements
#Você pode, opcionalmente, usar a cláusula implements para:
- Introduzir uma relação de subtipo no extension type, E
- Adicionar os membros do objeto de representação à interface do extension type.
A cláusula implements introduz uma relação de aplicabilidade,
parecida com a que existe entre um extension method
e o seu tipo on.
Membros aplicáveis ao supertipo também são aplicáveis ao
subtipo, a menos que o subtipo tenha uma declaração com o mesmo
nome de membro.
Um extension type só pode implementar:
-
Seu tipo de representação. Isso torna todos os membros do tipo de representação implicitamente disponíveis para o extension type.
dartextension type NumberI(int i) implements int{ // 'NumberI' can invoke all members of 'int', // plus anything else it declares here. } -
Um supertipo do seu tipo de representação. Isso torna os membros do supertipo disponíveis, embora não necessariamente todos os membros do tipo de representação.
dartextension type Sequence<T>(List<T> _) implements Iterable<T> { // Better operations than List. } extension type Id(int _id) implements Object { // Makes the extension type non-nullable. static Id? tryParse(String source) => int.tryParse(source) as Id?; } -
Outro extension type que seja válido sobre o mesmo tipo de representação. Isso permite reutilizar operações em vários extension types (algo semelhante à herança múltipla).
dartextension type const Opt<T>._(({T value})? _) { const factory Opt(T value) = Val<T>; const factory Opt.none() = Non<T>; } extension type const Val<T>._(({T value}) _) implements Opt<T> { const Val(T value) : this._((value: value)); T get value => _.value; } extension type const Non<T>._(Null _) implements Opt<Never> { const Non() : this._(null); }
Leia a seção Uso para saber mais sobre o efeito de implements
em diferentes cenários.
@redeclare
#
Declarar um membro de extension type que compartilha o nome com um membro de um supertipo não é uma relação de sobrescrita, como acontece entre classes, mas sim uma redeclaração. A declaração de membro de um extension type substitui completamente qualquer membro do supertipo com o mesmo nome. Não é possível fornecer uma implementação alternativa para a mesma função.
Você pode usar a annotation @redeclare
de package:meta para
informar ao compilador que você está conscientemente escolhendo
usar o mesmo nome de um membro do supertipo.
Então, o analisador avisará se isso não for verdade,
por exemplo, se um dos nomes estiver digitado errado.
import 'package:meta/meta.dart';
extension type MyString(String _) implements String {
// Replaces 'String.operator[]'.
@redeclare
int operator [](int index) => codeUnitAt(index);
}
Você também pode habilitar o lint annotate_redeclares
para receber um aviso quando declarar um método de extension type
que oculta um membro de superinterface e não estiver anotado com @redeclare.
Uso
#Para usar um extension type, crie uma instância da mesma forma que faria com uma classe: chamando um construtor:
extension type NumberE(int value) {
NumberE operator +(NumberE other) =>
NumberE(value + other.value);
NumberE get next => NumberE(value + 1);
bool isValid() => !value.isNegative;
}
void testE() {
var num = NumberE(1);
}
Depois, você pode invocar membros no objeto como faria com um objeto de classe.
Há dois casos de uso principais para extension types, igualmente válidos, mas substancialmente diferentes:
- Fornecer uma interface estendida para um tipo existente.
- Fornecer uma interface diferente para um tipo existente.
1. Fornecer uma interface estendida para um tipo existente
#Quando um extension type implementa seu tipo de representação, você pode considerá-lo "transparente", porque ele permite que o extension type "enxergue" o tipo subjacente.
Um extension type transparente pode invocar todos os membros do tipo de representação (que não tenham sido redeclarados), além de quaisquer membros auxiliares que ele defina. Isso cria uma nova interface estendida para um tipo existente. A nova interface fica disponível para expressões cujo tipo estático é o extension type.
Isso significa que você pode invocar membros do tipo de representação (ao contrário de um extension type não transparente), assim:
extension type NumberT(int value)
implements int {
// Doesn't explicitly declare any members of 'int'.
NumberT get i => this;
}
void main () {
// All OK: Transparency allows invoking `int` members on the extension type:
var v1 = NumberT(1); // v1 type: NumberT
int v2 = NumberT(2); // v2 type: int
var v3 = v1.i - v1; // v3 type: int
var v4 = v2 + v1; // v4 type: int
var v5 = 2 + v1; // v5 type: int
// Error: Extension type interface is not available to representation type
v2.i;
}
Você também pode ter um extension type "quase transparente", que adiciona novos membros e adapta outros redeclarando um determinado nome de membro do supertipo. Isso permitiria, por exemplo, usar tipos mais restritos em alguns parâmetros de um método, ou valores padrão diferentes.
Outra abordagem de extension type quase transparente é implementar um tipo que seja supertipo do tipo de representação. Por exemplo, se o tipo de representação é privado, mas o seu supertipo define a parte da interface que importa para os clientes.
2. Fornecer uma interface diferente para um tipo existente
#
Um extension type que não é transparente
(ou seja, que não implement seu tipo de representação)
é tratado estaticamente como um tipo completamente novo,
distinto do seu tipo de representação.
Você não pode atribuí-lo ao seu tipo de representação,
e ele não expõe os membros do tipo de representação.
Por exemplo, pegue o extension type NumberE que declaramos em Uso:
void testE() {
var num1 = NumberE(1);
int num2 = NumberE(2); // Error: Can't assign 'NumberE' to 'int'.
num1.isValid(); // OK: Extension member invocation.
num1.isNegative(); // Error: 'NumberE' does not define 'int' member 'isNegative'.
var sum1 = num1 + num1; // OK: 'NumberE' defines '+'.
var diff1 = num1 - num1; // Error: 'NumberE' does not define 'int' member '-'.
var diff2 = num1.value - 2; // OK: Can access representation object with reference.
var sum2 = num1 + 2; // Error: Can't assign 'int' to parameter type 'NumberE'.
List<NumberE> numbers = [
NumberE(1),
num1.next, // OK: 'next' getter returns type 'NumberE'.
1, // Error: Can't assign 'int' element to list type 'NumberE'.
];
}
Você pode usar um extension type dessa forma para substituir a interface
de um tipo existente. Isso permite modelar uma interface que
faça sentido para as restrições do seu novo tipo
(como o exemplo IdNumber da introdução), ao mesmo tempo que se beneficia do
desempenho e da conveniência de um tipo predefinido simples, como int.
Esse caso de uso é o mais próximo que você consegue chegar do encapsulamento completo de uma classe wrapper (mas, na prática, é apenas uma abstração parcialmente protegida).
Considerações sobre tipos
#Extension types são uma construção de wrapping em tempo de compilação. Em tempo de execução, não há absolutamente nenhum vestígio do extension type. Qualquer consulta de tipo ou operação semelhante em tempo de execução funciona sobre o tipo de representação.
Isso torna os extension types uma abstração insegura, porque você sempre pode descobrir o tipo de representação em tempo de execução e acessar o objeto subjacente.
Testes de tipo dinâmico (e is T), casts (e as T)
e outras consultas de tipo em tempo de execução (como switch (e) ...
ou if (e case ...))
avaliam todos para o objeto de representação subjacente,
e verificam o tipo contra o tipo de tempo de execução desse objeto.
Isso vale tanto quando o tipo estático de e é um extension type
quanto ao testar contra um extension type (case MyExtensionType(): ...
).
void main() {
var n = NumberE(1);
// The run-time type of 'n' is the representation type 'int'.
if (n is int) print(n); // Prints 1.
// Can use 'int' methods on 'n' at run time.
if (n case int x) print(x.toRadixString(10)); // Prints 1.
switch (n) {
case int(:var isEven): print("$n (${isEven ? "even" : "odd"})"); // Prints 1 (odd).
}
}
De forma semelhante, o tipo estático do valor correspondido é o do extension type neste exemplo:
void main() {
int i = 2;
if (i is NumberE) print("It is"); // Prints 'It is'.
if (i case NumberE v) print("value: ${v.value}"); // Prints 'value: 2'.
switch (i) {
case NumberE(:var value): print("value: $value"); // Prints 'value: 2'.
}
}
Quando i recebe o tipo estático NumberE com base em um cast ou em uma correspondência de padrão,
i
continua se referindo ao mesmo objeto, v se refere ao mesmo objeto que
i, e
o próprio objeto permanece inalterado. Só o tipo estático mudou
(e, portanto, os métodos que podemos chamar nesse objeto). Em particular, a
mudança de tipo não envolve a invocação de um construtor. Se você quiser
executar um construtor (por exemplo, para fazer algum tipo de validação), então é
necessário escrever uma invocação explícita do construtor (como NumberE(i)).
É importante estar atento a esse comportamento ao usar extension types. Tenha sempre em mente que um extension type existe e importa em tempo de compilação, mas é apagado durante a compilação.
Por exemplo, considere uma expressão e cujo tipo estático é o
extension type E. Suponha que o tipo de representação de E
seja R.
O tipo de tempo de execução do valor de e é, então, um subtipo de R.
Até o próprio tipo é apagado:
List<E> é exatamente a mesma coisa que List<R>
em tempo de execução.
Em outras palavras, uma classe wrapper de verdade pode encapsular um objeto envolvido, enquanto um extension type é apenas uma visão em tempo de compilação do objeto envolvido. Embora um wrapper real seja mais seguro, o compromisso é que os extension types dão a opção de evitar objetos wrapper, o que pode melhorar muito o desempenho em alguns cenários.
Salvo indicação em contrário, a documentação neste site reflete Dart 3.13.7. Página atualizada pela última vez em 2026-11-12. Ver código-fonte ou relatar um problema.
Encontrou essa página sem tradução ou que precisa de correção? Abra uma issue e nos ajude a manter esse site em PT-BR para ajudar pessoas como você.