Esse é o 19º post da série sobre C# 7, e o quinto 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: semânticas de referência com tipos de valor

O C# 7.2 tem uma série de novidades que têm como principal foco a melhoria de desempenho. Não são construções que a maioria dos desenvolvedores utilizarão no dia a dia, mas que permitem que o .NET ganhe desempenho. O foco todo está em aliar programação com memória segura (memory safe) enquanto ganhamos mais efetividade na hora de passar valores, sem gerar mais garbage collection, e sem realizar cópia de valores de memória.

Retornando valores imutáveis de funções

No C# 7.0 ganhamos a habilidade de retornar dados de funções por referência. No entanto, o dado retornado poderia ser alterado de maneira indevida.

Imagine que você tem um vetor, com valores x, y, e z, e quer manter a origem de forma estática no programa. Você cria uma struct e deixa-a disponível em um campo estático. No entanto, qual a garantia que tem que os dados da origem não serão alterados, ou que ela mesma não teria seu valor substituído, o que causaria toda sorte de problemas?

Por exemplo:

private static Vector origin = new Vector(0, 0, 0);
public ref Vector FetchOrigin() => ref origin;
public void M()
{
    ref var o = ref FetchOrigin();
    o.X = 3;
    WriteLine(origin.X); // 3
}

Nesse caso, ganhamos o desempenho esperado, ou seja, não fazemos a cópia do valor da origem, mas inadvertidamente alteramos seu valor.

Pra impedir isso, podíamos retornar por valor, em vez de retornar por referência, e pagar a conta na forma de desempenho sacrificado por causa da cópia dos valores da origem no retorno do método. Não era possível ter desempenho e segurança ao mesmo tempo até o C# 7.2 neste caso específico.

Para solucionar esse problema agora é possível retornar um valor imutável por referência utilizando as palavras chave ref readonly (nesta ordem obrigatóriamente). Elas devem ser usadas tanto como modificadores do tipo de retorno do método quanto do tipo da variável que vai receber o retorno do método.

O exemplo acima, reescrito para ser seguro e ter desempenho fica assim:

private readonly Vector origin = new Vector(0, 0, 0);
public ref readonly Vector FetchOrigin() => ref origin;
public void M()
{
    ref readonly var o = ref FetchOrigin();
    o = new Vector(); // erro, não compila: CS0131 The left-hand side of an assignment must be a variable, property or indexer
    o.X = 3; // mesmo erro do anterior
}

Neste caso não conseguimos nem alterar o valor de o, nem atribuir valor à propriedade X da variável o. Isso acontece porque ela é uma referência readonly a um tipo de valor (uma struct). Caso fosse uma classe seria possível alterar o valor de X, mas também não seria permitido sobrescrever o valor de o.

E o que acontece com chamadas de método? Um método pode alterar o estado interno de uma struct, e o compilador não tem como adivinhar que ele fez isso, certo? Sim, por isso toda chamada de método é feita sobre uma cópia do objeto. No exemplo a seguir, a chamada a SetX altera o valor de X para o seu primeiro argumento recebido, mas ela é feita sobre uma cópia de o, e o valor de X não é alterado na origem:

o.SetX(3);
WriteLine(origin.X); // 0

Você pode ignorar o fato de um método ser ref readonly e passar o resultado direto para uma variável, por valor, sem utilizar as palavras chave. Neste caso, uma cópia do valor retornado por referência é criada imediatamente. O exemplo a seguir faz exatamente isso:

private readonly Vector origin = new Vector(0, 0, 0);
public ref readonly Vector FetchOrigin() => ref origin;
public void M()
{
    var o = FetchOrigin();
    o = new Vector();
    o.X = 3;
    WriteLine(o.X); // 3
    WriteLine(origin.X); // 0
    o.SetX(4);
    WriteLine(o.X); // 4
    WriteLine(origin.X); // 0
}

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.