16 Estruturas

16.1 Geral

As estruturas são semelhantes às classes, pois representam estruturas de dados que podem conter membros de dados e membros de função. No entanto, ao contrário das classes, os structs são tipos de valor e não exigem alocação de heap. Uma variável de um struct tipo contém diretamente os dados do struct, enquanto uma variável de um tipo de classe contém uma referência aos dados, este último conhecido como objeto.

Observação: as estruturas são particularmente úteis para pequenas estruturas de dados que têm semântica de valor. Números complexos, pontos em um sistema de coordenadas ou pares chave-valor em um dicionário são exemplos de structs. A chave para essas estruturas de dados é que elas têm poucos membros de dados, que não exigem o uso de herança ou semântica de referência, em vez disso, podem ser convenientemente implementadas usando semântica de valor em que a atribuição copia o valor em vez da referência. nota final

Conforme descrito em §8.3.5, os tipos simples fornecidos pelo C#, como int, doublee bool, são, na verdade, todos os tipos de struct.

16.2 Declarações de structs

16.2.1 Geral

Um struct_declaration é um type_declaration (§14.8) que declara um novo struct:

struct_declaration
    : non_record_struct_declaration
    | record_struct_declaration
    ;

non_record_struct_declaration
    : attributes? struct_modifier* 'ref'? 'partial'? 'struct'
      identifier type_parameter_list? struct_interfaces?
      type_parameter_constraints_clause* struct_body ';'?
    ;

record_struct_declaration
    : attributes? struct_modifier* 'partial'? 'record' 'struct'
      identifier type_parameter_list? delimited_parameter_list? struct_interfaces?
      type_parameter_constraints_clause* record_struct_body
    ;

record_struct_body
    : struct_body ';'?
    | ';'
    ;

Um struct_declaration é para um struct não registro ou um struct de registro.

Um non_record_struct_declaration consiste em um conjunto opcional de atributos (§23), seguido por um conjunto opcional de struct_modifiers (§16.2.2), seguido por um modificador opcional ref (§16.2.3), seguido por um modificador parcial opcional (§15.2.7), seguido pela palavra-chave struct e um identificador que nomeia o struct, seguido por uma especificação de type_parameter_list opcional (§15.2.3), seguido por uma especificação opcional de struct_interfaces (§16.2.5), seguida por uma especificação opcional de cláusulas de type_parameter_constraints (§15.2.5), seguida por um struct_body (§16.2.6), opcionalmente seguido por um ponto-e-vírgula.

Um record_struct_declaration consiste em um conjunto opcional de atributos (§23), seguido por um conjunto opcional de struct_modifiers (§16.2.2), seguido por um modificador parcial opcional (§15.2.7), seguido pela palavra-chave record, seguida pela palavra-chave struct e um identificador que nomeia o struct, seguido por uma especificação de type_parameter_list opcional (§15.2.3), seguida por um delimited_parameter_list opcional especificação (§15.2.1), seguida por uma especificação opcional de struct_interfaces (§16.2.5), seguida por uma especificação opcional de cláusulas de type_parameter_constraints (§15.2.5), seguida por um record_struct_body.

Um struct_declaration não deve fornecer type_parameter_constraints_clauses, a menos que também forneça um type_parameter_list.

Um struct_declaration que fornece um type_parameter_list é uma declaração de struct genérica. Além disso, qualquer struct aninhado dentro de uma declaração de classe genérica ou uma declaração de struct genérica é em si uma declaração de struct genérica, uma vez que os argumentos de tipo para o tipo que o contém devem ser fornecidos para criar um tipo construído (§8.4).

Um non_record_struct_declaration que inclui um ref modificador não deve ter uma parte struct_interfaces .

Um record_struct_declaration ter um delimited_parameter_list declara um struct de registro posicional.

No máximo, apenas um record_struct_declaration que contém partial pode fornecer um delimited_parameter_list.

Os parâmetros em delimited_parameter_list não devem ter refmodificadores out ou this modificadores; no entanto, in e params modificadores são permitidos. Para um record_struct_declaration, os record_struct_bodye {}{};; são equivalentes. Todos eles indicam que os únicos membros são aqueles sintetizados pelo compilador (§16.4).

16.2.2 Modificadores de struct

Um struct_declaration pode incluir opcionalmente uma sequência de struct_modifiers:

struct_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'readonly'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier (§24.2) só está disponível em código não seguro (§24).

É um erro em tempo de compilação para o mesmo modificador aparecer várias vezes em uma declaração de estrutura.

Com exceção de readonly, os modificadores de uma declaração struct têm o mesmo significado que os de uma declaração de classe (§15.2.2).

O readonly modificador indica que o struct_declaration declara um tipo cujas instâncias são imutáveis.

Um stuct somente leitura tem as seguintes restrições:

  • Cada um de seus campos de instância também deve ser declarado readonly.
  • Ele não deve declarar nenhum evento semelhante a um campo (§15.8.2).

Quando uma instância de um struct somente leitura é passada para um método, seu this é tratado como um argumento/parâmetro de entrada, o que não permite o acesso de gravação a qualquer campo de instância (exceto por construtores).

16.2.3 Modificador Ref

O ref modificador indica que o non_record_struct_declaration declara um tipo cujas instâncias são alocadas na pilha de execução. Esses tipos são chamados de "ref struct" types. O ref modificador declara que as instâncias podem conter campos semelhantes a ref e não devem ser copiadas de seu contexto seguro (§16.5.15). As regras para determinar o contexto seguro de um struct ref são descritas em §16.5.15.

