C# Structs

Na aula de Valor × Referência, você viu que int, decimal e DateTime são tipos de valor: copiar a variável copia o dado. Com struct ("estrutura"), você cria os seus tipos de valor. Por fora, um struct parece muito uma classe: tem propriedades, construtores e métodos. A diferença está no comportamento: cada variável tem a sua própria cópia.

Você vai aprender
  • Como declarar um struct com propriedades, construtor e métodos.
  • Por que p2 = p1 faz uma cópia, e o valor padrão de um struct.
  • readonly struct: structs que não mudam (o erro CS8341).
  • O que um struct pode e não pode fazer: interfaces sim, herança não (CS0527).
  • As armadilhas: a List de structs (CS1612), o foreach (CS1654) e o == (CS0019).
  • Quando usar struct e quando usar class.

Todos os programas desta aula rodaram com dotnet run arquivo.cs no .NET 10 da máquina da Ana, na pasta ~/cs.


A ideia: um número com mais de uma parte

Uma coordenada num mapa é um par de números: (2, 3). Uma cor é um trio: vermelho, verde, azul. Um valor em dinheiro é um número com uma moeda. São "valores" do mesmo jeito que o número 5: se você anota a coordenada da casa da Ana num papel e entrega para a Bia, a Bia fica com uma cópia. Se ela riscar e escrever outra, o papel da Ana não muda.

Para coisas pequenas assim, que são só um "valor composto", o C# tem o struct. Para coisas com identidade própria (uma conta bancária, um cliente, um carrinho de compras), continue usando class.


Um struct Ponto

Ponto casa = new Ponto(2, 3);
Console.WriteLine(casa);
Console.WriteLine($"Distância até a origem: {casa.Distancia():F2}");

struct Ponto
{
    public int X { get; set; }
    public int Y { get; set; }

    public Ponto(int x, int y)
    {
        X = x;
        Y = y;
    }

    public double Distancia() => Math.Sqrt(X * X + Y * Y);

    public override string ToString() => $"({X}, {Y})";
}
struct Ponto
Declara um struct. Tudo o que vem dentro das chaves é escrito como numa classe.
public int X { get; set; }
Propriedades, como as de uma classe (veja Propriedades).
public Ponto(int x, int y)
Um construtor, igual ao de uma classe.
public double Distancia()
Um método: a distância de (X, Y) até (0, 0), pelo teorema de Pitágoras (veja Math).
public override string ToString()
Todo struct herda de object (por meio de System.ValueType) e pode sobrescrever o ToString (veja A Classe object).
new Ponto(2, 3)
Com struct, o new não cria um objeto separado na memória: ele só chama o construtor para preencher o valor que fica dentro da variável casa.
$ dotnet run ponto.cs
(2, 3)
Distância até a origem: 3,61

A diferença: a cópia

Ponto p1 = new Ponto(2, 3);
Ponto p2 = p1;
p2.X = 10;
Console.WriteLine($"p1 = {p1}, p2 = {p2}");

struct Ponto
{
    public int X { get; set; }
    public int Y { get; set; }

    public Ponto(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString() => $"({X}, {Y})";
}
$ dotnet run copia.cs
p1 = (2, 3), p2 = (10, 3)

O p2 = p1 copiou os dois números para dentro de p2. Mudar o p2 não afeta o p1, exatamente como com dois int. Agora troque só uma palavra, struct Ponto por class Ponto, e rode de novo:

$ dotnet run copiaclasse.cs
p1 = (10, 3), p2 = (10, 3)

Com class, p2 = p1 copia o endereço, e os dois nomes apontam para o mesmo objeto. Mesmo código, comportamento oposto: por isso é importante saber se um tipo é struct ou class.

Missão: rode e veja que o p1 não muda. Agora troque struct Ponto por class Ponto e rode de novo: a saída deve começar com p1 = (10, 3). Depois, volte para struct, que é o certo para um ponto.

Ponto p1 = new Ponto(2, 3);
Ponto p2 = p1;
p2.X = 10;
Console.WriteLine($"p1 = {p1}, p2 = {p2}");

struct Ponto
{
    public int X { get; set; }
    public int Y { get; set; }

