Os Perigos das Estruturas Mutáveis

Dada a seguinte estrutura:

public struct S : IDisposable
{
    public int Value { get; private set; }

    public S(int value) : this()
    {
        this.Value = value;
    }
    public void SetValue(int value)
    {
        this.Value = value;
    }

    public void Dispose()
    {
        Console.WriteLine(
            "Disposing: {0}",
            this.Value);
        this.Value = 0;
    }
}

Qual é o resultado da execução do seguinte código?

 using (var s1 = new S(1))
{
    s1.SetValue(-1);
}
Console.WriteLine();

var s2 = new S(2);
using (s2)
{
    s2.SetValue(-1);
}
Console.WriteLine("Disposed: {0}", s2.Value);
Console.WriteLine();

var s3 = new S(3);
try
{
    s3.SetValue(-1);
}
finally
{
    s3.Dispose();
}
Console.WriteLine("Disposed: {0}", s3.Value);
Console.WriteLine();

var s4 = new S(4);
try
{
    s4.SetValue(-1);
}
finally
{
    ((IDisposable)s4).Dispose();
}
Console.WriteLine("Disposed: {0}", s4.Value);
Console.WriteLine();

Resultado

O resultado da execução é:

Disposing: -1

Disposing: 2
Disposed: -1

Disposing: -1
Disposed: 0

Disposing: -1
Disposed: -1

Explicação

Para entender o que se está aqui a passar é necessário ter em mente que as estruturas (struct) são tipos valor, o que quer dizer que quando se copia uma estrutura não se está apenas a copiar o seu endereço no heap, mas está-se a copiar toda a estrutura.

No primeiro caso, o compilador está a lidar apenas com a variável s1. No entanto, dado o escopo da variável não é possível validar que o valor final de s1.Value é 0.

No segundo caso, como se está a usar na instrução using um valor previamente criado, o compilador usa uma cópia desse valor para no final invocar o método Dispose. A razão para a utilização desta cópia é para evitar que o valor do escopo da instrução using seja alterado. E como se trata de um tipo valor, é feita uma cópia integral do objeto o que faz com que o objeto contido na variável s2 não seja aquele a que foi invocado o método Dispose mas sim a cópia feita anteriormente. É por esta razão que o valor do campo Value do objeto em que foi invocado o método Dispose ainda é 2 e o valor do campo Value do objeto contido na variável s2 ainda é -1.

O terceiro caso é um caso simples em que não existe qualquer “truque”.

No quarto caso existe uma operação explícita de cast de um tipo valor para uma interface. Esta operação de cast é feita à custa de uma operação de boxing do objeto do tipo valor (que consiste em alocar memória no heap e copiar para lá o valor do objeto) e posterior acesso como se tratando do objeto de um tipo referência. É por existir esta cópia no troço finally que o objeto em que está a ser invocado o método Dispose não é o mesmo contido na variável s4 mas uma cópia feita no momento da invocação do método Dispose, pelo que, a variável s4 mantém o estado que tinha antes da invocação feita à sua cópia.

Conclusão

Os tipos valor, nomeadamente as estruturas (struct), têm algumas vantagens (como por exemplo alocação no stack evitando o peso do GC (garbage collector)) mas quando se altera o seu estado interno pode ter consequências imprevisíveis.

Recursos

Publicado na edição 43 (PDF) da Revista PROGRAMAR.