É um erro em tempo de compilação se um tipo de "ref struct" for usado em qualquer um dos seguintes contextos:

  • Como o tipo de elemento de uma matriz.
  • Como o tipo declarado de um campo de uma classe ou struct que não tem o modificador ref.
  • Como um argumento de tipo.
  • Como o tipo de um elemento de tupla.
  • Em um método assíncrono.
  • Em um iterador.
  • Como o tipo de receptor para uma conversão de grupo de métodos de um método de instância para um tipo delegado.
  • Como uma variável capturada em uma expressão lambda ou uma função local.

Além disso, as seguintes restrições se aplicam a um ref struct tipo:

  • Um ref struct tipo não deve ser encaixoado System.ValueType ou System.Object.
  • Um ref struct tipo não deve ser declarado para implementar nenhuma interface.
  • Um método de instância declarado em object ou em System.ValueType , mas não substituído em um tipo ref struct, não deve ser chamado com um receptor desse tipo ref struct.

Nota: A ref struct não deve declarar async métodos de instância nem usar uma instrução yield return ou yield break dentro de um método de instância, porque o parâmetro implícito this não pode ser usado nesses contextos. nota final

Essas restrições garantem que uma variável do tipo ref struct não se refira à memória de pilha que não é mais válida ou a variáveis que não são mais válidas.

16.2.4 Modificador parcial

O partial modificador indica que essa struct_declaration é uma declaração de tipo parcial. Várias declarações de struct parciais com o mesmo nome dentro de um namespace delimitador ou declaração de tipo se combinam para formar uma declaração de struct, seguindo as regras especificadas em §15.2.7.

16.2.5 Interfaces de struct

Uma declaração de struct pode incluir uma especificação struct_interfaces , caso em que se diz que o struct implementa diretamente os tipos de interface fornecidos. Para um tipo de struct construído, incluindo um tipo aninhado declarado em uma declaração de tipo genérico (§15.3.9.7), cada tipo de interface implementado é obtido substituindo, para cada type_parameter na interface fornecida, o type_argument correspondente do tipo construído.

struct_interfaces
    : ':' interface_type_list
    ;

O tratamento de interfaces em várias partes de uma declaração de struct parcial (§15.2.7) é discutido mais adiante em §15.2.4.3.

As implementações de interface são discutidas ainda mais em §19.6.

16.2.6 Corpo de struct

A struct_body de um struct define os membros do struct.

struct_body
    : '{' struct_member_declaration* '}'
    ;

16.3 Membros da estrutura

16.3.1 Geral

Os membros de um struct são compostos pelos membros introduzidos por seus struct_member_declaration e pelos membros herdados do tipo System.ValueType. Para um struct de registro, o conjunto de membros também inclui os membros sintetizados gerados pelo compilador (§synth-members).

struct_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | static_constructor_declaration
    | type_declaration
    | fixed_size_buffer_declaration   // unsafe code support
    ;

fixed_size_buffer_declaration (§24.8.2) só está disponível em código não seguro (§24).

Nota: Todos os tipos de class_member_declarations, exceto finalizer_declaration, também são struct_member_declarations. nota final

Com exceção das diferenças observadas no §16.5, as descrições dos membros da classe fornecidas em §15.3 a §15.12 também se aplicam aos membros do struct.

É um erro para um campo de instância de um struct de registro ter um tipo não seguro.

16.3.2 Membros readonly

Uma definição de membro de instância ou acessador de uma propriedade de instância, indexador ou evento que inclui o readonly modificador tem as seguintes restrições:

  • O this parâmetro é uma ref readonly referência.
  • O membro não deve reatribuir o valor ou this um campo de instância do receptor.
  • O membro não deve reatribuir o valor de um evento semelhante a um campo de instância (§15.8.2) do receptor.
  • Se um membro readonly invocar um membro não readonly, a estrutura referenciada deve this ser copiada para usar uma referência gravável para o this argumento.

Nota: Os campos de instância incluem o campo de backup oculto usado para propriedades implementadas automaticamente (§15.7.4). nota final

Exemplo: um membro readonly pode modificar o estado de um objeto referenciado por um campo de instância, mesmo que o membro readonly não possa reatribuir esse membro da instância. O código a seguir demonstra a reatribução e modificação de um campo de instância:

public struct S
{
    private List<string> messages;

    public S(IEnumerable<string> messages) =>
        this.messages = new List<string>(messages);

    public void InitializeMessages() =>
        messages = new List<string>();

    public readonly void AddMessage(string message)
    {
        if (messages == null)
        {
            throw new InvalidOperationException("Messages collection is not initialized.");
        }
        messages.Add(message);
    }
}

O readonly método AddMessage pode alterar o estado de uma lista de mensagens. O InitializeMessages membro pode limpar e inicializar novamente a lista de mensagens. No caso de AddMessage, o readonly modificador é válido. No caso de InitializeMessages, adicionar o readonly modificador é inválido. exemplo final

16.4 Membros do struct de registro sintetizado

16.4.1 Geral

No caso de um struct de registro, os membros são sintetizados, a menos que um membro com uma assinatura "correspondente" seja declarado no record_struct_body ou um membro não virtual concreto acessível com uma assinatura "correspondente" seja herdado. Dois membros são considerados correspondentes se tiverem a mesma assinatura ou forem considerados "ocultos" em um cenário de herança. (Consulte Assinaturas e sobrecarga de §7.6.)

