c# - statement - ¿Puede "usar" con más de un recurso causar una fuga de recursos?
using instruction c# (5)
Aquí hay un código de muestra para probar la respuesta de @SLaks:
void Main()
{
try
{
using (TestUsing t1 = new TestUsing("t1"), t2 = new TestUsing("t2"))
{
}
}
catch(Exception ex)
{
Console.WriteLine("catch");
}
finally
{
Console.WriteLine("done");
}
/* outputs
Construct: t1
Construct: t2
Dispose: t1
catch
done
*/
}
public class TestUsing : IDisposable
{
public string Name {get; set;}
public TestUsing(string name)
{
Name = name;
Console.WriteLine("Construct: " + Name);
if (Name == "t2") throw new Exception();
}
public void Dispose()
{
Console.WriteLine("Dispose: " + Name);
}
}
C # me permite hacer lo siguiente (ejemplo de MSDN):
using (Font font3 = new Font("Arial", 10.0f),
font4 = new Font("Arial", 10.0f))
{
// Use font3 and font4.
}
¿Qué sucede si font4 = new Font
throws? Por lo que entiendo, font3 perderá recursos y no se eliminará.
- ¿Es esto cierto? (font4 no se eliminará)
- ¿Esto significa
using(... , ...)
debería evitarse por completo a favor del uso anidado?
Como complemento a la respuesta de @SLaks, aquí está la IL para su código:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 74 (0x4a)
.maxstack 2
.entrypoint
.locals init (
[0] class [System.Drawing]System.Drawing.Font font3,
[1] class [System.Drawing]System.Drawing.Font font4,
[2] bool CS$4$0000
)
IL_0000: nop
IL_0001: ldstr "Arial"
IL_0006: ldc.r4 10
IL_000b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
IL_0010: stloc.0
.try
{
IL_0011: ldstr "Arial"
IL_0016: ldc.r4 10
IL_001b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
IL_0020: stloc.1
.try
{
IL_0021: nop
IL_0022: nop
IL_0023: leave.s IL_0035
} // end .try
finally
{
IL_0025: ldloc.1
IL_0026: ldnull
IL_0027: ceq
IL_0029: stloc.2
IL_002a: ldloc.2
IL_002b: brtrue.s IL_0034
IL_002d: ldloc.1
IL_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0033: nop
IL_0034: endfinally
} // end handler
IL_0035: nop
IL_0036: leave.s IL_0048
} // end .try
finally
{
IL_0038: ldloc.0
IL_0039: ldnull
IL_003a: ceq
IL_003c: stloc.2
IL_003d: ldloc.2
IL_003e: brtrue.s IL_0047
IL_0040: ldloc.0
IL_0041: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0046: nop
IL_0047: endfinally
} // end handler
IL_0048: nop
IL_0049: ret
} // end of method Program::Main
Tenga en cuenta los bloques try / finally anidados.
Este código (basado en la muestra original):
using System.Drawing;
public class Class1
{
public Class1()
{
using (Font font3 = new Font("Arial", 10.0f),
font4 = new Font("Arial", 10.0f))
{
// Use font3 and font4.
}
}
}
Produce el siguiente CIL (en Visual Studio 2013 , apuntando a .NET 4.5.1):
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 82 (0x52)
.maxstack 2
.locals init ([0] class [System.Drawing]System.Drawing.Font font3,
[1] class [System.Drawing]System.Drawing.Font font4,
[2] bool CS$4$0000)
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: nop
IL_0008: ldstr "Arial"
IL_000d: ldc.r4 10.
IL_0012: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string,
float32)
IL_0017: stloc.0
.try
{
IL_0018: ldstr "Arial"
IL_001d: ldc.r4 10.
IL_0022: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string,
float32)
IL_0027: stloc.1
.try
{
IL_0028: nop
IL_0029: nop
IL_002a: leave.s IL_003c
} // end .try
finally
{
IL_002c: ldloc.1
IL_002d: ldnull
IL_002e: ceq
IL_0030: stloc.2
IL_0031: ldloc.2
IL_0032: brtrue.s IL_003b
IL_0034: ldloc.1
IL_0035: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_003a: nop
IL_003b: endfinally
} // end handler
IL_003c: nop
IL_003d: leave.s IL_004f
} // end .try
finally
{
IL_003f: ldloc.0
IL_0040: ldnull
IL_0041: ceq
IL_0043: stloc.2
IL_0044: ldloc.2
IL_0045: brtrue.s IL_004e
IL_0047: ldloc.0
IL_0048: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_004d: nop
IL_004e: endfinally
} // end handler
IL_004f: nop
IL_0050: nop
IL_0051: ret
} // end of method Class1::.ctor
Como puede ver, el bloque try {}
no se inicia hasta después de la primera asignación, que tiene lugar en IL_0012
. A primera vista, parece que asigna el primer elemento en código desprotegido. Sin embargo, observe que el resultado se almacena en la ubicación 0. Si la segunda asignación falla, el bloque externo finally {}
ejecuta, y esto recupera el objeto de la ubicación 0, es decir, la primera asignación de font3
y llama a su método Dispose()
.
Curiosamente, la descompilación de este ensamblaje con dotPeek produce la siguiente fuente reconstituida:
using System.Drawing;
public class Class1
{
public Class1()
{
using (new Font("Arial", 10f))
{
using (new Font("Arial", 10f))
;
}
}
}
El código descompilado confirma que todo es correcto y que el using
se expande esencialmente en anidados using
s. El código CIL es un poco confuso de mirar, y tuve que mirarlo durante unos minutos antes de comprender correctamente lo que estaba sucediendo, así que no me sorprende que algunos "cuentos de viejas" hayan comenzado a brotar sobre esta. Sin embargo, el código generado es la verdad inexpugnable.
No.
El compilador generará un bloque finally
por separado para cada variable.
La spec (§8.13) dice:
Cuando una adquisición de recursos toma la forma de una declaración de variable local, es posible adquirir múltiples recursos de un tipo dado. Una declaración de
using
de la forma
using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement
es precisamente equivalente a una secuencia de instrucciones de uso anidadas:
using (ResourceType r1 = e1) using (ResourceType r2 = e2) ... using (ResourceType rN = eN) statement
ACTUALIZACIÓN : utilicé esta pregunta como la base para un artículo que se puede encontrar here ; verlo para una discusión adicional de este tema. Gracias por la buena pregunta!
Aunque la respuesta de Schabse es, por supuesto, correcta y responde la pregunta que se hizo, hay una variante importante en su pregunta que no hizo:
¿Qué ocurre si
font4 = new Font()
lanza después de que el constructor asignó el recurso no administrado pero antes de que el ctor regrese y rellene elfont4
con la referencia?
Déjame hacer eso un poco más claro. Supongamos que tenemos:
public sealed class Foo : IDisposable
{
private int handle = 0;
private bool disposed = false;
public Foo()
{
Blah1();
int x = AllocateResource();
Blah2();
this.handle = x;
Blah3();
}
~Foo()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!this.disposed)
{
if (this.handle != 0)
DeallocateResource(this.handle);
this.handle = 0;
this.disposed = true;
}
}
}
Ahora tenemos
using(Foo foo = new Foo())
Whatever(foo);
Esto es lo mismo que
{
Foo foo = new Foo();
try
{
Whatever(foo);
}
finally
{
IDisposable d = foo as IDisposable;
if (d != null)
d.Dispose();
}
}
DE ACUERDO. Supongamos lo que Whatever
arroje. Entonces el bloque finally
se ejecuta y el recurso es desasignado. No hay problema.
Supongamos que Blah1()
lanza. Luego, el lanzamiento ocurre antes de que se asigne el recurso. El objeto ha sido asignado, pero el ctor nunca regresa, por lo que foo
nunca se completa. Nunca entramos en el try
así que nunca ingresamos finally
. La referencia del objeto ha quedado huérfana. Finalmente, el GC lo descubrirá y lo pondrá en la cola del finalizador. handle
aún es cero, por lo que el finalizador no hace nada. Tenga en cuenta que se requiere que el finalizador sea robusto frente a un objeto que se está finalizando cuyo constructor nunca se completó . Debes escribir finalizadores que sean tan fuertes. Esta es una razón más por la que debe dejar los finalizadores de la escritura a los expertos y no tratar de hacerlo usted mismo.
Supongamos que Blah3()
lanza. El lanzamiento ocurre después de que se asigna el recurso. Pero, de nuevo, foo
nunca se rellena, nunca entramos finally
, y el objeto se limpia con el hilo del finalizador. Esta vez, el identificador no es cero y el finalizador lo limpia. De nuevo, el finalizador se ejecuta en un objeto cuyo constructor nunca tuvo éxito, pero el finalizador se ejecuta de todos modos. Obviamente debe ser porque esta vez, tenía trabajo por hacer.
Ahora supongamos que Blah2()
lanza. ¡El lanzamiento ocurre después de que se asigna el recurso pero antes de que se complete el handle
! De nuevo, el finalizador se ejecutará pero ahora el handle
sigue siendo cero y ¡se filtra el identificador!
Debe escribir un código extremadamente inteligente para evitar que ocurra esta filtración. Ahora, en el caso de tu recurso de Font
, ¿a quién diablos le importa? Perdemos un manejador de fuente, gran cosa. Pero si absolutamente requiere que se limpien todos los recursos no administrados sin importar el momento de las excepciones, entonces tiene un problema muy difícil en sus manos.
El CLR tiene que resolver este problema con bloqueos. Desde C # 4, los bloqueos que usan la declaración de lock
se han implementado de esta manera:
bool lockEntered = false;
object lockObject = whatever;
try
{
Monitor.Enter(lockObject, ref lockEntered);
lock body here
}
finally
{
if (lockEntered) Monitor.Exit(lockObject);
}
Enter
se ha escrito con mucho cuidado para que, independientemente de las excepciones que se lockEntered
, lockEntered
se establezca en verdadero si y solo si se realizó realmente el bloqueo. Si tiene requisitos similares, entonces lo que necesita es escribir:
public Foo()
{
Blah1();
AllocateResource(ref handle);
Blah2();
Blah3();
}
y escriba AllocateResource
ingeniosamente como Monitor.Enter
para que pase lo que pase dentro de AllocateResource
, el handle
se completa si y solo si debe desasignarse.
Describir las técnicas para hacerlo está más allá del alcance de esta respuesta. Consulte a un experto si tiene este requisito.