Tipos anônimos no C#

Tipos anônimos no C# permitem agrupar alguns valores em um objeto sem declarar antes uma classe, um struct ou um record. Neste artigo, vamos entender como eles funcionam, quando fazem sentido e quais alternativas o C# oferece para estruturar dados com pouca ou nenhuma tipagem explícita.

Tipar ou não tipar

O C# é uma linguagem versátil, presente em aplicações Web, Mobile, Desktop, inteligência artificial e jogos. Por isso, ele atende cenários muito diferentes entre si.

Entre as várias qualidades do C#, está a tipagem forte. Em outras palavras, toda variável tem um tipo definido, e o compilador verifica esse tipo antes de o código rodar.

Abaixo temos um exemplo em JavaScript e o mesmo exemplo em C#, apenas para efeito de comparação.

let name = 'André Baltieri';
string name = "André Baltieri";

No JavaScript, a variável aceita qualquer valor depois. Já no C#, a variável name só aceita textos durante toda a sua vida.

Se você quer conhecer melhor a linguagem antes de seguir, confira o nosso artigo sobre C#, que apresenta suas principais funcionalidades.

Quando não tipar

Um exemplo comum, que vivi mais de uma vez, é o consumo de APIs externas. Muitas vezes queremos fazer uma requisição, mas não queremos criar um tipo só para receber ou enviar alguns campos.

Outro cenário frequente é a importação e exportação de dados. Nesses casos, as tarefas são pequenas e pontuais, então criar tipos para tudo tomaria mais tempo do que o próprio trabalho.

É justamente aqui que os tipos anônimos e outras estruturas flexíveis entram em cena. Contudo, cada opção tem custos diferentes, e vale conhecer todas antes de escolher.

Dicionários como estrutura de dados

Normalmente lembramos dos dicionários para criar listas de chave e valor. Porém, também podemos usá-los para estruturar dados de um único objeto.

var person = new Dictionary<string, object>
{
    ["Name"] = "André Baltieri",
    ["BirthDate"] = DateTime.Now
};

Nesse exemplo, cada chave representa o nome de um campo e cada valor guarda o conteúdo correspondente. Assim, conseguimos montar uma estrutura parecida com um objeto sem declarar nenhuma classe.

Além do formato com colchetes, o C# aceita a sintaxe de inicialização com chaves, informando o par de chave e valor em cada linha.

var person = new Dictionary<string, object>
{
    { "Name", "André Baltieri" },
    { "BirthDate", DateTime.Now }
};

As duas formas produzem o mesmo resultado na prática. A diferença é que a sintaxe com colchetes usa o indexador e sobrescreve uma chave repetida, enquanto a sintaxe com chaves chama o método Add e lança uma exceção se a chave já existir.

Limitações do dicionário

O dicionário é flexível, mas paga um preço por isso. Como o tipo do valor é object, você perde a verificação do compilador e precisa converter cada valor ao ler.

// O compilador não sabe que o valor é um DateTime
var birthDate = (DateTime)person["BirthDate"];

// Um erro de digitação na chave só aparece em tempo de execução
var name = person["Nme"];

Além disso, tipos de valor como DateTime passam por boxing ao entrar em um object. Portanto, o dicionário funciona bem para dados realmente dinâmicos, mas raramente é a melhor escolha para estruturas conhecidas.

O que são tipos anônimos no C#

Se você não quer criar uma classe, um struct ou um record para seus dados, pode usar tipos anônimos no C#. Eles agrupam um conjunto de propriedades somente leitura em um único objeto, sem que você declare o tipo antes.

var person = new
{
    Name = "André Baltieri",
    BirthDate = DateTime.Now
};

Console.WriteLine(person.Name);

Como podemos notar, basta "fingir" que está instanciando uma classe e criar os nomes das propriedades e seus valores sob demanda. O compilador infere o tipo de cada propriedade a partir do valor informado.

Diferente do dicionário, aqui o compilador conhece cada propriedade. Por isso, o IntelliSense sugere Name e BirthDate, e um erro de digitação vira erro de compilação.

Como o compilador gera os tipos anônimos