Os membros sintetizados são descritos nas subclaus a seguir.

16.4.2 Membros de igualdade

Os membros de igualdade sintetizados são semelhantes aos de uma classe de registro (§15.16.2), exceto pela falta de EqualityContract, verificações nulas ou herança.

Um struct R de registro implementa System.IEquatable<R> e inclui uma sobrecarga sintetizada fortemente tipada, Equals(R other)que é pública, da seguinte maneira:

public readonly bool Equals(R other);

Esse método pode ser declarado explicitamente. No entanto, será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada.

Se Equals(R other) for definido pelo usuário (ou seja, não sintetizado), mas GetHashCode não for, um aviso será produzido.

O sintetizado Equals(R) retornará true se e somente se para cada campo fieldN de instância no registro struct o valor de System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN), onde TN está o tipo de campo, é true.

O struct de registro inclui sintetizado == e != operadores equivalentes aos operadores declarados da seguinte maneira:

public static bool operator==(R r1, R r2) => r1.Equals(r2);
public static bool operator!=(R r1, R r2) => !(r1 == r2);

O Equals método chamado pelo == operador é o Equals(R other) método especificado acima. O != operador delega ao == operador. Será um erro se os operadores forem declarados explicitamente.

O struct de registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte maneira:

public override readonly bool Equals(object? obj);

Será um erro se a substituição for declarada explicitamente. A substituição sintetizada deve retornar other is R temp && Equals(temp) onde R está o struct de registro.

O struct de registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte maneira:

public override readonly int GetHashCode();

Esse método pode ser declarado explicitamente.

Um aviso será relatado se um deles Equals(R)GetHashCode() for declarado explicitamente, mas o outro método não for.

A substituição GetHashCode() sintetizada deve retornar um int resultado da combinação dos valores de cada campo fieldN de System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) instância com TN o tipo de fieldN.

Exemplo: considere o seguinte struct de registro:

record struct R1(T1 P1, T2 P2);

Para isso, os membros de igualdade sintetizados seriam algo como:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }
    public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
    public bool Equals(R1 other)
    {
        return
            EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R1 r1, R1 r2) => r1.Equals(r2);
    public static bool operator!=(R1 r1, R1 r2) => !(r1 == r2);    
    public override int GetHashCode()
    {
        return HashCode.Combine(
            EqualityComparer<T1>.Default.GetHashCode(P1),
            EqualityComparer<T2>.Default.GetHashCode(P2));

exemplo final

16.4.3 Membros de impressão

Um struct de registro inclui um método sintetizado equivalente ao seguinte:

private bool PrintMembers(System.Text.StringBuilder builder);

Este método executa as seguintes tarefas:

  1. Para cada um dos membros imprimíveis do struct de registro (campo público não estático e membros da propriedade legível), acrescenta o nome desse membro seguido por "=" seguido pelo valor do membro separado com ", “,
  2. Retornará true se o struct de registro tiver membros imprimíveis.

Para um membro que tenha um tipo de valor, seu valor será convertido em uma representação de cadeia de caracteres.

Se os membros imprimíveis do registro não incluirem uma propriedade legível com um não acessadorreadonlyget , o sintetizado PrintMembers será readonly. Não há nenhum requisito para que os campos do registro sejam readonly para que o PrintMembers método seja readonly.

O PrintMembers método pode ser declarado explicitamente. No entanto, será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada.

O struct de registro inclui um método sintetizado equivalente ao seguinte:

public override string ToString();

Se o método do struct de PrintMembers registro for readonly, o método sintetizado ToString() será readonly.

Esse método pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada.

Este método executa as seguintes tarefas:

  1. Cria uma StringBuilder instância,
  2. Acrescenta o nome do struct de registro ao construtor, seguido por "{",
  3. Invoca o método do struct de PrintMembers registro dando-lhe o construtor, seguido por " " se ele retornou true,
  4. Acrescenta "}",
  5. Retorna o conteúdo do construtor com builder.ToString().

Exemplo: considere o seguinte struct de registro:

record struct R1(T1 P1, T2 P2);

Para este struct de registro, os membros de impressão sintetizados seriam algo como:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString());
                                 // if P1 has a value type
        builder.Append(", ");

        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2.ToString());
                                 // if P2 has a value type

        return true;
    }

    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

exemplo final

16.4.4 Membros do struct de registro posicional

16.4.4.1 Geral

Além de fornecer aos membros descritos nas subclausas anteriores, os structs de registro posicional (§16.2.1) sintetizam membros adicionais com as mesmas condições que os outros membros, conforme descrito nas subclaus a seguir.

Construtor primário 16.4.4.2

Um struct de registro tem um construtor público cuja assinatura corresponde aos parâmetros de valor da declaração de tipo. Isso é chamado de construtor primário para o tipo. É um erro ter um construtor primário e um construtor com a mesma assinatura já presente no struct. Se a declaração de tipo não incluir um delimited_parameter_list, nenhum construtor primário será gerado.

record struct R1
{
    public R1() { } // OK
}

record struct R2()
{
    public R2() { } // error: 'R2' already defines
                    // a constructor with the same parameter types
}

