O que realmente acontece em uma tentativa {return x; } finalmente {x = null; } declaração?

259

Vi essa dica em outra pergunta e fiquei imaginando se alguém poderia me explicar como isso funciona.

try { return x; } finally { x = null; }

Quero dizer, a finallycláusula realmente é executada após a returndeclaração? Quão inseguro é esse código? Você consegue pensar em alguma try-finallyinvasão adicional que possa ser feita através dessa invasão?

Dmitri Nesteruk
fonte

Respostas:

235

Não - no nível IL, você não pode retornar de dentro de um bloco manipulado por exceção. Essencialmente, armazena-o em uma variável e retorna depois

ou seja, semelhante a:

int tmp;
try {
  tmp = ...
} finally {
  ...
}
return tmp;

por exemplo (usando refletor):

static int Test() {
    try {
        return SomeNumber();
    } finally {
        Foo();
    }
}

compila para:

.method private hidebysig static int32 Test() cil managed
{
    .maxstack 1
    .locals init (
        [0] int32 CS$1$0000)
    L_0000: call int32 Program::SomeNumber()
    L_0005: stloc.0 
    L_0006: leave.s L_000e
    L_0008: call void Program::Foo()
    L_000d: endfinally 
    L_000e: ldloc.0 
    L_000f: ret 
    .try L_0000 to L_0008 finally handler L_0008 to L_000e
}

Basicamente, isso declara uma variável local ( CS$1$0000), coloca o valor na variável (dentro do bloco manipulado) e, depois de sair do bloco, carrega a variável e depois a retorna. Refletor processa isso como:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}
Marc Gravell
fonte
10
Não é exatamente isso que ocdedio disse: o finalmente é executado após o cálculo do valor de retorno e antes de realmente retornar da função ???
mmmmmmmm
"bloco manipulado por exceção" Acho que esse cenário não tem nada a ver com exceções e manipulação de exceção. É sobre como o .NET implementa a construção finalmente da guarda de recursos .
g.pickardou
361

A instrução finalmente é executada, mas o valor de retorno não é afetado. A ordem de execução é:

  1. Código antes da declaração de retorno ser executada
  2. A expressão na declaração de retorno é avaliada
  3. finalmente o bloco é executado
  4. O resultado avaliado na etapa 2 é retornado

Aqui está um pequeno programa para demonstrar:

using System;

class Test
{
    static string x;

    static void Main()
    {
        Console.WriteLine(Method());
        Console.WriteLine(x);
    }

    static string Method()
    {
        try
        {
            x = "try";
            return x;
        }
        finally
        {
            x = "finally";
        }
    }
}

Isso imprime "try" (porque é isso que é retornado) e depois "finalmente" porque esse é o novo valor de x.

Obviamente, se retornarmos uma referência a um objeto mutável (por exemplo, um StringBuilder), todas as alterações feitas no objeto no bloco finally serão visíveis no retorno - isso não afetou o próprio valor de retorno (que é apenas um referência).

Jon Skeet
fonte
Eu gostaria de perguntar, Existe alguma opção no visual studio para ver a linguagem intermediária gerado (IL) para código escrito C # no momento da execução ....
Estado Enigma
A exceção "se estamos retornando uma referência a um objeto mutável (por exemplo, um StringBuilder), todas as alterações feitas no objeto no bloco final serão visíveis no retorno" é se o objeto StringBuilder estiver definido como nulo no bloco final, nesse caso, um objeto não nulo é retornado.
Nick
4
@ Nick: Isso não é uma alteração no objeto - é uma alteração na variável . Não afeta o objeto ao qual o valor anterior da variável se refere. Então não, não é uma exceção.
91313 Jon Skeet
3
@Skeet Isso significa "declaração de retorno retorna uma cópia"?
Prabhakaran
4
@prabhakaran: Bem, avalia a expressão no ponto da returndeclaração, e esse valor será retornado. A expressão não é avaliada como o controle sai do método.
Jon Skeet
19

A cláusula finally é executada após a declaração de retorno, mas antes de realmente retornar da função. Isso tem pouco a ver com a segurança dos fios, eu acho. Não é um hack - o finalmente é garantido para sempre executar, não importa o que você faça no bloco try ou no bloco catch.

Otávio Décio
fonte
13

Além das respostas dadas por Marc Gravell e Jon Skeet, é importante observar que objetos e outros tipos de referência se comportam de maneira semelhante quando retornados, mas existem algumas diferenças.

O "O quê" retornado segue a mesma lógica dos tipos simples:

class Test {
    public static Exception AnException() {
        Exception ex = new Exception("Me");
        try {
            return ex;
        } finally {
            // Reference unchanged, Local variable changed
            ex = new Exception("Not Me");
        }
    }
}

A referência que está sendo retornada já foi avaliada antes da variável local receber uma nova referência no bloco final.

A execução é essencialmente:

class Test {
    public static Exception AnException() {
        Exception ex = new Exception("Me");
        Exception CS$1$0000 = null;
        try {
            CS$1$0000 = ex;
        } finally {
            // Reference unchanged, Local variable changed
            ex = new Exception("Not Me");
        }
        return CS$1$0000;
    }
}

A diferença é que ainda seria possível modificar tipos mutáveis ​​usando as propriedades / métodos do objeto que podem resultar em comportamentos inesperados, se você não tomar cuidado.

class Test2 {
    public static System.IO.MemoryStream BadStream(byte[] buffer) {
        System.IO.MemoryStream ms = new System.IO.MemoryStream(buffer);
        try {
            return ms;
        } finally {
            // Reference unchanged, Referenced Object changed
            ms.Dispose();
        }
    }
}

Uma segunda coisa a considerar sobre try-return-finalmente é que os parâmetros passados ​​"por referência" ainda podem ser modificados após o retorno. Somente o valor de retorno foi avaliado e é armazenado em uma variável temporária aguardando retorno, quaisquer outras variáveis ​​ainda são modificadas da maneira normal. O contrato de um parâmetro de saída pode até não ser cumprido até o bloqueio final dessa maneira.

class ByRefTests {
    public static int One(out int i) {
        try {
            i = 1;
            return i;
        } finally {
            // Return value unchanged, Store new value referenced variable
            i = 1000;
        }
    }

    public static int Two(ref int i) {
        try {
            i = 2;
            return i;
        } finally {
            // Return value unchanged, Store new value referenced variable
            i = 2000;
        }
    }

    public static int Three(out int i) {
        try {
            return 3;
        } finally {
            // This is not a compile error!
            // Return value unchanged, Store new value referenced variable
            i = 3000;
        }
    }
}

Como qualquer outra construção de fluxo, "try-return-finalmente" tem seu lugar e pode permitir um código com aparência mais limpa do que escrever a estrutura na qual ele realmente compila. Mas deve ser usado com cuidado para evitar pegadinhas.

Arkaine55
fonte
4

Se xfor uma variável local, não vejo o ponto, pois xserá efetivamente definido como nulo de qualquer maneira quando o método for encerrado e o valor do valor de retorno não for nulo (desde que foi colocado no registro antes da chamada para definir xpara nulo).

Só posso ver isso acontecendo se você quiser garantir a alteração do valor de um campo no retorno (e após a determinação do valor de retorno).

casperOne
fonte
A menos que a variável local também é capturada por um delegado :)
Jon Skeet
Depois, há um fechamento e o objeto não pode ser coletado como lixo, porque ainda há uma referência.
recursiva
Mas ainda não vejo por que você usaria um var local em um delegado, a menos que pretendesse usar seu valor.
recursiva