Esse é o 20º post da série sobre C# 7, e o sexto sobre C# 7.2. Pra acompanhar a série você pode seguir a tag C#7 no blog ou voltar no post que agrega a série.
Lembrando que para utilizar as versões minor do C# (como a 7.1, ou 7.2) você precisa habilitá-la nos atributos do projeto. Veja neste post como fazê-lo e também como habilitar na solution inteira pra não ter que ficar configurando cada projeto individualmente.
Novidades do C# 7.2: readonly struct
A essa altura você já sabe como funcionam os parâmetros in e os retornos com ref readonly e como eles funcionam melhor com struct
do que com class
. No entanto, conforme mencionado no post sobre os parâmetros in
, sempre que um membro da struct
é acessado é feita uma cópia, para evitar efeitos colaterais. E há um custo pra criar essa cópia. A forma de resolver isso é utilizar uma estrutura imutável, algo que surgiu no C# 7.2. Ao utilizar uma readonly struct
não existirá copia do objeto ao acessar um de seus membros. Essa estrutura imutável se torna especialmente necessária porque o uso de variáveis readonly
crescerá com o uso do in
e do retorno com ref readonly
.
Para criar uma estrutura imutável basta acrescentar readonly
à struct
, simples assim. Todos os campos de uma estrutura imutável também devem ser imutáveis, o que garante a imutabilidade da estrutura como um todo. O compilador vai garantir essa regra, e esquecer de colocar readonly
em um campo, ou criar uma propriedade com set
vai gerar um erro de compilação.
Abaixo você vê um exemplo de uma estrutura imutável que representa um ponto. Ele possui uma referência para o ponto da origem, em 0,0
, que é retornada por referência atravéz do método Origem
, e um método estático de soma, que utiza o Ponto
como parâmetro in
.
readonly struct Ponto { public Ponto(float x, float y) { X = x; Y = y; } public float X { get; } public float Y { get; } private readonly static Ponto origem = new Ponto(); public static ref readonly Ponto Origem => ref origem; public static Ponto Soma(in Ponto p1, in Ponto p2) => new Ponto(p1.X + p2.X, p1.Y + p2.Y); }
Sempre que os valores das propriedades X
e Y
forem acessados no método Soma
, o acesso será direto ao objeto que foi passado como argumento, não a uma cópia, resolvendo o problema de desempenho que mencionei antes.
Observando a IL
Nesta seção vamos observar como a cópia é feita e como pode ser evitada. Se você não quer entender esse mecanismo por dentro, pode pular para o próximo ponto.
Temos abaixo uma classe que tem um Ponto
, e que o retorna este Ponto
como uma referência imutável no método Obter
. Estamos usando o Ponto
da seção anterior, uma readonly struct
.
class TemUmPonto { private static readonly Ponto p = new Ponto(); public static ref readonly Ponto Obter() => ref p; }
Abaixo vemos uma chamada deste método, e o resultado (o Ponto
obtido) é colocado na variável p
, e na linha seguinte buscamos o valor da propriedade X
:
ref readonly var p = ref Obter(); var x = p.X;
O acesso a essa propriedade X
, se a a estrutura é imutável não gera uma cópia. Veja como fica a IL:
IL_0001: call valuetype ConsoleApp2.Ponto& modreq ([System.Runtime.InteropServices]System.Runtime.InteropServices.InAttribute) ConsoleApp2.TemUmPonto::Obter() IL_0006: stloc.0 // p IL_0007: ldloc.0 // p IL_0008: call instance float32 ConsoleApp2.Ponto::get_X() IL_000d: stloc.1 // x
Explicando linha a linha:
IL_0001
: é a chamada do método, que, ao retornar, coloca a referência doPonto
obtido na stack;IL_0006
: é o armazenamento do valor da stack (a referência para oPonto
) na variável de nomep
, que está na posição 0 (por isso você vê.0
no final da instrução);IL_0007
: já acontece na segunda linha código C#, e já está caminhando para obter o valor da propriedade, ela coloca o valor da variávelp
(uma referência aoPonto
) de volta na stack;IL_0008
: invoca o métodoget_X
, que é o getter da propriedade, utilizando a referência doPonto
que está na stack e coloca o valor encontrado emX
na stack;IL_000d
: pega o valor da stack (o retorno do acesso à propriedade) e coloca na variávelx
(é a segunda variável local, note o.1
no final).
Como você pode notar, não há cópia em local algum, a chamada do método acontece diretamente sobre a estrutura de dados que está na variável p. Note também que a IL anotada nos ajuda a entender os nomes das variáveis.
Abaixo você vê o mesmo código sem o modificador readonly
na estrutura Ponto
. Essa foi a única alteração realizada. Note as diferenças:
IL_0001: call valuetype ConsoleApp2.Ponto& modreq ([System.Runtime.InteropServices]System.Runtime.InteropServices.InAttribute) ConsoleApp2.TemUmPonto::Obter() IL_0006: stloc.0 // p IL_0007: ldloc.0 // p IL_0008: ldobj ConsoleApp2.Ponto IL_000d: stloc.2 // V_2 IL_000e: ldloca.s V_2 IL_0010: call instance float32 ConsoleApp2.Ponto::get_X() IL_0015: stloc.1 // x
Explicarei agora apenas os pontos que mudaram.
Logo depois da terceira instrução temos a IL_0008
com um ldobj
. Essa intrução pega a referência ao Ponto
, que está no topo da stack, segue a referência e coloca o valor na stack. Note que o valor no topo da stack agora não é mais uma referência ao valor do Ponto
, mas o valor do Ponto
em si mesmo, os dados do ponto (os valores de X
e Y
). Estes dados foram copiados para a stack.
Nesse momento, a instrução da linha IL_000d
chama stloc.2
, ou seja, guarda o valor do topo da stack (o valor do Ponto
) na terceira variável local, no caso V_2
(veja o comentário na IL), que é uma variável local introduzida pelo compilador e que não existe no C#.
Em seguida, na instrução IL_000e
com o ldloca.s V_2
é obtido o endereço do valor de V_2
. Este endereço é colocadado na stack e depois utilizado na chamada de get_X
na linha seguinte. Ou seja, no lugar de p
, usamos esta cópia.
Caso você queira se aprofundar um pouco mais este post do Jon Skeet é uma boa referência para explicar o problema da cópia com campos readonly
(essencialmente o mesmo problema).
Conclusão
Podemos notar, analisando a IL, que temos:
- uma cópia de dados do valor original da
struct
; - a subsequente movimentação do dado da stack para uma variável.
Essas duas operações terão custo, que pode ser evitado simplesmente utilizando uma estrutura imutável.
Além disso, a readonly struct
é a unica forma de termos imutabilidade garantida pelo compilador. Até então, um objeto imutável não tinha garantia alguma de que seria imutável, fora o cuidado da pessoa desenvolvendo sua class
ou struct
. Com isso agora temos a garantia do compilador nos auxiliando nesta tarefa.
Você consegue ler um pouco mais sobre este assunto nos docs sobre semântica de referência com tipos de valor.
E você acha no Github a proposta da especificação inicial.
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.