c# - practices - Lo que realmente sucede en un intento{return x;}finalmente{x=nulo; declaración?
throw exception c# (5)
Al agregar a las respuestas de 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 se asigne una nueva referencia a la variable local en el bloque finally.
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 resultar en comportamientos inesperados si no 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-finalmente es que los parámetros pasados "por referencia" todavía pueden modificarse después de la devolución. Solo el valor de retorno se ha evaluado y se almacena en una variable temporal en espera de ser devuelta; cualquier otra variable se modifica de la forma habitual. El contrato de un parámetro de salida puede incluso quedar sin cumplirse 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;
}
}
}
Al igual que 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 que realmente compila. Pero debe usarse con cuidado para evitar las gotcha.
Vi esta sugerencia en otra pregunta y me preguntaba si alguien podría explicarme cómo funciona esto?
try { return x; } finally { x = null; }
Quiero decir, ¿la cláusula finally
realmente se ejecuta después de la declaración de return
? ¿Qué tan peligroso es este código? ¿Puedes pensar en algún hackery adicional que se pueda hacer con este try-finally
hackear?
La cláusula finally se ejecuta después de la instrucción de retorno pero antes de regresar realmente de la función. Tiene poco que ver con la seguridad de los hilos, creo. No es un pirateo: se garantiza que el final siempre se ejecuta sin importar lo que hagas en tu bloque try o en tu bloque catch.
La sentencia finally se ejecuta, pero el valor devuelto no se ve afectado. La orden de ejecución es:
- Código antes de ejecutar la declaración de devolución
- Expresión en la declaración de retorno se evalúa
- finalmente se ejecuta el bloque
- Se devuelve el resultado evaluado en el paso 2
Aquí hay un programa corto 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 "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 finally será visible en el retorno, esto no ha afectado al valor de retorno en sí mismo (que es solo un referencia).
No: en el nivel de IL no puede regresar desde dentro de 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
}
Esto básicamente 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 hace esto como:
private static int Test()
{
int CS$1$0000;
try
{
CS$1$0000 = SomeNumber();
}
finally
{
Foo();
}
return CS$1$0000;
}
Si x
es una variable local, no veo el punto, ya que x
se establecerá efectivamente en nulo de todos modos 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 establecer x
en nulo).
Solo puedo ver que esto suceda si desea garantizar el cambio del valor de un campo en el momento de la devolución (y una vez que se determina el valor de retorno).