Estrutura vs estrutura somente leitura vs estrutura de referência vs estrutura de registro

Imagem de capa Estrutura vs estrutura somente leitura vs estrutura de referência vs estrutura de registro

Estrutura vs estrutura somente leitura vs estrutura de referência vs estrutura de registro

C# conhece vários tipos de declarações de estrutura "tipos". Neste artigo vou mostrar quais são as diferenças entre um estrutura, estrutura somente leitura e estrutura de referência . Além disso, quais são as limitações entre esses tipos.

estrutura

Todo mundo usou isso de uma forma ou de outra. Uma estrutura é um tipo de valor, que idealmente deve representar um único valor. Exemplos proeminentes seriam DateTime, int, double ou TimeSpan, mas também coisas como um Point2D que consistiria em uma coordenada x e y seria um exemplo válido. Na documentação da Microsoft, encontraremos uma dica de quando e quando não usar estrutura:

"Evite... Não terá de ser encaixotado com frequência."

A última parte é importante e é para onde muito deste artigo irá. Não devemos encaixotar nossa estrutura com frequência. Normalmente, os tipos de valor, como uma estrutura, vivem na pilha. A pilha é uma boa maneira de modelar o tempo de elevação de uma variável. Em contraste com isso, temos o heap.

Importante entender é que normalmente uma estrutura ou qualquer tipo de valor viverá nessa pilha. Então, se você declarar um int em uma função, ele vive exatamente nesse escopo e isso é tudo:

public  void  MyFunc()
{
    int a = 2;
}

a existe apenas dentro de MyFunc e, portanto, apenas na pilha. Mas se usarmos qualquer tipo de valor, por exemplo, como um campo em uma classe, essa variável deve residir no heap, pois o escopo pai / tempo de vida vive no escopo. estrutura permitem muitas coisas que uma classe permitiria com algumas diferenças importantes:

  • estrutura não pode herdar de outras classes ou estrutura (interfaces são possíveis)
  • estrutura não pode ter um destruidor
  • ...

Um exemplo de estrutura:

public  struct Color
{
        public  byte Red;
        public  byte Green;
        public  byte Blue;

        public Color() { Red = 0; Green = 0; Blue = 0; }

        public  void  ShiftToGrayscale()
        {
                var avg = (byte)((Red + Green + Blue) / 3);
                Red = Green = Blue = avg;
        }
}

estrutura somente leitura

A estrutura readonly foi introduzida no C# 7.2. A ideia básica é esclarecer seu estrutura como imutável, portanto, só é permitido ler dados, mas não alterar nenhum estado. Claro que você pode fazer isso sem o modificador readonly (como DateTime existe desde o início do próprio framework), mas o compilador irá ajudá-lo quando você violar essa maneira readonly.

Nosso exemplo Color acima lançará vários erros de compilador quando declaramos o estrutura como somente leitura sem adotar nenhum código: estrutura pública somente leitura Color fornecerá algo assim:

Erro de compilação (linha 16, coluna 14): Campos de instância de estrutura somente leitura devem ser somente leitura. Erro de compilação (linha 17, coluna 14): Campos de instância de estruturas somente leitura devem ser somente leitura. Erro de compilação (linha 18, coluna 14): Os campos de instância de estruturas somente leitura devem ser somente leitura. Erro de compilação (linha 25, col 3): não é possível atribuir a 'Red' porque é somente leitura Erro de compilação (linha 25, coluna 9): Não é possível atribuir a 'Verde' porque é somente leitura Erro de compilação (linha 25, col 17): Não é possível atribuir a 'Blue' porque é somente leitura

A razão é simples: primeiro temos que declarar todos os campos como somente leitura. Isso faz sentido, caso contrário, qualquer chamador poderia apenas alterar as cores. Mas isso não é tudo: o método ShiftToGrayscale ainda está alterando o estado interno, portanto, para obter a imutabilidade, você retornaria uma nova cor com os valores fornecidos:

public  readonly  struct Color
{
        public  readonly  byte Red;
        public  readonly  byte Green;
        public  readonly  byte Blue;

        public Color() { Red = 0; Green = 0; Blue = 0; }

        public  Color(byte red, byte green, byte blue)
        {
                Red = red; Green = green; Blue = blue;
        }

        public Color ShiftToGrayscale()
        {
               var avg = (byte)((Red + Green + Blue) / 3);
               return  new Color (avg, avg, avg);
        }
}

Esse é um grande passo e a imutabilidade traz muitas vantagens:

  • Maior testabilidade
  • Segurança do fio.
  • Atomicidade do fracasso.
  • Ausência de efeitos colaterais ocultos

Conclusão: O compilador tenta ajudá-lo com a imutabilidade. Mas: Também não pode garantir isso. Se você tiver listas ou classes mutáveis como o exemplo a seguir, ainda poderá modificá-las.

public  readonly  struct MyStruct
{  
        public  readonly System.Collections.Generic.List<object> myStuff = new System.Collections.Generic.List<object>() {1, 2};

        public  MyStruct()
        {
        }

        public  void  Mutate()
        {
                 myStuff[0] = 3;
        }
 }

estrutura de referência

