Tipos de estrutura (referência de C#)

Um tipo de estrutura (ou tipo de struct) é um tipo de valor que pode encapsular dados e funcionalidades relacionadas. Você usa a palavra-chave struct para definir um tipo de estrutura:

public struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; }
    public double Y { get; }

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

Para obter informações sobre os tipos ref struct e readonly ref struct, confira o artigo tipos de estrutura ref.

Os tipos de estrutura têm semântica de valor. Ou seja, uma variável de um tipo de estrutura contém uma instância do tipo. Por padrão, os valores das variáveis ​​são copiados na atribuição, passando um argumento para um método e retornando um resultado de método. Para variáveis de tipo de estrutura, uma instância do tipo é copiada. Para obter mais informações, confira Tipos de valor.

Normalmente, você usa tipos de estrutura para criar pequenos tipos centrados em dados que fornecem pouco ou nenhum comportamento. Por exemplo, o .NET usa tipos de estrutura para representar um número ( inteiro e real), um valor booliano, um caractere Unicode, uma instância de tempo. Se você estiver focado no comportamento de um tipo, considere definir uma classe. Os tipos de classe têm semântica de referência. Ou seja, uma variável de um tipo de classe contém uma referência a uma instância do tipo, não à instância em si.

Como os tipos de estrutura têm semântica de valor, recomendamos que você defina tipos de estrutura imutáveis.

Struct readonly

Use o modificador readonly para declarar que um tipo de estrutura é imutável. Todos os membros de dados de um struct readonly devem ser somente leitura da seguinte maneira:

  • Qualquer declaração de campo deve ter o modificador readonly
  • Qualquer propriedade, incluindo as implementadas automaticamente, precisa ser somente leitura ou somente init.

Isso garante que nenhum membro de um struct readonly modifique o estado do struct. Isso significa que outros membros de instância, exceto constructos, são implicitamente readonly.

Observação

Em um struct readonly, um membro de dados de um tipo de referência mutável ainda pode alterar seu próprio estado. Por exemplo, você não pode substituir uma instância List<T>, mas pode adicionar novos elementos a ela.

O seguinte código define um struct readonly com setters de propriedade somente inicialização:

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; init; }
    public double Y { get; init; }

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

Membros da instância readonly

Você também pode usar o modificador readonly para declarar que um membro de instância não modifica o estado de um struct. Se você não puder declarar todo o tipo de estrutura como readonly, use o modificador readonly para marcar os membros da instância que não modificam o estado do struct.

Em um membro de instância readonly, você não pode atribuir aos campos de instância da estrutura. No entanto, um membro readonly pode chamar um não membro readonly. Nesse caso, o compilador cria uma cópia da instância da estrutura e chama o membro não readonly nessa cópia. Como resultado, a instância de estrutura original não é modificada.

Normalmente, você aplica o modificador readonly aos seguintes tipos de membros de instância:

  • métodos:

    public readonly double Sum()
    {
        return X + Y;
    }
    

    Você também pode aplicar o modificador readonly aos métodos que substituem os métodos declarados em System.Object:

    public readonly override string ToString() => $"({X}, {Y})";
    
  • propriedades e indexadores:

    private int counter;
    public int Counter
    {
        readonly get => counter;
        set => counter = value;
    }
    

    Se você precisar aplicar o modificador readonly a ambos os acessadores de uma propriedade ou indexador, aplique-o na declaração da propriedade ou do indexador.

    Observação

    O compilador declara um acessador get de uma propriedade autoimplementada como readonly, independentemente da presença do modificador readonly em uma declaração de propriedade.

    Você pode aplicar o modificador readonly a uma propriedade ou a um indexador com um acessador init:

    public readonly double X { get; init; }
    

Você pode aplicar o modificador readonly a campos estáticos de um tipo de estrutura, mas não a outros membros estáticos, como propriedades ou métodos.

O compilador pode usar o modificador readonly para otimizações de desempenho. Para obter mais informações, confira Como evitar alocações.

Mutação não destrutiva

A partir do C# 10, você pode usar a expressãowith para produzir uma cópia de uma instância do tipo estrutura com as propriedades e campos especificados modificados. Você usa a sintaxe do inicializador de objeto para especificar quais membros modificar e seus novos valores.

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; init; }
    public double Y { get; init; }

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