As declarações de campo de instância para um struct de registro têm permissão para incluir inicializadores de variável. Se não houver nenhum construtor primário, os inicializadores de instância serão executados como parte do construtor sem parâmetros. Caso contrário, em runtime, o construtor primário executará os inicializadores de instância que aparecem no corpo do registro-struct.

Se um struct de registro tiver um construtor primário, qualquer construtor definido pelo usuário deverá ter um inicializador de construtor explícito this que chame o construtor primário ou um construtor declarado explicitamente.

Os parâmetros do construtor primário, bem como os membros do struct de registro, estão no escopo dentro de inicializadores de campos ou propriedades de instância. Os membros da instância seriam um erro nesses locais, mas os parâmetros do construtor primário estariam no escopo e podem ser usados e seriam membros de sombra. Membros estáticos também seriam utilizáveis.

Um aviso será produzido se um parâmetro do construtor primário não for lido.

As regras de atribuição definidas para construtores de instância de struct se aplicam ao construtor primário de structs de registro. Por exemplo, o seguinte é um erro:

record struct Pos(int X) // def assignment error in primary constructor
{
    private int x;
    public int X {
        get { return x; } set { x = value; } 
    } = X;
}

16.4.4.3 Propriedades

Para cada parâmetro de um delimited_parameter_list que tem o mesmo nome e tipo que um campo de instância explicitamente declarado, o restante desse subclauso não se aplica.

Para cada parâmetro de struct de registro de um delimited_parameter_list há um membro de propriedade pública correspondente cujo nome e tipo são retirados da declaração de parâmetro de valor.

Para um struct de registro:

  • uma propriedade pública get e init automática será criada se o struct de registro tiver um readonly modificador eset, get caso contrário. Ambos os tipos de acessadores de conjunto (set e init) são considerados "correspondentes". Portanto, o usuário pode declarar uma propriedade somente init no lugar de uma sintetizada mutável.

  • Uma propriedade herdada abstract com o tipo correspondente é substituída.

  • Nenhuma propriedade automática será criada se o struct de registro tiver um campo de instância com o nome e o tipo esperados.

  • Será um erro se a propriedade herdada não tiver publicget e set/init acessadores.

  • Será um erro se a propriedade ou o campo herdado estiver oculto.

  • A propriedade automática é inicializada para o valor do parâmetro de construtor primário correspondente.

  • Os atributos podem ser aplicados à propriedade automática sintetizada e ao seu campo de backup usando property: ou field: destinos para atributos aplicados sintaticamente ao parâmetro de struct de registro correspondente.

Desconstrução 16.4.4.4

Um struct de registro posicional com pelo menos um parâmetro sintetiza um método de instância de retorno público voidchamado Deconstruct com uma declaração de parâmetro out para cada parâmetro da declaração do construtor primário. Cada parâmetro tem Deconstruct o mesmo tipo que o parâmetro correspondente da declaração do construtor primário. O corpo do método atribui cada parâmetro do método Deconstruct ao valor de um acesso de membro de instância a um membro do mesmo nome. Se os membros da instância acessados no corpo não incluirem uma propriedade com um não acessadorreadonlyget , o método sintetizado Deconstruct será readonly. O método pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada ou for estática.

16.5 Diferenças de classe e struct

16.5.1 Geral

Os structs diferem das classes de várias maneiras importantes:

  • Structs são tipos de valor (§16.5.2).
  • Todos os tipos de struct herdam implicitamente da classe System.ValueType (§16.5.3).
  • A atribuição a uma variável de um tipo de struct cria uma cópia do valor atribuído (§16.5.4).
  • O valor padrão de um struct é o valor produzido definindo todos os campos como seu valor padrão (§16.5.5).
  • Operações de conversão de boxe e de unboxing são usadas para converter entre um tipo de struct e determinados tipos de referência (§16.5.6).
  • O significado é this diferente nos membros do struct (§16.5.7).
  • Um struct não tem permissão para declarar um finalizador.
  • Declarações de evento, declarações de propriedade, acessores de propriedade, declarações de indexador e declarações de método têm permissão para usar o modificador readonly, enquanto isso geralmente não é permitido para esses mesmos tipos de membros em classes.

16.5.2 Semântica de valor

Structs são tipos de valor (§8.3) e são conhecidos por ter semântica de valor. As classes, por outro lado, são tipos de referência (§8.2) e dizem ter semântica de referência.

Uma variável de um tipo struct contém diretamente os dados do struct, enquanto uma variável de um tipo de classe contém uma referência a um objeto que contém os dados. Quando um struct B contém um campo de instância do tipo A e A é um tipo struct, é um erro de tempo de compilação para A depender de B ou de um tipo construído a partir de B. Um struct Xdepende diretamente de um struct Y se X contém um campo de instância do tipo Y. Dada essa definição, o conjunto completo de structs das quais um struct depende é o fechamento transitivo da relação de dependência direta.

Exemplo:

struct Node
{
    int data;
    Node next; // error, Node directly depends on itself
}

é um erro porque Node contém um campo de instância de seu próprio tipo. Outro exemplo

struct A { B b; }
struct B { C c; }
struct C { A a; }

é um erro porque cada um dos tipos A, B, e C dependem um do outro.

exemplo final

Com classes, é possível que duas variáveis façam referência ao mesmo objeto e, portanto, é possível que as operações em uma variável afetem o objeto referenciado pela outra variável. Com structs, cada uma das variáveis tem sua própria cópia dos dados (exceto no caso de parâmetros por referência) e não é possível que as operações em uma afetem a outra. Além disso, exceto quando explicitamente anulável (§8.3.12), não é possível que os valores de um tipo struct sejam null.