Também com o C# 7.2, a Microsoft introduziu o estrutura de referência. A linha inferior é: as estruturas de referência são garantidas para viver na pilha e nunca podem escapar para o heap. Com isso, você pode obter cenários de baixa alocação em que não exerce pressão extra sobre o coletor de lixo. Agora quem garante isso? O compilador faz isso para você. Você pode perguntar como ele faz isso? E a resposta é simples: ele vai proibir todos os cenários em que um tipo de valor é movido para o heap gerenciado:

  • Uma estrutura de referência não pode ser o tipo de elemento de uma matriz. Isso significa que nenhuma lista, nenhuma myrefStructs = new MyRefStructs[] e também não: myrefStructs = stackalloc MyRefStructs[10];
  • Uma estrutura de referência não pode ser um tipo declarado de um campo de uma classe ou uma estrutura não-ref. O motivo é simples: o compilador não pode garantir que sua estrutura não vá para o heap por todos os motivos explicados acima. Como uma classe sempre vive no heap fica claro que é proibido tê-la ali como um campo.
  • Uma variável estrutura de referência não pode ser capturada por uma expressão lambda ou uma função local. A expressão Lamda, bem como as funções locais, estão fora do escopo pai e, portanto, teriam que ser encaixotadas
  • Uma variável estrutura de referência não pode ser usada em um método assíncrono. A razão é como o compilador C# gera código quando encontra uma instrução assíncrona ou melhor, um await . Basicamente, ele constrói uma máquina de estado, que em si é uma classe.
  • Uma variável estrutura de referência não pode ser usada em iteradores. O mesmo motivo que assíncrono: criamos uma máquina de estado que estenderia o escopo/vida útil e, portanto, a estrutura referência terminaria no heap.
  • Uma estrutura de referência não pode implementar interfaces Bem, eles não podem fazer isso porque isso significaria que há uma chance de que eles sejam encaixotados. A única exceção é IDisposable para limpar alguns recursos.

O uso é muito restrito, mas ainda tem seus usos. Um dos exemplos mais proeminentes é Span introduzido no .NET Core 2.1. Ele permite cenários de alocação zero. E sim Span é internamente uma estrutura de referência.

Bônus: estrutura de referência somente leitura

Ambos os modificadores também podem ser colocados juntos. A semântica significaria que a estrutura deve ser imutável e, graças à palavra-chave ref, só é permitido viver na pilha.

estrutura de registro

O C# 10 os apresentou. E muito parecido com o registro que foi introduzido no C# 9.0 e "aprimora" a classe, a estrutura de registro aprimora uma estrutura. É um bom açúcar sintático do compilador para fornecer implementações "pré-definidas" de Equals, ToString() e GetHashcode

Você pode definir uma estrutura super fácil assim:

public  record  struct  Color(byte Red, byte Green, byte Blue)
{
    public Color ShiftToGrayscale()
        {
                var avg = (byte)((Red + Green + Blue) / 3);
                return  new Color (avg, avg, avg);
        }
}

Agora onde está o ToString() e outros métodos? Tudo isso faz o compilador para você em segundo plano. O resultado será algo assim:

public  struct Color : IEquatable<Color>
{
    public  byte Red{ ... }

    public  byte Green { ... }

    public  byte Blue { ... }

    public  Color(byte Red, byte Green, byte Blue)
    {
        ....
    }

    public Color ShiftToGrayscale()
    {
       byte num = (byte)((Red + Green + Blue) / 3);
       return  new Color(num, num, num);
    }

    [IsReadOnly]
    [CompilerGenerated]
    public  override  string  ToString()
    {
        ....
    }

    [IsReadOnly]
    [CompilerGenerated]
    private bool PrintMembers(StringBuilder builder)
    {
        ....
    }

    [CompilerGenerated]
    public  static  bool  operator !=(Color left, Color right)
    {
       ...
    }

    [CompilerGenerated]
    public  static  bool  operator ==(Color left, Color right)
    {
        ...
    }

    [IsReadOnly]
    [CompilerGenerated]
    public  override  int  GetHashCode()
    {
        ...
    }

    [IsReadOnly]
    [CompilerGenerated]
    public  override  bool  Equals(object obj)
    {
        ...
    }

    [IsReadOnly]
    [CompilerGenerated]
    public  bool  Equals(Color other)
    {
        if (EqualityComparer<byte>.Default.Equals(<Red>k__BackingField, other.<Red>k__BackingField) && EqualityComparer<byte>.Default.Equals(<Green>k__BackingField, other.<Green>k__BackingField))
        {
            return EqualityComparer<byte>.Default.Equals(<Blue>k__BackingField, other.<Blue>k__BackingField);
         }
         return  false;
    }

    [IsReadOnly]
    [CompilerGenerated]
    public void Deconstruct(out byte Red, out byte Green, out byte Blue)
    {
       Red = this.Red;
       Green = this.Green;
       Blue = this.Blue;
    }
 }          

A primeira coisa importante é que o registro implemente automaticamente IEquatable. Para a implementação completa, dê uma olhada no código gerado no Sharplab.io. Um dos melhores sites para entender o que seu código e açúcar sintático realmente fazem! Hoje em dia, mais e mais desenvolvedores estão usando estruturas de registro e registro, pois são super convenientes de usar.

Bônus: estrutura de registro somente leitura

Claro que você também pode aplicar a palavra-chave readonly a uma estrutura de registro, ela fará o mesmo que uma estrutura "normal", pois um registro é traduzido para um normal, o que significa que o compilador aplicará certas restrições para impor mais imutabilidade.

Bônus estrutura de registro referência somente leitura

Este não existe. Também estrutura de registro de referência para esse assunto. E a razão é muito simples: estrutura de registro implementa IEquatable e vimos anteriormente que estruturas de referência não têm permissão para implementar nenhuma interface exceto IDisposable.

Conclusão

Espero poder fornecer uma visão geral das diferentes palavras-chave usadas em combinação com struct, o que elas fazem e onde usá-las (ou melhor ainda, quando evitá-las).

Recursos