public static void Main()
{
    var p1 = new Coords(0, 0);
    Console.WriteLine(p1);  // output: (0, 0)

    var p2 = p1 with { X = 3 };
    Console.WriteLine(p2);  // output: (3, 0)

    var p3 = p1 with { X = 1, Y = 4 };
    Console.WriteLine(p3);  // output: (1, 4)
}

Struct record

A partir do C# 10, você pode definir tipos de estrutura de registro. Os tipos de registro fornecem funcionalidade interna para encapsular dados. Você pode definir os tipos record struct e readonly record struct. Um struct de registro não pode ser um ref struct. Saiba mais e obtenha exemplos em Registros.

Matrizes embutidas

Começando com C# 12, você pode declarar as matrizes embutidas como um tipo struct:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
    private char _firstElement;
}

Uma matriz embutida é uma estrutura que contém um bloco contíguo de N elementos do mesmo tipo. É um equivalente de código seguro da declaração buffer fixo disponível apenas em código inseguro. Uma matriz embutida é um struct com as seguintes características:

  • Ele contém um único campo.
  • O struct não especifica um layout explícito.

Além disso, o compilador valida o atributo System.Runtime.CompilerServices.InlineArrayAttribute:

  • O comprimento deve ser maior que zero (> 0).
  • O tipo de destino deve ser um struct.

Na maioria dos casos, uma matriz embutida pode ser acessada como uma matriz, tanto para leitura quanto para gravação de valores. Além disso, você pode usar os operadores range e index.

Existem restrições mínimas quanto ao tipo do campo único. Ele não pode ser um tipo de ponteiro, mas pode ser qualquer tipo de referência ou qualquer tipo de valor. Você pode usar matrizes embutidas com quase qualquer estrutura de dados C#.

As matrizes embutida são um recurso avançado da linguagem. Destinam-se a cenários de alto desempenho em que um bloco de elementos embutidos e contíguo é mais rápido do que outras estruturas de dados alternativas. Você pode aprender mais sobre matrizes embutidas a partir da funcionalidade speclet

Inicialização de struct e valores padrão

Uma variável de um tipo structcontém diretamente os dados para struct. Isso cria uma distinção entre um valor não inicializado struct, que tem seu valor padrão e um inicializado struct, que armazena valores definidos pela construção dele. Por exemplo, considere o código a seguir:

public readonly struct Measurement
{
    public Measurement()
    {
        Value = double.NaN;
        Description = "Undefined";
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; }

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement();
    Console.WriteLine(m1);  // output: NaN (Undefined)

    var m2 = default(Measurement);
    Console.WriteLine(m2);  // output: 0 ()

    var ms = new Measurement[2];
    Console.WriteLine(string.Join(", ", ms));  // output: 0 (), 0 ()
}

Como mostra o exemplo anterior, a expressão de valor padrão ignora um construtor sem parâmetros e produz o valor padrão do tipo de estrutura. A instanciação de matriz do tipo estrutura também ignora um construtor sem parâmetros e produz uma matriz preenchida com os valores padrão de um tipo de estrutura.

A situação mais comum em que você verá valores padrão está em matrizes ou em outras coleções em que o armazenamento interno inclui blocos de variáveis. O exemplo a seguir cria uma matriz de 30 estruturas TemperatureRange, cada uma com o valor padrão:

// All elements have default values of 0:
TemperatureRange[] lastMonth = new TemperatureRange[30];

Todos os campos membros de um struct precisam ser atribuídos definitivamente quando ele é criado porque os tipos struct armazenam dados diretamente. O valor default de um struct definitivamente atribuiu todos os campos a 0. Todos os campos devem ser atribuídos definitivamente quando um construtor é invocado. Você inicializa campos usando os seguintes mecanismos:

  • Você pode adicionar inicializadores de campo a qualquer campo ou propriedade implementada automaticamente.
  • Você pode inicializar quaisquer campos ou propriedades automáticas no corpo do construtor.

