¿Puede el "uso" con más de un recurso causar una pérdida de recursos?

106

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é pasa si font4 = new Fontlanza? Por lo que entiendo, font3 filtrará recursos y no se eliminarán.

  • ¿Es esto cierto? (font4 no se eliminará)
  • ¿Esto significa que using(... , ...)debería evitarse por completo en favor del uso anidado?
Benjamin Gruenbaum
fuente
7
No va a fugas de memoria; en el peor de los casos, seguirá obteniendo GC.
SLaks
3
No me sorprendería si using(... , ...)se compila en bloques anidados de cualquier forma, pero no lo sé con certeza.
Dan J
1
Eso no es lo que quise decir. Incluso si no lo usa usingen absoluto, el GC eventualmente lo recogerá.
SLaks
1
@zneak: si se hubiera compilado en un solo finallybloque, no habría entrado en el bloque hasta que se hubieran construido todos los recursos.
SLaks
2
@zneak: Porque en la conversión de a usinga try- finally, la expresión de inicialización se evalúa fuera de try. Entonces es una pregunta razonable.
Ben Voigt

Respuestas:

158

No.

El compilador generará un finallybloque separado para cada variable.

La especificación (§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 determinado. Una usingdeclaración de la forma

using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement 

es exactamente equivalente a una secuencia de instrucciones using anidadas:

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement
SLaks
fuente
4
Eso es 8.13 en la versión 5.0 de la especificación C #, por cierto.
Ben Voigt
11
@WeylandYutani: ¿Qué estás preguntando?
SLaks
9
@WeylandYutani: Este es un sitio de preguntas y respuestas. Si tiene una pregunta, comience una nueva pregunta, por favor.
Eric Lippert
5
@ user1306322 ¿por qué? ¿Y si realmente quiero saber?
Oxímoron
2
@Oxymoron, entonces debe proporcionar alguna evidencia de esfuerzo antes de publicar la pregunta en forma de investigación y conjeturas, o de lo contrario se le dirá lo mismo, perderá la atención y, de lo contrario, estará más perdido. Solo un consejo basado en la experiencia personal.
user1306322
67

ACTUALIZACIÓN : utilicé esta pregunta como base para un artículo que se puede encontrar aquí ; consúltelo para una discusión adicional sobre este tema. ¡Gracias por la buena pregunta!


Aunque la respuesta de Schabse es, por supuesto, correcta y responde a la pregunta que se hizo, hay una variante importante en su pregunta que no hizo:

¿Qué sucede si se font4 = new Font()lanza después de que el constructor asignó el recurso no administrado pero antes de que el ctor regrese y complete font4con la referencia?

Déjame aclararlo un poco más. 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();
    }
}

OKAY. Supongamos Whateverlanzamientos. Luego, el finallybloque se ejecuta y el recurso se desasigna. No hay problema.

Supongamos Blah1()lanzamientos. Luego, el lanzamiento ocurre antes de que se asigne el recurso. El objeto ha sido asignado pero el ctor nunca regresa, por foolo que nunca se completa. tryNunca ingresamos, por lo que nunca ingresamos finallytampoco. La referencia del objeto se ha quedado huérfana. Finalmente, el GC lo descubrirá y lo pondrá en la cola del finalizador. handlesigue siendo cero, por lo que el finalizador no hace nada. Observe que se requiere que el finalizador sea robusto frente a un objeto que se está finalizando cuyo constructor nunca se completó . Usted está obligado a escribir finalizadores que son tan fuerte. Esta es otra razón más por la que debería dejar los finalizadores de escritura a expertos y no intentar hacerlo usted mismo.

Supongamos Blah3()lanzamientos. El lanzamiento ocurre después de que se asigna el recurso. Pero nuevamente, foonunca se completa, nunca ingresamos finallyy el objeto es limpiado por 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 porque esta vez, tenía trabajo que hacer.

Ahora suponga que Blah2()lanza. ¡El lanzamiento ocurre después de que se asigna el recurso pero antes de que handle se complete! Nuevamente, el finalizador se ejecutará, pero ahora handlesigue siendo cero y ¡filtramos el controlador!

Debe escribir un código extremadamente inteligente para evitar que ocurra esta fuga. Ahora, en el caso de su Fontrecurso, ¿a quién diablos le importa? Filtramos un identificador de fuente, gran cosa. Pero si necesita de manera absolutamente positiva que todos los recursos no administrados se limpien 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 lockdeclaración se han implementado así:

bool lockEntered = false;
object lockObject = whatever;
try
{
    Monitor.Enter(lockObject, ref lockEntered);
    lock body here
}
finally
{
    if (lockEntered) Monitor.Exit(lockObject);
}

Enterse ha escrito con mucho cuidado para que, independientemente de las excepciones que se generen , lockEnteredse establezca en verdadero si y solo si el bloqueo se tomó realmente. Si tiene requisitos similares, entonces lo que necesita es escribir:

    public Foo()
    {
        Blah1();
        AllocateResource(ref handle);
        Blah2();
        Blah3();
    }

y escriba AllocateResourceinteligentemente como Monitor.Enterpara que, pase lo que pase en el interior AllocateResource, handlese rellene si y sólo si es necesario desasignarlo.

Describir las técnicas para hacerlo está más allá del alcance de esta respuesta. Consulte a un experto si tiene este requisito.

Eric Lippert
fuente
6
@gnat: La respuesta aceptada. Esa S tiene que representar algo. :-)
Eric Lippert
12
@Joe: Por supuesto que el ejemplo es artificial . Simplemente lo inventé . Los riesgos no son exagerados porque no he indicado cuál es el nivel de riesgo; más bien, he dicho que este patrón es posible . El hecho de que crea que establecer el campo resuelve directamente el problema indica precisamente mi punto: que, al igual que la gran mayoría de los programadores que no tienen experiencia con este tipo de problemas, no son competentes para resolver este problema; de hecho, la mayoría de la gente ni siquiera reconocen que no es un problema, que es por eso que escribí esta respuesta en el primer lugar .
Eric Lippert
5
@Chris: Supongamos que no hay trabajo realizado entre la asignación y la devolución, y entre la devolución y la asignación. Eliminamos todas esas Blahllamadas a métodos. ¿Qué impide que suceda una ThreadAbortException en cualquiera de esos puntos?
Eric Lippert
5
@Joe: Esta no es una sociedad de debate; No busco ganar puntos siendo más convincente . Si es escéptico y no quiere creer en mi palabra de que se trata de un problema delicado que requiere consultar con expertos para resolverlo correctamente, puede estar en desacuerdo conmigo.
Eric Lippert
7
@GilesRoberts: ¿Cómo resuelve eso el problema? Suponga que la excepción ocurre después de la llamada a AllocateResourcepero antes de la asignación a x. A ThreadAbortExceptionpuede suceder en ese momento. Todos aquí parecen estar perdiendo mi punto, que es la creación de un recurso y la asignación de una referencia a él a una variable no es una operación atómica . Para resolver el problema que he identificado, debe convertirlo en una operación atómica.
Eric Lippert
32

Como complemento a la respuesta de @SLaks, aquí está el 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 de prueba / finalmente anidados.

David Heffernan
fuente
17

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 , dirigido 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 try {}bloque no comienza hasta después de la primera asignación, que tiene lugar en IL_0012. A primera vista, esto parece asignar el primer elemento en un 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 {} se ejecuta, y este obtiene el objeto de la ubicación 0, es decir, la primera asignación de font3, y llama a su Dispose()método.

Curiosamente, la descompilación de este ensamblado 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 usingestá esencialmente expandido en usings anidados . El código CIL es un poco confuso de ver, y tuve que mirarlo durante unos minutos antes de entender correctamente lo que estaba sucediendo, por lo 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.

Tim Long
fuente
@Peter Mortensen, su edición eliminó fragmentos del código IL (entre IL_0012 e IL_0017), lo que hace que la explicación sea inválida y confusa. Ese código estaba destinado a ser una copia literal de los resultados que obtuve y la edición invalida eso. ¿Puedes revisar tu edición y confirmar que es lo que pretendías?
Tim Long
7

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);
    }
}
wdosanjos
fuente
1
Eso no lo prueba. ¿Dónde está Dispose: t2? :)
Piotr Perak
1
La pregunta es sobre la eliminación del primer recurso en la lista de usuarios, no el segundo. "¿Qué sucede si se font4 = new Fontlanza? Por lo que tengo entendido, font3 filtrará recursos y no se eliminarán".
wdosanjos