    public Ponto(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString() => $"({X}, {Y})";
}
Ponto p1 = new Ponto(2, 3);
Ponto p2 = p1;
p2.X = 10;
Console.WriteLine($"p1 = {p1}, p2 = {p2}");

class Ponto
{
    public int X { get; set; }
    public int Y { get; set; }

    public Ponto(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString() => $"({X}, {Y})";
}

O valor padrão

Um struct nunca é null (é um tipo de valor). Se você não preencher nada, ele nasce com tudo zerado: números em 0, bool em false, referências em null.

Ponto a = new Ponto();
Ponto b = default;
Ponto[] tres = new Ponto[3];
Console.WriteLine($"{a} {b} {tres[2]}");

struct Ponto
{
    public int X { get; set; }
    public int Y { get; set; }

    public override string ToString() => $"({X}, {Y})";
}
$ dotnet run padrao.cs
(0, 0) (0, 0) (0, 0)
new Ponto()
Sem argumentos: todos os campos zerados. Todo struct tem esse jeito de ser criado, mesmo que você escreva outros construtores.
default
O "valor padrão" do tipo. Para um struct, o mesmo que zerar tudo. (Para uma classe, seria null.)
new Ponto[3]
Um array de 3 pontos já prontos, todos (0, 0). Num array de uma classe, seriam 3 null.

O DateTime também é um struct, e o valor padrão dele é a "data zero": DateTime.MinValue aparece como 01/01/0001 00:00:00. Se você vir essa data num programa, quase sempre é um DateTime que ninguém preencheu.

O construtor não precisa preencher tudo C# 11

Desde o C# 11 (.NET 7), o construtor de um struct pode deixar propriedades sem valor: elas ficam zeradas. No C# 10 e antes, era obrigatório preencher todas, e o compilador dava o erro CS0843 ("A propriedade auto-implementada 'Cor.G' deve ser totalmente atribuída antes que o controle seja devolvido ao chamador"). Se você ler código antigo cheio de G = 0; B = 0; em construtores de struct, é por isso.


readonly struct: valores que não mudam

Valores de verdade não mudam: o número 5 é sempre 5. Para structs, isso também é o mais seguro, porque evita as armadilhas que você vai ver mais abaixo. Um readonly struct ("somente leitura") garante que nada nele muda depois de criado. Para "mudar", você cria um valor novo:

Dinheiro preco = new Dinheiro(18.90m);
Dinheiro dobro = preco.Vezes(2);
Console.WriteLine($"{preco} e {dobro}");

readonly struct Dinheiro
{
    public decimal Valor { get; }

    public Dinheiro(decimal valor)
    {
        Valor = valor;
    }

    public Dinheiro Vezes(int n) => new Dinheiro(Valor * n);

    public override string ToString() => Valor.ToString("C");
}
readonly struct Dinheiro
Um struct imutável. O compilador confere que nenhuma propriedade ou campo pode ser mudado depois do construtor.
public decimal Valor { get; }
Só get: o valor só pode ser definido no construtor.
public Dinheiro Vezes(int n) => new Dinheiro(Valor * n);
Em vez de mudar o valor, o método devolve um novo. É assim que o DateTime funciona: data.AddDays(1) não muda a data, devolve outra.
$ dotnet run dinheiro.cs
R$ 18,90 e R$ 37,80

Se a Ana tentar dar um set à propriedade (public decimal Valor { get; set; }):

$ dotnet run readonly-erro.cs
/home/ana/cs/readonly-erro.cs(7,20): error CS8341: Propriedades da instância autoimplementadas em structs somente leitura devem ser somente leitura.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Num readonly struct, propriedades automáticas só podem ter get (ou init; veja readonly e init).


O que um struct pode e não pode

Interfaces: pode

Um struct pode implementar interfaces, como uma classe:

IForma[] formas = [new Quadrado(2), new Quadrado(3)];
foreach (IForma f in formas)
{
    Console.WriteLine(f.Area());
}

interface IForma
{
    double Area();
}

struct Quadrado : IForma
{
    public double Lado { get; }

    public Quadrado(double lado)
    {
        Lado = lado;
    }