Observação: se um struct contiver um campo do tipo de referência, o conteúdo do objeto referenciado poderá ser alterado por outras operações. No entanto, o valor do campo em si, ou seja, qual objeto ele referencia, não pode ser alterado por meio de uma mutação de um valor de struct diferente. nota final

Exemplo: Dado o seguinte

struct Point
{
    public int x, y;

    public Point(int x, int y) 
    {
        this.x = x;
        this.y = y;
    }
}

class A
{
    static void Main()
    {
        Point a = new Point(10, 10);
        Point b = a;
        a.x = 100;
        Console.WriteLine(b.x);
    }
}

A saída é 10. A atribuição de a para b cria uma cópia do valor e b , portanto, não é afetada pela atribuição a a.x. Se Point tivesse sido declarado como uma classe, a saída seria 100 porque a e b fariam referência ao mesmo objeto.

exemplo final

Herança 16.5.3

Todos os tipos de struct herdam implicitamente da classe System.ValueType, que, por sua vez, herda da classe object. Uma declaração struct pode especificar uma lista de interfaces implementadas, mas não é possível que uma declaração struct especifique uma classe base.

Os tipos de struct nunca são abstratos e estão sempre implicitamente selados. Os modificadores abstract e sealed não são, portanto, permitidos em uma declaração de struct.

Como não há suporte para herança para structs, a acessibilidade declarada de um membro struct não pode ser protected, private protectedou protected internal.

Os membros da função em um struct não podem ser abstratos ou virtuais, e o override modificador só tem permissão para substituir métodos herdados de System.ValueType.

Atribuição 16.5.4

A atribuição a uma variável de um tipo de struct cria uma cópia do valor que está sendo atribuído. Isso difere da atribuição a uma variável de um tipo de classe, que copia a referência, mas não o objeto identificado pela referência.

Semelhante a uma atribuição, quando um struct é passado como um parâmetro de valor ou retornado como resultado de um membro da função, uma cópia do struct é criada. Um struct pode ser passado por referência a um membro de uma função usando um parâmetro por referência.

Quando uma propriedade ou indexador de um struct é o destino de uma atribuição, a expressão de instância associada à propriedade ou ao acesso do indexador deve ser classificada como uma variável. Se a expressão de instância for classificada como um valor, ocorrerá um erro em tempo de compilação. Isso é descrito em mais detalhes em §12.24.2.

16.5.5 Valores padrão

Conforme descrito na seção 9.3, vários tipos de variáveis são inicializados automaticamente para seu valor padrão quando são criados. Para variáveis de tipos de classe e outros tipos de referência, esse valor padrão é null. No entanto, como os structs são tipos de valor que não podem ser null, o valor padrão de um struct é o valor produzido pela definição de todos os campos de tipo de valor como seu valor padrão e todos os campos de tipo de referência como null.

Exemplo: Referindo-se à estrutura declarada Point acima, o exemplo

Point[] a = new Point[100];

inicializa cada elemento Point na matriz ao valor produzido ao definir os campos x e y a zero.

exemplo final

O valor padrão de um struct corresponde ao valor retornado pelo construtor padrão do struct (§8.3.3). Quando um struct não declara um construtor de instância sem parâmetro explícito, o construtor padrão é sintetizado e sempre retorna o valor resultante da definição de todos os campos para seus valores padrão. A default expressão sempre produz o valor padrão inicializado zero, mesmo quando um struct declara um construtor de instância sem parâmetros explícito (§16.4.9).

Observação: os structs devem ser projetados para considerar o estado de inicialização padrão um estado válido. No exemplo

struct KeyValuePair
{
    string key;
    string value;

    public KeyValuePair(string key, string value)
    {
        if (key == null || value == null)
        {
            throw new ArgumentException();
        }

        this.key = key;
        this.value = value;
    }
}

O construtor de instância definido pelo usuário protege contra null valores somente onde ele é chamado explicitamente. Nos casos em que uma KeyValuePair variável está sujeita à inicialização do valor padrão, os campos key e value serão null, e a estrutura precisa estar preparada para lidar com esse estado.

nota final

16.5.6 Boxe e unboxing

Um valor de um tipo de classe pode ser convertido em tipo object ou em um tipo de interface implementado pela classe simplesmente tratando a referência como outro tipo em tempo de compilação. Da mesma forma, um valor de type object ou um valor de um tipo de interface pode ser convertido de volta em um tipo de classe sem alterar a referência (mas, é claro, uma verificação de tipo de tempo de execução é necessária nesse caso).

Como os structs não são tipos de referência, essas operações são implementadas de forma diferente para os tipos de struct. Quando um valor de um tipo struct é convertido em determinados tipos de referência (conforme definido em §10.2.9), ocorre uma operação de boxing. Da mesma forma, quando um valor de certos tipos de referência (conforme definido em §10.3.7) é convertido de volta para um tipo struct, ocorre uma operação de unboxing. Uma diferença importante em relação às mesmas operações em tipos de classe é que as operações de boxing e unboxing copiam o valor struct para dentro ou para fora da instância de boxing.

Observação: Assim, após uma operação de encaixotamento ou desembrulho, as alterações feitas no objeto desembrulhado struct não são refletidas no objeto encaixotado struct. nota final