A partir do C# 11, se você não inicializar todos os campos em um struct, o compilador adicionará código ao construtor que inicializa esses campos ao valor padrão. O compilador executa sua análise de atribuição definida habitual. Todos os campos acessados antes de serem atribuídos ou não atribuídos definitivamente quando o construtor terminar de executar receberão seus valores padrão antes da execução do corpo do construtor. Se this for acessado antes de todos os campos serem atribuídos, o struct será inicializado para o valor padrão antes que o corpo do construtor seja executado.

public readonly struct Measurement
{
    public Measurement(double value)
    {
        Value = value;
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public Measurement(string description)
    {
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; } = "Ordinary measurement";

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement(5);
    Console.WriteLine(m1);  // output: 5 (Ordinary measurement)

    var m2 = new Measurement();
    Console.WriteLine(m2);  // output: 0 ()

    var m3 = default(Measurement);
    Console.WriteLine(m3);  // output: 0 ()
}

Cada struct tem um construtor sem parâmetro public. Se você escrever um construtor sem parâmetros, ele deverá ser público. Se um struct declarar qualquer inicializador de campo, ele deverá declarar explicitamente um constructo. Esse constructo não precisa ser sem parâmetros. Se um struct declarar um inicializador de campo, mas nenhum constructo, o compilador relatará um erro. Qualquer constructo declarado explicitamente (com parâmetros ou sem parâmetros) executa todos os inicializadores de campo para esse struct. Todos os campos sem um inicializador de campo ou uma atribuição em um constructo são definidos como valor padrão. Para obter mais informações, consulte nota de proposta de recurso de construtores de struct sem parâmetros.

Começando no C# 12, tipos struct podem definir um construtor primário como parte de sua declaração. Os construtores primários fornecem uma sintaxe concisa para os parâmetros do construtor que podem ser usados em todo o corpo struct, em qualquer declaração de membro para esse struct.

Se todos os campos de instância de um tipo de estrutura estiverem acessíveis, você também poderá instanciá-lo sem o operador new. Nesse caso, você deve inicializar todos os campos de instância antes do primeiro uso da instância. O seguinte exemplo mostra como fazer isso:

public static class StructWithoutNew
{
    public struct Coords
    {
        public double x;
        public double y;
    }

    public static void Main()
    {
        Coords p;
        p.x = 3;
        p.y = 4;
        Console.WriteLine($"({p.x}, {p.y})");  // output: (3, 4)
    }
}

No caso dos tipos de valor internos, use os literais correspondentes para especificar um valor do tipo.

Limitações com o design de um tipo de estrutura

Os structs têm a maioria dos recursos de um tipo de classe. Há algumas exceções e algumas exceções que foram removidas em versões mais recentes:

  • Um tipo de estrutura não pode herdar de outro tipo de classe ou estrutura e não pode ser a base de uma classe. No entanto, um tipo de estrutura pode implementar interfaces.
  • Você não pode declarar um finalizador dentro de um tipo de estrutura.
  • Antes do C# 11, um construtor de um tipo de estrutura deve inicializar todos os campos de instância do tipo.

Passando variáveis de tipo de estrutura por referência

Quando você passa uma variável de tipo de estrutura para um método como um argumento ou retorna um valor de tipo de estrutura de um método, toda a instância de um tipo de estrutura é copiada. Passar por valor pode afetar o desempenho do código em cenários de alto desempenho que envolvem tipos de estrutura grandes. Você pode evitar a cópia de valor passando uma variável de tipo de estrutura por referência. Use os modificadores de parâmetro ref, out, in ou ref readonly método para indicar que um argumento deve ser passado por referência . Use ref returns para retornar um resultado de método por referência. Para obter mais informações, confira Evitar alocações.

restrição de struct

Você também usa a palavra-chave struct na restrição struct para especificar que um parâmetro de tipo é um tipo de valor não anulável. Os tipos de estrutura e enumeração atendem à restrição struct.

Conversões

Para qualquer tipo de estrutura (exceto tipos ref struct), existem conversões de boxing e unboxing de e para os tipos System.ValueType e System.Object. Também existem conversões de boxing e unboxing entre um tipo de estrutura e qualquer interface que ele implemente.

Especificação da linguagem C#

Para saber mais, confira a seção Structs da Especificação da linguagem C#.

Para mais informações sobre struct recursos, consulte as seguintes notas sobre a proposta de recurso:

Confira também