    public double Area() => Lado * Lado;
}
$ dotnet run iface.cs
4
9

Um detalhe: guardar um struct numa variável do tipo de uma interface (o IForma[]) faz boxing, uma caixa com uma cópia (veja Valor × Referência). Funciona, mas tira parte da vantagem do struct.

Herança: não pode

Structs não herdam de outros structs nem de classes, e ninguém herda deles (são selados, como os de sealed). Com struct Ponto3D : Ponto:

$ dotnet run heranca.cs
/home/ana/cs/heranca.cs(2,18): error CS0527: Tipo "Ponto" na lista de interfaces não é uma interface

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Repare na mensagem: para um struct, tudo o que vem depois do : é tratado como uma lista de interfaces. Como Ponto não é uma interface, vem o erro. Sem herança, também não há virtual, abstract nem protected em structs.


As armadilhas da cópia

1. Mudar um struct dentro de uma List

A Ana tem uma rota, uma lista de pontos, e quer mover o primeiro:

Este código tem um erro de propósito
List<Ponto> rota = [new Ponto(0, 0), new Ponto(1, 1)];
rota[0].X = 5;

// … o struct Ponto de antes
$ dotnet run lista.cs
/home/ana/cs/lista.cs(2,1): error CS1612: Não é possível modificar o valor de retorno "List<Ponto>.this[int]" porque ele não é uma variável

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que acontece: o rota[0] de uma List é um método por baixo dos panos (o this[int] da mensagem, um indexador) que devolve uma cópia do ponto. Mudar o X da cópia não serviria para nada (ela seria jogada fora), então o compilador proíbe. O conserto é copiar, mudar e devolver:

List<Ponto> rota = [new Ponto(0, 0), new Ponto(1, 1)];
Ponto primeiro = rota[0];
primeiro.X = 5;
rota[0] = primeiro;
Console.WriteLine(rota[0]);

Ponto[] array = [new Ponto(0, 0)];
array[0].X = 7;
Console.WriteLine(array[0]);

// … o struct Ponto de antes
$ dotnet run lista2.cs
(5, 0)
(7, 0)      « num array, pode: array[0] é a variável de verdade

O array é diferente: array[0] é o próprio lugar na memória, não uma cópia, e por isso array[0].X = 7 funciona.

2. Mudar a variável do foreach

No foreach, a variável é uma cópia de cada elemento, e o C# não deixa mudá-la:

Ponto[] rota = [new Ponto(0, 0), new Ponto(1, 1)];
foreach (Ponto p in rota)
{
    p.X = 9;
}
$ dotnet run foreach.cs
/home/ana/cs/foreach.cs(4,5): error CS1654: Não é possível modificar membros de "p" porque ele é um "variável de iteração foreach"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Para mudar os elementos, use um for com o índice: for (int i = 0; i < rota.Length; i++) rota[i].X = 9;.

3. O == não existe

Ponto a = new Ponto(1, 2);
Ponto b = new Ponto(1, 2);
Console.WriteLine(a.Equals(b));
Console.WriteLine(a == b);
$ dotnet run igual.cs
/home/ana/cs/igual.cs(4,19): error CS0019: O operador "==" não pode ser aplicado a operandos dos tipos "Ponto" e "Ponto"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Uma classe ganha um == de fábrica (que compara endereços). Um struct não ganha nenhum. O Equals funciona e compara os campos um a um (apagando a última linha, a saída é True), mas ele usa um mecanismo lento por baixo. Para ter ==, Equals rápido, GetHashCode e ToString prontos, use um record struct (veja Records).


Struct ou class?

structclass
FamíliaTipo de valorTipo de referência
b = aCopia o valor inteiroCopia o endereço
Pode ser nullNão (só com Ponto?)Sim
Valor padrãoTudo zeradonull
HerançaNão (só interfaces)Sim
== de fábricaNão existe (CS0019)Compara endereços
Bom paraValores pequenos e imutáveis: ponto, cor, dinheiro, medidaQuase todo o resto: cliente, conta, pedido, carrinho

Qual usar? Na dúvida, class. Use struct quando as quatro respostas forem "sim":

