why statement net namespace instruction end c# using using-statement

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 el font4 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.