Os tipos anônimos não são mágica. Segundo a documentação oficial sobre tipos anônimos, o compilador gera uma classe internal sealed que herda de object, com um nome que você não consegue acessar no código.

De forma simplificada, o exemplo anterior vira algo parecido com a classe abaixo.

// Representação aproximada do que o compilador gera
internal sealed class AnonymousType0
{
    public string Name { get; }
    public DateTime BirthDate { get; }

    public AnonymousType0(string name, DateTime birthDate)
    {
        Name = name;
        BirthDate = birthDate;
    }
}

Como o nome real é gerado pelo compilador, você precisa declarar a variável com var. Afinal, não existe um nome de tipo que você possa escrever no lugar.

Outro detalhe interessante: dois inicializadores no mesmo assembly, com as mesmas propriedades, nos mesmos tipos e na mesma ordem, compartilham o mesmo tipo gerado. Assim, você consegue comparar e colocar essas instâncias no mesmo array.

Arrays e listas de tipos anônimos

Como inicializadores idênticos compartilham o mesmo tipo, você pode criar um array implícito com vários objetos anônimos. Para isso, use new[] sem informar o tipo dos elementos.

var people = new[]
{
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 }
};

foreach (var person in people)
    Console.WriteLine($"{person.Name} is {person.Age} years old.");

Se um dos itens tiver uma propriedade a mais ou um tipo diferente, o compilador acusa erro. Afinal, cada combinação de propriedades gera um tipo distinto.

Também é possível transformar o array em lista com ToList(). Dessa forma, você continua com a tipagem completa e ganha os métodos de List<T>, mesmo sem saber o nome de T.

Nomes inferidos nos tipos anônimos

Você não precisa repetir o nome da propriedade quando ele já existe em uma variável. Nesse caso, o compilador usa o nome da variável ou do membro acessado.

var productName = "Notebook";
var price = 4999.90m;

var product = new { productName, price };

Console.WriteLine($"{product.productName}: {product.price}");

Esse recurso deixa as projeções mais curtas. Por exemplo, new { customer.Name, customer.Email } cria propriedades chamadas Name e Email automaticamente.

Características dos tipos anônimos

Os tipos anônimos têm regras bem definidas. Conhecer essas regras evita surpresas no dia a dia.

Primeiro, todas as propriedades são públicas e somente leitura. Ou seja, depois de criar o objeto, você não consegue alterar nenhum valor.

var person = new { Name = "André Baltieri", Age = 30 };

// Erro de compilação: a propriedade é somente leitura
// person.Name = "Balta";

Em segundo lugar, o compilador sobrescreve Equals, GetHashCode e ToString. Com isso, a comparação usa os valores das propriedades, não a referência do objeto.

var first = new { Name = "Alice", Age = 30 };
var second = new { Name = "Alice", Age = 30 };

Console.WriteLine(first.Equals(second)); // True
Console.WriteLine(first == second);      // False, compara referências
Console.WriteLine(first);                // { Name = Alice, Age = 30 }

Repare que o operador == continua comparando referências, porque o compilador não o sobrescreve. Portanto, use Equals quando quiser comparar os valores.

Por fim, um tipo anônimo pode conter outro tipo anônimo como valor de propriedade. Dessa forma, você monta estruturas aninhadas com pouco código.

var order = new
{
    Id = 1,
    Customer = new { Name = "Alice", City = "Seattle" },
    Total = 150.00m
};

Console.WriteLine(order.Customer.City);

Alterando valores com with

Como as propriedades são somente leitura, a forma de "alterar" um tipo anônimo é criar uma cópia. Desde o C# 10, a expressão with funciona com tipos anônimos e faz exatamente isso.

var original = new { Name = "Notebook", Price = 4999.90m };
var discounted = original with { Price = 4499.90m };

Console.WriteLine(original.Price);   // 4999.90
Console.WriteLine(discounted.Price); // 4499.90

O objeto original continua intacto, e a cópia recebe apenas o valor alterado. Se quiser aprofundar esse recurso, veja o artigo sobre a expressão with no C#.

Onde os tipos anônimos brilham