Para obter mais detalhes sobre boxing e unboxing, consulte §10.2.9 e §10.3.7.

16.5.7 Significado disso

O significado de this em uma estrutura difere do significado de this em uma classe, conforme descrito em §12.8.14. Quando um tipo struct substitui um método virtual herdado de System.ValueType (como Equals, GetHashCode ou ToString), a invocação do método virtual através de uma instância do tipo struct não faz com que ocorra boxing. Isso é verdadeiro mesmo quando o struct é usado como um parâmetro de tipo e a invocação ocorre por meio de uma instância do tipo de tipo de parâmetro.

Exemplo:

struct Counter
{
    int value;
    public override string ToString() 
    {
        value++;
        return value.ToString();
    }
}

class Program
{
    static void Test<T>() where T : new()
    {
        T x = new T();
        Console.WriteLine(x.ToString());
        Console.WriteLine(x.ToString());
        Console.WriteLine(x.ToString());
    }

    static void Main() => Test<Counter>();
}

A saída do programa é:

1
2
3

Embora seja um estilo ruim para ToString ter efeitos colaterais, o exemplo demonstra que nenhum boxing ocorreu para as três invocações de x.ToString().

exemplo final

Da mesma forma, o boxing nunca ocorre implicitamente ao acessar um membro em um parâmetro de tipo restrito quando o membro é implementado dentro de um tipo valor. Por exemplo, suponha que uma interface ICounter contenha um método Increment, que pode ser usado para modificar um valor. Se ICounter for usado como uma restrição, a implementação do método Increment é chamado com uma referência à variável em que Increment foi chamado, nunca uma cópia de boxing.

Exemplo:

interface ICounter
{
    void Increment();
}

struct Counter : ICounter
{
    int value;

    public override string ToString() => value.ToString();

    void ICounter.Increment() => value++;
}

class Program
{
    static void Test<T>() where T : ICounter, new()
    {
        T x = new T();
        Console.WriteLine(x);
        x.Increment();              // Modify x
        Console.WriteLine(x);
        ((ICounter)x).Increment();  // Modify boxed copy of x
        Console.WriteLine(x);
    }

    static void Main() => Test<Counter>();
}

A primeira chamada para Increment modifica o valor na variável x. Isso não é equivalente à segunda chamada para Increment, que modifica o valor em uma cópia de boxing de x. Assim, a saída do programa é:

0
1
1

exemplo final

16.5.8 Inicializadores de campo

Conforme descrito em §16.5.5, o valor padrão de um struct consiste no valor resultante da definição de todos os campos de tipo de valor para o valor padrão e todos os campos de tipo de referência para null. Campos estáticos e de instância de um struct têm permissão para incluir inicializadores variáveis; no entanto, no caso de um inicializador de campo de instância, pelo menos um construtor de instância também deve ser declarado ou, para um struct de registro, um delimited_parameter_list deverá estar presente.

Exemplo:

Console.WriteLine($"Point is {new Point()}");

struct Point
{
    public int x = 1;
    public int y = 1;

    public Point() { }

    public override string ToString()
    {
        return "(" + x + ", " + y + ")";
    }
}
Point is (1, 1)

exemplo final

Quando um construtor de instância de struct não tem nenhum inicializador de construtor, esse construtor executa implicitamente as inicializações especificadas pelos variable_initializerdos campos de instância declarados em seu struct. Isso corresponde a uma sequência de atribuições que são executadas imediatamente após a entrada no construtor.

Quando um construtor de instância de struct tem um this() inicializador de construtor que representa o construtor sem parâmetros padrão, o construtor declarado limpa implicitamente todos os campos de instância e executa as inicializações especificadas pelos variable_initializerdos campos de instância declarados em seu struct. Imediatamente após a entrada no construtor, todos os campos de tipo de valor são definidos como seu valor padrão e todos os campos de tipo de referência são definidos como null. Imediatamente após isso, uma sequência de atribuições correspondentes aos variable_initializersão executadas.

Um field_declaration declarado diretamente dentro de uma struct_declaration que possui o struct_modifierreadonly deve ter o field_modifierreadonly.

16.5.9 Construtores

Um struct pode declarar construtores de instância, com zero ou mais parâmetros. Se um struct não tiver um construtor de instância explicitamente declarado sem parâmetros, um será sintetizado, com acessibilidade pública, que sempre retornará o valor resultante da definição de todos os campos de tipo de valor para o valor padrão e todos os campos de tipo de referência para null (§8.3.3). Nesse caso, os inicializadores de campo de instância são ignorados quando esse construtor é executado.

Um construtor de instância sem parâmetro explicitamente declarado deve ter acessibilidade pública.

Exemplo: Dado o seguinte:

using System;
struct Point
{
    int x = -1, y = -2;

    public Point(int x, int y) 
    {
        this.x = x;
        this.y = y;
    }

    public override string ToString()
    {
        return "(" + x + ", " + y + ")";
    }
}

class A
{
    static void Main()
    {
        Console.WriteLine($"Point is {new Point()}");
        Console.WriteLine($"Point is {new Point(0,0)}");
    }
}
Point is (0, 0)
Point is (0, 0)

as instruções criam um Point com x e y inicializado como zero, que no caso da chamada para o construtor de instância sem parâmetros, pode ser surpreendente, pois ambos os campos de instância têm inicializadores, mas não são executados.

exemplo final

