Lo que realmente sucede en un intento {return x; } finalmente {x = nulo; } declaración?

259

Vi este consejo en otra pregunta y me preguntaba si alguien podría explicarme cómo diablos funciona esto.

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

Quiero decir, ¿la finallycláusula realmente se ejecuta después de la returndeclaración? ¿Qué tan inseguro es este código? ¿Puedes pensar en algún hacker adicional que se pueda hacer con este try-finallyhack?

Dmitri Nesteruk
fuente

Respuestas:

235

No, en el nivel de IL no puede regresar desde un bloque manejado por excepciones. Esencialmente lo almacena en una variable y luego regresa

es decir, similar a:

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

por ejemplo (usando reflector):

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

compila a:

.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
}

Básicamente, esto declara una variable local ( CS$1$0000), coloca el valor en la variable (dentro del bloque manejado), luego, después de salir del bloque, carga la variable y luego la devuelve. Reflector representa esto como:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}
Marc Gravell
fuente
10
¿No es exactamente lo que dijo ocdedio: finalmente se ejecuta después del cálculo del valor de retorno y antes de regresar realmente de la función?
mmmmmmmm
"bloque manejado por excepciones" Creo que este escenario no tiene nada que ver con excepciones y manejo de excepciones. Se trata de cómo .NET implementa la construcción de protección de recursos finalmente .
g.pickardou
361

La sentencia finalmente se ejecuta, pero el valor de retorno no se ve afectado. La orden de ejecución es:

  1. Código antes de ejecutar la declaración de devolución
  2. Se evalúa la expresión en la declaración de retorno
  3. finalmente se ejecuta el bloque
  4. Se devuelve el resultado evaluado en el paso 2

Aquí hay un breve programa para demostrar:

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";
        }
    }
}

Esto imprime "probar" (porque eso es lo que se devuelve) y luego "finalmente" porque ese es el nuevo valor de x.

Por supuesto, si devolvemos una referencia a un objeto mutable (por ejemplo, un StringBuilder), cualquier cambio realizado en el objeto en el bloque finalmente será visible al regresar; esto no ha afectado el valor de retorno en sí (que es solo un referencia).

Jon Skeet
fuente
Me gustaría preguntar, ¿Hay alguna opción en Visual Studio para ver el lenguaje intermedio generado (IL) para el código C # escrito en el momento de la ejecución ....
Enigma Estado
La excepción a "si devolvemos una referencia a un objeto mutable (por ejemplo, un StringBuilder), entonces cualquier cambio realizado en el objeto en el último bloque será visible al regresar" es si el objeto StringBuilder se establece como nulo en el bloque final, en cuyo caso se devuelve un objeto no nulo.
Nick
44
@ Nick: Eso no es un cambio en el objeto , es un cambio en la variable . No afecta al objeto al que hace referencia el valor anterior de la variable. Entonces no, no es una excepción.
Jon Skeet
3
@Skeet ¿Eso significa "la declaración de devolución devuelve una copia"?
prabhakaran
44
@prabhakaran: Bueno, evalúa la expresión en el punto de la returndeclaración, y ese valor será devuelto. La expresión no se evalúa ya que el control abandona el método.
Jon Skeet
19

La cláusula finalmente se ejecuta después de la declaración return pero antes de regresar de la función. Creo que tiene poco que ver con la seguridad del hilo. No es un truco: finalmente se garantiza que siempre se ejecutará sin importar lo que hagas en tu bloque de prueba o tu bloque de captura.

Otávio Décio
fuente
13

Además de las respuestas dadas por Marc Gravell y Jon Skeet, es importante tener en cuenta que los objetos y otros tipos de referencia se comportan de manera similar cuando se devuelven, pero tienen algunas diferencias.

El "Qué" que se devuelve sigue la misma lógica que los 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");
        }
    }
}

La referencia que se devuelve ya se ha evaluado antes de que a la variable local se le asigne una nueva referencia en el bloque finalmente.

La ejecución es esencialmente:

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;
    }
}

La diferencia es que aún sería posible modificar los tipos mutables utilizando las propiedades / métodos del objeto, lo que puede generar comportamientos inesperados si no se tiene 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();
        }
    }
}

Una segunda cosa a considerar acerca de try-return-finally es que los parámetros pasados ​​"por referencia" aún pueden modificarse después del retorno. Solo se ha evaluado el valor de retorno y se almacena en una variable temporal en espera de ser devuelta, cualquier otra variable aún se modifica de la manera normal. El contrato de un parámetro de salida incluso puede quedar sin cumplir hasta que finalmente se bloquee de esta manera.

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 cualquier otra construcción de flujo, "try-return-finally" tiene su lugar y puede permitir un código de aspecto más limpio que escribir la estructura con la que realmente se compila. Pero debe usarse con cuidado para evitar los gotcha's.

Arkaine55
fuente
4

Si xes una variable local, no veo el punto, ya xque de todos modos se establecerá efectivamente en nulo cuando se salga del método y el valor del valor de retorno no sea nulo (ya que se colocó en el registro antes de la llamada para establecerx a nulo).

Solo puedo ver que esto suceda si desea garantizar el cambio del valor de un campo al regresar (y después de que se determine el valor de retorno).

casperOne
fuente
A menos que la variable local también sea capturada por un delegado :)
Jon Skeet
Luego hay un cierre, y el objeto no se puede recolectar basura, porque todavía hay una referencia.
recursivo el
Pero todavía no veo por qué usaría una var local en un delegado a menos que tuviera la intención de usar su valor.
recursivo