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

Resuelto Dmitri Nesteruk asked hace 15 años • 5 respuestas

Vi este consejo en otra pregunta y me preguntaba si alguien podría explicarme cómo 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 para subprocesos es este código? ¿Se te ocurre algún truco adicional que se pueda hacer con este try-finallytruco?

Dmitri Nesteruk avatar Jan 08 '09 02:01 Dmitri Nesteruk
Aceptado

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

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

Aquí hay un breve programa para demostrarlo:

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 "intentar" (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 en el retorno; esto no ha afectado el valor de retorno en sí (que es solo un referencia).

Jon Skeet avatar Jan 07 '2009 19:01 Jon Skeet

No, en el nivel IL no se puede regresar desde dentro de un bloque controlado por excepciones. Básicamente lo almacena en una variable y regresa después.

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 avatar Jan 07 '2009 19:01 Marc Gravell

La cláusula finalmente se ejecuta después de la declaración de retorno pero antes de regresar de la función. Creo que tiene poco que ver con la seguridad de los hilos. No es un truco: se garantiza que finalmente se ejecutará siempre sin importar lo que haga en su bloque try o en su bloque catch.

Otávio Décio avatar Jan 07 '2009 19:01 Otávio Décio

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 ha sido evaluada 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 tipos mutables utilizando las propiedades/métodos del objeto, lo que puede dar lugar a 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 sobre try-return-finally es que los parámetros pasados ​​"por referencia" aún se pueden modificar después de la devolución. Solo se ha evaluado el valor de retorno y se almacena en una variable temporal en espera de ser devuelto; cualquier otra variable aún se modifica de la manera normal. El contrato de un parámetro out puede incluso quedar incumplido 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, "intentar-retornar-finalmente" tiene su lugar y puede permitir un código con una apariencia más limpia que escribir la estructura en la que realmente se compila. Pero debe usarse con cuidado para evitar problemas.

Arkaine55 avatar Jul 12 '2012 17:07 Arkaine55

Si xes una variable local, no veo el punto, ya que xde 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 a establecerx a nulo).

Solo veo 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 avatar Jan 07 '2009 19:01 casperOne