  1. Ele representa um valor, como um número (dois com o mesmo conteúdo são "o mesmo")?
  2. É pequeno (poucos campos, uns 16 bytes: dois double ou um decimal)? Structs grandes custam caro, porque são copiados inteiros a cada atribuição e a cada chamada de método.
  3. Pode ser imutável (readonly struct)?
  4. Não precisa de herança?

Exemplos do próprio .NET: int, double, decimal, bool, DateTime, TimeSpan, Guid são structs. string, List<T> e Random são classes.


Quando dá errado

CS1612: "Não é possível modificar o valor de retorno … porque ele não é uma variável"
$ dotnet run lista.cs
/home/ana/cs/lista.cs(2,1): error CS1612: Não é possível modificar o valor de retorno "List<Ponto>.this[int]" porque ele não é uma variável

O que aconteceu: você tentou mudar um campo de um struct que veio de uma lista, de uma propriedade ou de um método. O que veio foi uma cópia.

Como resolver: copie para uma variável, mude e guarde de volta (rota[0] = primeiro;), ou crie um valor novo: rota[0] = new Ponto(5, rota[0].Y);.

CS1654: "Não é possível modificar membros de … porque ele é um variável de iteração foreach"

Como resolver: troque o foreach por um for com índice (num array) ou monte uma lista nova.

CS0019: "O operador "==" não pode ser aplicado a operandos dos tipos …"

Como resolver: use a.Equals(b), transforme em record struct ou defina o operador (veja Sobrecarga de Operadores).

CS0527: "Tipo … na lista de interfaces não é uma interface"

O que aconteceu: um struct tentou herdar de outro tipo.

Como resolver: structs não têm herança. Coloque os dados em comum como uma propriedade (struct Ponto3D { public Ponto Base { get; } … }) ou use classes.


Referência

EscritaSignificado
struct Ponto { … }Declara um tipo de valor
readonly struct Dinheiro { … }Struct imutável (só get/init)
new Ponto(), defaultTudo zerado
struct Quadrado : IFormaStruct que implementa uma interface
Ponto? talvez = null;Struct anulável (veja null)
record struct Ponto(int X, int Y);Struct com ==, ToString e Equals prontos (veja Records)
lista[0].X = 5Proibido numa List de structs (CS1612); permitido num array

Exercícios

1. Declarar

Complete para declarar um tipo de valor chamado Cor:

Cor { }

2. Imutável

Complete para declarar um struct que não muda depois de criado:

Medida { }

3. A cópia

Ponto é um struct. Ponto a = new Ponto(1, 1); Ponto b = a; b.X = 4; Quanto vale a.X?

4. O que mostra new Ponto[2], sendo Ponto um struct com X e Y?

5. O que um struct pode fazer?

6. Qual destes é o melhor candidato a struct?

Missão final: a Ana quer mover o início da rota para X = 5, mas a linha rota[0].X = 5; dá o erro CS1612. Rode e leia o erro. Conserte sem transformar o Ponto em classe: copie rota[0] para uma variável, mude o X dela e guarde de volta em rota[0]. A saída deve ter Início: (5, 0).

List<Ponto> rota = [new Ponto(0, 0), new Ponto(1, 1), new Ponto(2, 3)];

rota[0].X = 5;

Console.WriteLine($"Início: {rota[0]}");
Console.WriteLine($"Pontos: {rota.Count}");

struct Ponto
{
    public int X { get; set; }
    public int Y { get; set; }

    public Ponto(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString() => $"({X}, {Y})";
}
List<Ponto> rota = [new Ponto(0, 0), new Ponto(1, 1), new Ponto(2, 3)];

Ponto inicio = rota[0];
inicio.X = 5;
rota[0] = inicio;

Console.WriteLine($"Início: {rota[0]}");
Console.WriteLine($"Pontos: {rota.Count}");

struct Ponto
{
    public int X { get; set; }
    public int Y { get; set; }

    public Ponto(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString() => $"({X}, {Y})";
}
$ dotnet run rota.cs      « a solução
Início: (5, 0)
Pontos: 3

Resumo

  • struct cria um tipo de valor: propriedades, construtores e métodos como numa classe, mas b = a copia tudo.
  • Um struct nunca é null; o padrão (new Ponto(), default, arrays) é tudo zerado. Desde o C# 11, o construtor pode deixar campos sem valor.
  • readonly struct é imutável (CS8341 se houver set): métodos devolvem valores novos, como o DateTime.
  • Structs implementam interfaces, mas não têm herança (CS0527) nem == de fábrica (CS0019).
  • Cuidado com cópias: lista[0].X = … (CS1612) e a variável do foreach (CS1654).
  • Use struct para valores pequenos e imutáveis; na dúvida, class. Próxima aula: Enums.