Um construtor de instância struct não tem permissão para incluir um inicializador de construtor do formulário base(argument_list), em que argument_list é opcional. A execução de um construtor de instância não deve resultar na execução de um construtor no tipo System.ValueTypebase do struct.

O this parâmetro de um construtor de instância struct corresponde a um parâmetro de saída do tipo struct. Portanto, this deve ser definitivamente atribuído (§9.4) em todos os locais onde o construtor retorna. Da mesma forma, ele não pode ser lido (mesmo implicitamente) no corpo do construtor antes de ser definitivamente atribuído.

Se o construtor de instância struct especificar um inicializador de construtor, esse inicializador será considerado uma atribuição definitiva a isso que ocorre antes do corpo do construtor. Portanto, o corpo em si não tem requisitos de inicialização.

Campos de instância (exceto fixed campos) devem ser definitivamente atribuídos em construtores de instância de struct que não têm um this() inicializador.

Exemplo: considere a implementação do construtor de instância abaixo:

struct Point
{
    int x, y;

    public int X
    {
        set { x = value; }
    }

    public int Y 
    {
        set { y = value; }
    }

    public Point(int x, int y) 
    {
        X = x; // error, this is not yet definitely assigned
        Y = y; // error, this is not yet definitely assigned
    }
}

Nenhum membro de função de instância (incluindo os acessadores definidos para as propriedades X e Y) pode ser chamado até que todos os campos do struct que está sendo construído tenham sido definitivamente atribuídos. Observe, no entanto, que se Point fosse uma classe em vez de um struct, a implementação do construtor de instância seria permitida. Há uma exceção a isso, e isso envolve propriedades implementadas automaticamente (§15.7.4). As regras de atribuição definidas (§12.24.2) isentam especificamente a atribuição a uma propriedade automática de um tipo de struct dentro de um construtor de instância desse tipo de struct: essa atribuição é considerada uma atribuição definitiva do campo de backup oculto da propriedade automática. Assim, é permitido:

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

    public Point(int x, int y)
    {
        X = x; // allowed, definitely assigns backing field
        Y = y; // allowed, definitely assigns backing field
   }
}

fim do exemplo]

16.5.10 Construtores estáticos

Os construtores estáticos para structs seguem a maioria das mesmas regras que para classes. A execução de um construtor estático para um tipo de struct é disparada pelo primeiro dos seguintes eventos a ocorrer em um domínio de aplicativo:

  • Um membro estático do tipo struct é referenciado.
  • Um construtor do tipo struct declarado explicitamente é chamado.

Observação: a criação de valores padrão (§16.5.5) de tipos de struct não dispara o construtor estático. (Um exemplo disso é o valor inicial dos elementos em uma matriz.) nota final

16.5.11 Propriedades

Um property_declaration (§15.7.1) para uma propriedade de instância em um struct_declaration pode conter o property_modifierreadonly. No entanto, uma propriedade estática não deve conter esse modificador.

É um erro de tempo de compilação tentar modificar o estado de uma variável de struct de instância por meio de uma propriedade readonly declarada nesse struct.

É um erro de tempo de compilação para uma propriedade implementada automaticamente com um readonly modificador, para também ter um set acessador.

É um erro de tempo de compilação para uma propriedade implementada automaticamente em um readonly struct ter um set acessador.

Uma propriedade implementada automaticamente declarada dentro de um readonly struct não precisa ter um readonly modificador, pois seu get acessador é implicitamente considerado como leitura exclusiva.

É um erro de tempo de compilação ter um readonly modificador na própria propriedade, assim como em qualquer um de seus acessadores get e set.

É um erro de tempo de compilação para uma propriedade ter um modificador somente leitura em todos os seus acessadores.

Observação: para corrigir o erro, mova o modificador dos acessadores para a própria propriedade. nota final

Para uma expressão de acessador de propriedade: s.P

  • Será um erro em tempo de compilação se s.P invocar o acessador M definido do tipo T quando o processo em §12.6.6.1 criar uma cópia temporária de s.
  • Se s.P invocar o acessador get do tipo T, o processo em §12.6.6.1 será seguido, incluindo a criação de uma cópia temporária, s se necessário.

As propriedades implementadas automaticamente (§15.7.4) usam campos de suporte ocultos, que só podem ser acessados pelos acessadores de propriedade.

Observação: essa restrição de acesso significa que os construtores em structs que contêm propriedades implementadas automaticamente geralmente precisam de um inicializador de construtor explícito onde, de outra forma, não precisariam de um, para satisfazer o requisito de todos os campos serem definitivamente atribuídos antes que qualquer membro da função seja invocado ou o construtor retorne. nota final

Métodos 16.5.12

Um method_declaration (§15.6.1) para um método de instância em um struct_declaration pode conter o method_modifierreadonly. No entanto, um método estático não deve conter esse modificador.

É um erro de compilação tentar modificar o estado de uma variável de estrutura de instância por meio de um método de somente leitura declarado nessa estrutura.

Embora um método de somente leitura possa chamar um método irmão, que não é de somente leitura, ou acessador de propriedade ou indexador, isso resulta na criação de uma cópia implícita de this como uma medida de segurança.

Um método readonly pode chamar uma propriedade irmão ou acessador de conjunto de indexadores que é somente leitura. Se o acessador de um membro irmão não for explicitamente ou implicitamente lido, ocorrerá um erro de compilação.

