Uma struct é um tipo de valor que você mesmo define. Ela parece uma classe, com campos, propriedades, métodos e construtores, mas uma variável de um tipo struct contém os dados diretamente, em vez de uma referência para um objeto. Essa única diferença muda como se comportam a atribuição, as chamadas de métodos, a igualdade e o null.
Semântica de valor: a atribuição copia
Os tipos numéricos embutidos, bool, char, DateTime, TimeSpan e Guid são todos structs. As suas funcionam do mesmo jeito:
Saída:
struct: s1.X = 1, s2.X = 50
class: c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150
Com a struct, s2 é uma cópia independente, e Move também trabalha na sua própria cópia. Para deixar um método mudar a struct de quem chama, passe-a por referência com ref (veja ref e out). Para evitar copiar uma struct grande sem permitir mudanças, o C# 7.2 adicionou os parâmetros in.
Outras consequências de ser um tipo de valor:
- Uma variável struct nunca pode ser
null.PointS p = null;não compila. UsePointS?(um tipo de valor anulável) quando "nenhum valor" tem significado. - Uma struct não inicializada é toda zero: campos numéricos
0, camposboolfalse, campos de referêncianull. - Uma struct não pode herdar de outra struct ou classe, e nada pode herdar de uma struct. Ela pode implementar interfaces.
- Variáveis struct locais normalmente ficam na pilha ou dentro do objeto que as contém, então criar uma não aloca no heap.
A armadilha da List de structs: CS1612
Como ler uma struct produz uma cópia, esta linha de aparência inocente não compila:
var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable
O indexador de List<T> é um método que retorna uma cópia do elemento. Definir X nessa cópia temporária não mudaria nada, então o compilador impede você. O mesmo erro aparece quando uma propriedade retorna uma struct: order.Location.X = 10. Copie, modifique, escreva de volta:
Saída:
35
7
Os arrays são a exceção: array[0] é o próprio elemento, não uma cópia. Essa dor recorrente é o motivo do conselho padrão de tornar as structs imutáveis: se uma struct não pode ser modificada, não há nada a perder por modificar uma cópia.
Construtores e valores padrão
Do C# 7 ao 9, as regras para construtores de struct são rígidas:
- Você não pode declarar um construtor sem parâmetros.
new PointS()sempre existe e zera todos os campos. - Um construtor que você declara precisa atribuir todos os campos (e auto-properties) antes de retornar.
- Inicializadores de campo (
public int X = 1;) não são permitidos em campos de instância.
Saída:
19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)
Money também mostra por que o valor padrão importa: um Money zerado tem uma moeda null, e o seu código precisa tratar isso, porque arrays, default(T) e campos não inicializados produzem um.
Versões mais novas relaxaram essas regras:
// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
public int Retries = 3;
public Settings() { }
}
// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.
Um construtor sem parâmetros do C# 10 executa em new Settings(), mas não em default(Settings) nem nos elementos de um array, que continuam todos zerados. Essa divisão surpreende, então use-a com cuidado.
readonly struct (C# 7.2)
Marcar a própria struct como readonly faz o compilador garantir a imutabilidade: todo campo precisa ser readonly e toda auto-property só com get.
public readonly struct Temperature
{
public double Celsius { get; }
public Temperature(double celsius) { Celsius = celsius; }
public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta); // returns a new value
}
Além de documentar a intenção, ajuda o desempenho: quando uma struct não readonly é guardada em um campo readonly ou passada como parâmetro in, o compilador a copia antes de cada chamada de método (ele não tem como saber que o método não vai modificá-la). Uma readonly struct não precisa dessas cópias defensivas. No C# 7.0 você ainda pode tornar cada campo readonly, como Money faz com propriedades só com get: isso dá a imutabilidade, mas não economiza as cópias, porque o compilador só confia em uma struct declarada readonly.
Igualdade
Equals em uma struct compara campo a campo por padrão, que é o comportamento de valor que você quer. Mas a implementação padrão (ValueType.Equals) pode usar reflection e é lenta, e o operador == nem é definido: a == b na sua própria struct é o erro CS0019. Implemente os dois quando a struct for comparada:
Saída:
True
True
True
Implementar IEquatable<T> importa para as coleções: HashSet<T>, Dictionary<TKey, TValue> e List<T>.Contains chamam Equals(GridCell) diretamente, em vez de fazer boxing de cada valor para chamar Equals(object). As record structs (abaixo) geram tudo isso para você.
Boxing
Converter uma struct para object ou para um tipo de interface faz o boxing dela: o runtime copia o valor para um novo objeto no heap. A caixa e o original passam a ser independentes:
Saída:
2
0
O boxing custa uma alocação a cada vez, e é por isso que as antigas coleções não genéricas como ArrayList eram lentas com tipos de valor e por que os genéricos as substituíram. Isso também significa que uma struct mutável acessada por uma interface é alterada na caixa, não no original, mais um motivo para manter as structs imutáveis.
Struct vs class
| struct | class | |
|---|---|---|
| Tipo | tipo de valor | tipo de referência |
| Atribuição e parâmetros | copiam os dados | copiam a referência |
Pode ser null | não (T? pode) | sim |
| Valor padrão | todos os campos zerados | null |
| Herança | nenhuma; pode implementar interfaces | uma classe base, interfaces |
== | não definido, a menos que você o sobrecarregue | igualdade de referência, a menos que sobrecarregado |
Equals padrão | compara os campos | compara as referências |
| Alocação | embutida (pilha ou objeto que a contém) | heap, com garbage collector |
| Serve para | valores pequenos e imutáveis | entidades, estado grande ou compartilhado |
Quando usar uma struct
Escolha uma struct quando todas estas condições valem: o tipo é um único valor lógico (uma coordenada, um valor em dinheiro, uma cor, um intervalo de datas), é pequeno (a orientação da Microsoft é cerca de 16 bytes, mais ou menos quatro ints), é imutável e você não precisa de herança. Structs compensam quando você cria muitas delas, por exemplo milhões de pontos em um array, porque evitam uma alocação no heap e uma entrada no garbage collector por item.
Escolha uma class para tudo o que tem identidade (um cliente, um pedido), tudo o que é grande, tudo o que é alterado a partir de vários lugares, e sempre que estiver em dúvida. Uma struct grande e mutável dá os custos da cópia mais a confusão de modificar cópias.
record struct (C# 10)
O C# 10 adicionou a record struct, que gera igualdade por valor, ==, ToString e suporte a with para uma struct em uma linha:
public readonly record struct Coordinate(double Lat, double Lng);
var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same); // True
Console.WriteLine(home); // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };
Uma record struct simples tem propriedades posicionais mutáveis; readonly record struct as torna init-only, que normalmente é o que você quer. A página de records cobre a versão de classe e mostra os membros que o compilador escreve.
Erros comuns
- Modificar uma struct por meio de uma cópia.
list[0].X = 1(CS1612), uma variável doforeach(CS1654), o resultado do getter de uma propriedade. Escreva de volta a cópia modificada, ou torne a struct imutável. - Structs grandes. Toda atribuição e toda chamada copiam tudo. Passando de alguns campos, uma class normalmente é mais rápida.
- Esquecer o padrão zerado. Arrays e
default(T)criam valores struct sem executar o seu construtor, então campos que você valida ali ainda podem ser zero ounull. - Comparar com
==sem defini-lo. CS0019. ImplementeIEquatable<T>e os operadores, ou use uma record struct. - Structs mutáveis atrás de interfaces. O boxing significa que a mudança acontece em uma cópia que você não está olhando.
Perguntas frequentes
O que é uma struct em C#?
Uma struct é um tipo de valor definido pelo usuário: struct Point { public int X; public int Y; }. Uma variável de um tipo struct guarda os próprios dados, e não uma referência para um objeto, então a atribuição e a passagem para um método copiam o valor inteiro. int, double, DateTime e Guid são todos structs.
Qual a diferença entre uma struct e uma class em C#?
Uma class é um tipo de referência: as variáveis compartilham um objeto, e null é permitido. Uma struct é um tipo de valor: cada variável tem a sua própria cópia, ela não pode ser null (a menos que você use Point?), não pode herdar nem ser herdada, e o valor padrão dela é com todos os campos zerados. Structs servem para valores pequenos e imutáveis; classes servem para entidades com identidade e comportamento.
Quando usar uma struct em vez de uma class em C#?
Quando o tipo representa um único valor pequeno (uma coordenada, um valor em dinheiro, um intervalo de datas), é de preferência imutável, é comparado pelo conteúdo e é criado em grande quantidade, em que evitar alocações no heap importa. A orientação da Microsoft acrescenta um tamanho de cerca de 16 bytes ou menos. Se o tipo tem identidade, muitos campos ou é alterado por meio de referências, use uma class.
Por que recebo "Cannot modify the return value because it is not a variable"?
É o erro CS1612, normalmente vindo de list[0].X = 5 em uma List<Point> de structs. O indexador retorna uma cópia da struct, então a mudança na cópia se perderia, e o compilador recusa. Copie o elemento para uma variável, mude-a e atribua de volta: var p = list[0]; p.X = 5; list[0] = p;. Arrays não têm esse problema porque array[0] se refere ao próprio elemento.
O que é uma readonly struct em C#?
readonly struct (C# 7.2) declara que nenhum membro da struct modifica o seu estado: todos os campos precisam ser readonly e as auto-properties só com get. O compilador garante isso, e isso permite que ele evite cópias defensivas quando a struct é passada com in ou guardada em um campo readonly. A maioria das structs deveria ser readonly.