O uso mais comum dos tipos anônimos está nas consultas LINQ. Na cláusula select, eles projetam apenas as propriedades necessárias de cada item.

var words = new[] { "apple", "blueberry", "cherry" };

var results = words.Select(word => new { Word = word, Length = word.Length });

foreach (var item in results)
    Console.WriteLine($"{item.Word} has {item.Length} letters.");

Nesse caso, criar uma classe só para guardar a palavra e o tamanho seria exagero. Além disso, o resultado vive apenas dentro do método, então ninguém precisa conhecer o tipo.

Os tipos anônimos também aparecem no Entity Framework Core e em outros provedores LINQ. Isso acontece porque eles funcionam dentro de árvores de expressão, que esses provedores traduzem para SQL.

Na prática, uma consulta como context.Customers.Select(c => new { c.Name, c.Email }) busca no banco apenas as duas colunas pedidas. Com isso, você reduz o volume de dados trafegados e evita carregar entidades inteiras sem necessidade.

Para ver mais exemplos de projeções em consultas, confira o artigo sobre como utilizar o let no LINQ.

Respostas rápidas em APIs

Outro cenário prático é retornar um JSON simples em uma API. O serializador do .NET converte as propriedades do objeto anônimo em campos do JSON.

app.MapGet("/health", () => Results.Ok(new
{
    Status = "Healthy",
    CheckedAt = DateTime.UtcNow
}));

Isso resolve o caso que comentei no início, de não querer criar um tipo apenas para uma resposta pontual. Ainda assim, para contratos públicos de API, um tipo nomeado documenta melhor o retorno.

Limitações dos tipos anônimos

Os tipos anônimos são práticos, mas têm limites claros. O principal deles é o escopo, já que o tipo só existe de forma utilizável dentro do método que o declara.

Você não consegue usar tipos anônimos como retorno de método, parâmetro ou campo de classe. Afinal, não existe um nome de tipo para escrever nessas posições.

// Não compila: não há como nomear o tipo de retorno
// ??? GetPerson() => new { Name = "André" };

// Compila, mas perde a tipagem
object GetPerson() => new { Name = "André" };

Retornar como object até funciona, porém o chamador perde o acesso às propriedades sem reflexão ou dynamic. Nesse caso, a vantagem da tipagem forte desaparece.

Além disso, você não pode adicionar métodos, eventos ou operadores a um tipo anônimo. Ele serve apenas para carregar dados.

Por último, a desserialização é um ponto fraco. Como não há construtor acessível nem nome de tipo, transformar um JSON de volta em um tipo anônimo não é um caminho natural no System.Text.Json.

Erros comuns com tipos anônimos

O primeiro erro comum é atribuir null diretamente a uma propriedade. O compilador não consegue inferir o tipo de null, então você precisa de uma conversão explícita.

// Não compila: o tipo de null não pode ser inferido
// var item = new { Name = null };

// Compila: o tipo fica explícito
var item = new { Name = (string?)null };

Em seguida, vem a tentação de usar dynamic para contornar o escopo. Embora funcione em alguns cenários, você perde a verificação do compilador e o IntelliSense, que eram justamente as vantagens do recurso.

Por fim, evite espalhar tipos anônimos por métodos muito longos. Quando a mesma estrutura aparece em vários pontos, isso indica que ela merece um nome próprio.

Tipos anônimos ou tuplas

As tuplas resolvem um problema parecido e, para a maior parte do código novo, a Microsoft recomenda considerá-las primeiro. Elas são tipos de valor, aceitam desconstrução e podem ser usadas como retorno de métodos.

(string Name, DateTime BirthDate) GetPerson()
    => ("André Baltieri", DateTime.Now);

var (name, birthDate) = GetPerson();

Por outro lado, os tipos anônimos continuam sendo a escolha certa quando você precisa de árvores de expressão ou de semântica de tipo de referência. A comparação oficial entre tipos anônimos e tuplas resume essas diferenças.

Característica Tipos anônimos ValueTuple
Categoria Classe (referência) Struct (valor)
Nomes personalizados Sim Sim
Desconstrução Não Sim
Árvores de expressão Sim Não
Retorno de método Não Sim