Todas as declarações de métodos de um método parcial devem ter um readonly modificador, ou nenhuma delas deve ter.

16.5.13 Indexadores

Uma indexer_declaration (§15.9) para um indexador de instância dentro de uma struct_declaration pode conter o indexer_modifierreadonly.

É um erro de tempo de compilação tentar modificar o estado de uma variável de struct de instância por meio de um indexador readonly declarado nesse struct.

É um erro de tempo de compilação ter um modificador readonly no próprio indexador, bem como em qualquer um de seus get ou set acessadores.

É um erro de tempo de compilação para um indexador ter um modificador somente leitura em todos os seus acessadores.

Observação: para corrigir o erro, mova o modificador dos acessadores para o próprio indexador. nota final

16.5.14 Eventos

Um event_declaration (§15.8.1) para um evento de instância que não é semelhante a um campo em uma struct_declaration pode conter o event_modifierreadonly. No entanto, um evento estático não deve conter esse modificador.

16.5.15 Restrição de contexto seguro

16.5.15.1 Geral

Em tempo de compilação, cada expressão é associada a um contexto em que essa instância e todos os seus campos podem ser acessados com segurança, seu contexto seguro. O contexto seguro é um contexto, que envolve uma expressão, para onde é seguro que o valor escape.

Qualquer expressão cujo tipo de tempo de compilação não seja um struct ref tem um contexto seguro do contexto do chamador.

Uma expressão default, de qualquer tipo, possui um contexto seguro proveniente do contexto do chamador.

Para qualquer expressão não padrão cujo tipo de tempo de compilação é um ref, struct tem um contexto seguro definido pelas seções a seguir.

O contexto seguro registra em qual contexto um valor pode ser copiado. Dada uma atribuição de uma expressão E1 com um contexto seguro S1, para uma expressão E2 com um contexto seguro S2, é um erro se S2 for um contexto mais amplo que S1.

Há três valores diferentes de contexto seguro, os mesmos que os valores ref-safe-context definidos para variáveis de referência (§9.7.2): declaration-block, function-member e caller-context. O contexto seguro de uma expressão restringe seu uso da seguinte maneira:

  • Para uma instrução de retorno return e1, o contexto seguro de e1 deve ser o contexto do chamador.
  • Para uma atribuição e1 = e2 , o contexto seguro de e2 deve ser pelo menos tão amplo quanto o contexto seguro de e1.

Para uma invocação de método, se houver um argumento ref ou out de um tipo ref struct (incluindo o receptor, a menos que o tipo seja readonly), com contexto seguro S1, nenhum argumento (incluindo o receptor) poderá ter um contexto seguro mais restrito do que S1.

16.5.15.2 Contexto seguro do parâmetro

Um parâmetro de um tipo ref struct, incluindo o parâmetro this de um método de instância, possui um contexto seguro do contexto do chamador.

16.5.15.3 Contexto seguro de variável local

Uma variável local de um tipo ref struct tem um contexto seguro da seguinte maneira:

  • Se a variável for uma variável de iteração de um foreach loop, o contexto seguro da variável será o mesmo que o contexto seguro da expressão do foreach loop.
  • Caso contrário, se a declaração da variável tiver um inicializador, o contexto seguro da variável será o mesmo que o contexto seguro desse inicializador.
  • Caso contrário, a variável não é inicializada no momento da declaração e tem um contexto seguro de contexto do chamador.

16.5.15.4 Contexto seguro de campo

Uma referência a um campo e.F, onde o tipo de F é um tipo de struct ref, tem um contexto seguro que é o mesmo que o contexto seguro de e.

Operadores 16.5.15.5

A aplicação de um operador definido pelo usuário é tratada como uma invocação de método (§16.5.15.6).

Para um operador que produz um valor, como e1 + e2 ou c ? e1 : e2, o contexto seguro do resultado é o contexto mais estreito entre os contextos seguros dos operandos do operador. Como consequência, para um operador unário que produz um valor, como +e, o contexto seguro do resultado é o contexto seguro do operando.

Observação: o primeiro operando de um operador condicional é um bool, portanto, seu contexto seguro é caller-context. Segue-se que o contexto seguro resultante é o contexto seguro mais restrito do segundo e do terceiro operando. nota final

16.5.15.6 Método e invocação de propriedade

Um valor resultante de uma invocação de método e1.M(e2, ...) ou de propriedade e.P, tem um contexto seguro do menor dos seguintes contextos:

  • contexto do chamador.
  • O contexto seguro de todas as expressões de argumento (incluindo o receptor).

Uma invocação de propriedade (ou get ou set) é tratada, pelas regras acima, como uma invocação do método subjacente.

16.5.15.7 stackalloc

O resultado de uma expressão stackalloc tem o contexto seguro de membro da função.

Invocações do construtor 16.5.15.8

Uma new expressão que invoca um construtor obedece às mesmas regras que uma invocação de método que é considerada para retornar o tipo que está sendo construído.

Além disso, o contexto seguro é o menor dos contextos seguros de todos os argumentos e operandos de todas as expressões de inicialização de objetos, recursivamente, se houver algum inicializador presente.

Observação: essas regras dependem de Span<T> não ter um construtor da seguinte forma:

public Span<T>(ref T p)

Esse construtor torna as instâncias de Span<T> usadas como campos indistinguíveis de um campo ref. As regras de segurança descritas neste documento dependem de ref campos não sendo uma construção válida em C# ou .NET. nota final