I. O que é um Record?
O Record é um tipo agregado, imutável e anônimo, usado para agrupar dados de tipos diferentes em um único objeto. Confira as diferenças centrais em relação ao List / Set / Map:
- Tamanho fixo: após criado, não é possível adicionar nem remover campos; armazenamento heterogêneo:
- Dentro de um mesmo Record podem ser armazenados tipos completamente distintos como int, String, bool
- Restrição de tipagem forte: o compilador reconhece o tipo de cada campo, sem perda de segurança de tipos
- Apenas leitura, imutável: os valores dos campos não podem ser reatribuídos após a criação
O Record é uma variável válida por completo, suporta todas as operações comuns: armazenar em variáveis, passar como parâmetro / múltiplos retornos de função, aninhar, inserir em List/Map/Set.
II. Sintaxe básica do Record
1. Literal de Record
Envolvido por parênteses e separado por vírgulas, dividido em duas categorias: campos posicionais e campos nomeados. Os campos posicionais vêm primeiro, os campos nomeados seguem com a sintaxe chave:.
// Mistura de campos posicionais + campos nomeados
var info = ('Jim', age: 20, flag: true, 'Student');Code language: JavaScript (javascript)
2. Formas de utilizar o Record
1 Campos posicionais
// Declaração: dois campos posicionais, respectivamente String e int
(String, int) user;
// Atribuição compatível com a estrutura
user = ('李四', 22);
Code language: JavaScript (javascript)
2 Campos nomeados
A declaração de tipo usa chaves {}
// Declarar dois campos nomeados a(int), b(bool)
({int a, bool b}) data;
data = (a: 100, b: false);Code language: JavaScript (javascript)
3 Uso misto
Declarar tipo com combinação de campos posicionais e nomeados
// 1 campo posicional String, 2 campos nomeados score(int) e pass(bool)
(String, {int score, bool pass}) exam;
exam = ('Prova final', score: 90, pass: true);
Code language: JavaScript (javascript)
III. Erros comuns
1: O nome dos campos nomeados faz parte da estrutura do tipo; nomes diferentes geram tipos totalmente distintos
// Tipo: ({int a, int b})
({int a, int b}) pointAB = (a: 1, b: 2);
// Tipo: ({int x, int y})
({int x, int y}) pointXY = (x: 3, y: 4);
// Erro de compilação! Tipos incompatíveis, atribuição mútua proibida
// pointAB = pointXY;
Code language: JavaScript (javascript)
Para os campos nomeados, mesmo com dois valores int, os nomes ab e xy são diferentes, logo não é possível atribuir um ao outro. Veja abaixo para campos posicionais.
2: Apelidos em campos posicionais na anotação de tipo são apenas comentários, não interferem na correspondência de tipos
Os nomes dados aos campos posicionais dentro dos parênteses servem apenas como documentação para o desenvolvedor e são ignorados pelo compilador. Se a estrutura for igual, a atribuição é permitida:
(int a, int b) p1 = (10, 20);
(int x, int y) p2 = (30, 40);
// Válido, internamente dois campos posicionais int, tipos idênticos
p1 = p2;Code language: JavaScript (javascript)
Análogo aos parâmetros de função: renomear parâmetros não altera a assinatura da função, a lógica é a mesma.
IV. Acessar campos internos do Record
O Record é imutável, possui apenas getters e não tem setters; não é possível alterar os valores dos campos.
- Campos nomeados: acesso direto com
.nomeDoCampo - Campos posicionais: acesso via
$número; o número ignora todos os campos nomeados e conta apenas a ordem dos campos posicionais
var record = ('first', a: 2, b: true, 'last');
print(record.$1); // Primeiro campo posicional: first
print(record.a); // Campo nomeado a: 2
print(record.b); // Campo nomeado b: true
print(record.$2); // Segundo campo posicional: last
Code language: PHP (php)
V. Regras de identificação de tipo estrutural do Record
O Record não precisa de uma definição de classe separada; o mesmo tipo é identificado pela sua forma estrutural:
Forma = quantidade total de campos + tipo de cada campo + nomes dos campos nomeados
Se a forma for exatamente igual, mesmo vindos de arquivos ou bibliotecas diferentes, são considerados o mesmo tipo.
O compilador reconhece o tipo estático de cada campo permanentemente, sem perda de informações de tipo:
// Posição 1 tipo num, posição 2 tipo Object
(num, Object) pair = (42, 'Um texto');
var firstVal = pair.$1; // Tipo estático num, em tempo de execução int
var secondVal = pair.$2;// Tipo estático Object, em tempo de execução StringCode language: JavaScript (javascript)
VI. Comparação de igualdade com == no Record
Duas condições devem ser atendidas para que dois Records sejam iguais:
- Os dois Records têm a mesma forma estrutural exata
- Todos os valores dos campos posicionais / nomeados correspondentes são iguais
A ordem de escrita dos campos nomeados não interfere na comparação de igualdade; porém, nomes de campos diferentes implicam tipos distintos, retornando sempre falso.
// Campos posicionais, mesma forma e valores idênticos → true
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // true
// Nomes de campos nomeados diferentes, tipos distintos → false
({int x, int y, int z}) p = (x:1, y:2, z:3);
({int r, int g, int b}) c = (r:1, g:2, b:3);
print(p == c); // falseCode language: PHP (php)
O Record sobrescreve automaticamente o operador == e o método hashCode, não é necessário implementá-los manualmente.
VII. Múltiplos retornos em funções
Funções Dart só podem retornar um único valor. Use o Record para agrupar dados de vários tipos, combinado com a desestruturação por padrões para separá-los rapidamente em variáveis independentes; substitui perfeitamente List/Map (sem perder segurança de tipos).
1 Desestruturação de campos posicionais
// Retorna: Nome(String), Idade(int)
(String name, int age) getUserInfo(Map<String, dynamic> json) {
return (json['name'] as String, json['age'] as int);
}
void main() {
final userJson = {'name': 'Dash', 'age': 10, 'color': 'blue'};
// Desestruturação direta, duas variáveis em uma linha
var (userName, userAge) = getUserInfo(userJson);
print(userName); // Dash
print(userAge); // 10
}
Code language: JavaScript (javascript)
2 Desestruturação de campos nomeados
Sintaxe :nomeDoCampo
({String name, int age}) getUserInfo(Map<String, dynamic> json) {
return (name: json['name'] as String, age: json['age'] as int);
}
void main() {
final userJson = {'name': 'Dash', 'age': 10};
// Sintaxe de desestruturação nomeada
final (:name, :age) = getUserInfo(userJson);
print(name);
}
Code language: JavaScript (javascript)
Desvantagens das alternativas
Em outras linguagens como C++, C#, Java sem Record, é preciso fazer os passos abaixo:
- Criar uma nova classe: código redundante, precisa escrever construtor e campos;
- Retornar via List: perda de segurança de tipos, tudo vira dynamic;
- Retornar via Map: risco de erro de digitação nas chaves, sem validação estática de tipos.
VIII. Dados estruturados leves
Quando só precisa armazenar dados sem métodos, use diretamente o Record sem criar uma classe nova. Elimina código redundante de declaração de classe e é ideal para listas volumosas de dados.
Exemplo
Lista de configuração de botões da página (exemplo Flutter, compreensível após estudar Flutter)
import 'package:flutter/material.dart';
void main() {
final buttonList = [
(
label: "Enviar arquivo",
icon: const Icon(Icons.upload_file),
onPressed: () => print("Clique no botão de envio"),
),
(
label: "Ver detalhes",
icon: const Icon(Icons.info),
onPressed: () => print("Clique no botão detalhes"),
)
];
}
Code language: JavaScript (javascript)
IX. typedef: apelido de tipo para Record
Simplificar tipos longos
A declaração de tipo de Record costuma ser extensa. Use typedef para criar um apelido de tipo, facilita reutilização e alteração uniforme da estrutura de campos futuramente.
Uso básico
// Definir apelido para Record de botão, onPressed aceita null
typedef ButtonConfig = ({
String label,
Icon icon,
void Function()? onPressed
});
// Declarar lista diretamente com o apelido
List<ButtonConfig> buttons = [
(label: "Enviar", icon: const Icon(Icons.check), onPressed: () {}),
(label: "Cancelar", icon: const Icon(Icons.close), onPressed: null),
];
Code language: PHP (php)
Expansão futura
Se depois for necessário adicionar métodos ou lógica encapsulada aos dados, não precisa alterar o código de laço de negócio: basta trocar o tipo:
- Solução 1: substituir por uma classe comum
class ButtonConfig {
final String label;
final Icon icon;
final void Function()? onPressed;
ButtonConfig({required this.label, required this.icon, this.onPressed});
// Adicionar método personalizado
bool get hasClickEvent => onPressed != null;
}
Code language: PHP (php)
- Solução 2: envolver o Record original com Extension Type
extension type ButtonConfig._(({String label, Icon icon, void Function()? onPressed}) _) {
String get label => _.label;
Icon get icon => _.icon;
void Function()? get onPressed => _.onPressed;
ButtonConfig({required String label, required Icon icon, void Function()? onPressed})
: this._((label: label, icon: icon, onPressed: onPressed));
bool get hasClickEvent => _.onPressed != null;
}
Code language: PHP (php)
Com ambas as formas de migração, o código de negócio que percorre e renderiza botões na página permanece totalmente inalterado.
X. Resumo
- Requisito de versão: Dart 3.0 ou superior;
- Três atributos centrais: anônimo, imutável, agregado; tamanho fixo, armazenamento heterogêneo, tipagem forte;
- Dois tipos de campos: campos posicionais
$1/$2, campos nomeados.nome; - Regra de identificação de tipo: o tipo é definido pela forma estrutural, nomes de campos nomeados fazem parte da forma; apelidos de campos posicionais são apenas comentários;
- Comparação de igualdade: mesma forma + todos os valores dos campos iguais → true;
- Vantagem principal: múltiplos retornos de função, contêiner leve de dados, segurança total de tipos;
- Forma de expansão: typedef para simplificar apelidos de tipo, troca transparente para class / extension type posteriormente.