Para aprofundar nas tuplas, leia o artigo completo sobre tuplas no C#. Em seguida, veja também como o Deconstruct no C# funciona em outros tipos.

Tipagem com records

Caso queira uma abordagem com o mínimo de tipagem possível, mas com um tipo nomeado, a recomendação mais simples são os records. Eles chegaram no C# 9 e reduzem a declaração a uma única linha.

public record Person(string Name, DateTime BirthDate);

var person = new Person("André Baltieri", DateTime.Now);

Basta definir o tipo e o nome das propriedades na criação do record, como se fossem os parâmetros de um método. Então, o compilador gera o construtor, as propriedades, a igualdade por valor e o ToString.

Na prática, o record oferece quase tudo que os tipos anônimos oferecem. A diferença é que ele tem nome, então você pode usá-lo como retorno, parâmetro e contrato de API.

Tipagem com classes

Por fim, se você quer o modelo clássico, com todo o poder da orientação a objetos, vale a pena ir de classes. Elas permitem comportamento, encapsulamento, herança e validações.

public class Person
{
    public string Name { get; init; } = string.Empty;
    public DateTime BirthDate { get; init; }
}

var person = new Person
{
    Name = "André Baltieri",
    BirthDate = DateTime.Now
};

O modificador init permite atribuir as propriedades apenas na inicialização. Assim, a classe fica imutável depois de criada, de forma parecida com os tipos anônimos.

Se você quer revisar esses conceitos, o artigo sobre orientação a objetos é um bom ponto de partida.

Como escolher entre tipos anônimos e tipos nomeados

A escolha depende principalmente do alcance do dado. Se ele nasce e morre dentro de um método, tipos anônimos ou tuplas resolvem bem.

Se o dado precisa sair do método, a tupla atende casos simples. Contudo, quando o dado representa um conceito do domínio, prefira um record ou uma classe.

  • Dicionário: dados realmente dinâmicos, com chaves que você só conhece em tempo de execução.
  • Tipo anônimo: projeções LINQ, árvores de expressão e respostas pontuais.
  • Tupla: retornos múltiplos e agrupamentos temporários entre métodos.
  • Record: dados imutáveis com nome, igualdade por valor e pouca cerimônia.
  • Classe: modelos com comportamento, regras de negócio e ciclo de vida próprio.

Em resumo, comece pela opção mais simples que resolve o problema. Depois, evolua para um tipo nomeado assim que o dado ganhar importância no sistema.

Conclusão

Podemos trabalhar com tipos anônimos de forma simples no C#, sem a necessidade de criar classes, structs ou records para cada pequeno conjunto de dados. Eles são ótimos para projeções, consultas LINQ e respostas rápidas.

Ao mesmo tempo, conhecer as alternativas faz diferença. Dicionários, tuplas, records e classes completam o repertório e cobrem os casos em que os tipos anônimos não alcançam.

Portanto, use os tipos anônimos quando o dado for local e passageiro. Quando ele precisar atravessar fronteiras, dê um nome a ele.

FAQ

O que são tipos anônimos no C#?

São classes geradas pelo compilador a partir de um inicializador new { }, com propriedades públicas e somente leitura. Você não declara o tipo, e o compilador infere o tipo de cada propriedade.

Posso alterar uma propriedade de um tipo anônimo?

Não, todas as propriedades são somente leitura. Desde o C# 10, porém, você pode criar uma cópia com valores diferentes usando a expressão with.

Posso retornar um tipo anônimo de um método?

Não diretamente, porque não existe um nome de tipo para declarar o retorno. Nesse caso, use uma tupla, um record ou uma classe.

Qual a diferença entre tipos anônimos e tuplas?

Os tipos anônimos são classes e suportam árvores de expressão. As tuplas (ValueTuple) são structs, suportam desconstrução e podem ser usadas como retorno de métodos.

Dois tipos anônimos iguais são o mesmo tipo?

Sim, quando estão no mesmo assembly e têm as mesmas propriedades, com os mesmos nomes, tipos e ordem. Nesse caso, o compilador reutiliza o mesmo tipo gerado.