2010-11-19 15 views
6

Hice una pregunta con respecto a returning a Disposable (IDisposable) object from a function, pero pensé que podría enturbiar la discusión si planteara esta pregunta allí.No entiendo muy bien el funcionamiento del uso/Objetos desechables

creé un código de ejemplo:

class UsingTest 
{ 
    public class Disposable : IDisposable 
    { 
     public void Dispose() 
     { 
      var i = 0; 
      i++; 
     } 
    } 
    public static Disposable GetDisposable(bool error) 
    { 
     var obj = new Disposable(); 
     if (error) 
      throw new Exception("Error!"); 
     return obj; 
    } 
} 

codifiqué esta manera deliberada, porque entonces yo llamo:

using (var tmp = UsingTest.GetDisposable(true)) { } 

Usando el depurador, noto que el método Dispose nunca se ejecuta, incluso aunque ya hemos creado una instancia de un objeto Disposable. Si entiendo correctamente el propósito de Dispose, si esta clase en realidad tuviera mangos abiertos y similares, entonces no los cerraríamos tan pronto como hayamos terminado con ellos.

Hago esta pregunta porque este comportamiento se alinea con lo que esperaría, pero en las respuestas a la pregunta relacionada, las personas parecían indicar que using se haría cargo de todo.

Si using de alguna manera se encarga de todo esto, ¿podría alguien explicar lo que me falta? Pero, si este código podría causar una fuga de recursos, ¿cómo sugeriría que codifique GetDisposable (con la condición de que debo crear una instancia del objeto IDisposable y ejecutar un código que arroje una excepción antes de la declaración de devolución)?

Respuesta

4

Dependiendo de lo que la semántica que desea para GetDisposable, esto es, probablemente, cómo iba a ponerlo en práctica:

public static Disposable GetDisposable(bool error) 
{ 
    var obj = new Disposable(); 

    try 
    { 
     if (error) 
      throw new Exception("Error!"); 

     return obj; 
    } 
    catch (Exception) 
    { 
     obj.Dispose(); 
     throw; 
    } 
} 
11

La razón por la que nunca se llama es por la forma en que lo asigna. La variable "tmp" nunca se asigna en absoluto, porque la función GetDisposable(bool) nunca regresa debido al hecho de que usted lanzó una excepción.

Si se va a decir en su lugar lo siguiente,

using (var tmp = new Disposable()) 
{ 
    throw new ArgumentException("Blah"); 
} 

entonces se vería que IDisposable::Dispose() en efecto, ser llamado.

Lo fundamental es comprender que el bloque using tiene que obtener una referencia válida para el objeto IDisposable. Si se produce alguna excepción para que la variable declarada en el bloque using no se asigne, no tiene suerte, porque el bloque using no tendrá conocimiento del objeto IDisposable.

Como para el retorno de un objeto IDisposable de una función, se debe utilizar un bloque estándar catch interior de la función a llamar Dispose() en el caso de un fallo, pero, obviamente, no se debe utilizar un bloque using porque esto se deshará el objeto antes de que estés listo para hacerlo tú mismo.

+0

es correcto. Si el método que crea el IDisposable puede fallar antes de devolver la referencia, es responsable de garantizar que se invoque el desecho. – ScottS

1

La interfaz IDisposable simplemente garantiza que la clase que lo implementa tiene un método Dispose. No hace nada con respecto a llamar a este método. Un bloque que utiliza invocará Dispose en el objeto cuando se salga del bloque.

+0

Hay una situación en la que 'IDisposable' es opcional, pero si se implementa hará que 'Dispose' se llame automáticamente: si el tipo de devolución de la función 'GetEnumerator' llamado por un C#' foreach' o vb.net 'For Each' la declaración implementa 'IDisposable', o si el tipo de retorno es' IEnumerator' y la instancia que devuelve implementa 'IDisposable', el compilador llamará a' IDisposable.Dispose' una vez completada la enumeración. – supercat

+0

@supercat: eso es bueno saberlo; No me di cuenta de que este era el caso. –

+0

Es un poco complicado. Si el tipo de devolución es una * clase * que no implementa 'IDisposable', pero' GetEnumerator' devuelve una clase derivada que implementa 'IDisposable', no se llamará al método' Dispose', pero si se trata de una interfaz como la "IEnumerator" no genérico, el compilador calculará que el objeto devuelto podría implementar 'IDisposable' y verificarlo en tiempo de ejecución. Un diseño feo, pero como 'Dispose' se ha omitido de' IEnumerator', en realidad no hay ninguna alternativa. – supercat

1

Crea un IDisposable en GetDisposable pero como sale de la función lanzando una excepción, nunca se devuelve y, por lo tanto, tmp nunca se asigna. La instrucción using es una abreviatura de

var tmp = UsingTest.GetDisposable(true); 
try { } 
finally 
{ 
    if(tmp != null) tmp.Dispose(); 
} 

y nunca se llega al bloque try.La solución en su ejemplo es comprobar la bandera error antes de crear el obj desechable:

public static Disposable GetDisposable(bool error) 
{ 
    if (error) 
     throw new Exception("Error!"); 
    return new Disposable(); 
} 
+0

No; eso falla con la condición que coloqué sobre el código. – palswim

3

Este es porque la variable tmp nunca se asigna. Es algo de lo que debe tener cuidado con los objetos desechables. Una mejor definición para GewtDisposable sería:

public static Disposable GetDisposable(bool error) 
{ 
    var obj = new Disposable(); 

    try 
    { 
     if (error) 
      throw new Exception("Error!"); 
     return obj; 
    } 
    catch 
    { 
     obj.Dispose(); 
     throw; 
    } 
} 

Porque asegura que obj está dispuesto.

+0

Una ligera mejora sería copiar la variable de objeto principal a una segunda y anular la primera antes de devolver la segunda, y luego utilizar un bloque "finalmente" en lugar de una "captura" para manejar la eliminación (si la variable es nulo, no lo elimine). Esto asegurará que la excepción aparezca como ocurrida en el lugar apropiado, no en el momento de la reanudación. – supercat

0

Una pregunta relacionada es Handling iDisposable in failed initializer or constructor y creo que la respuesta es que si desea evitar la fuga de objetos desechables de un constructor que ha fallado, tendrá que pasar de contrabando una copia del objeto desde el constructor (por ejemplo, esconderlo en un contenedor pasado, o asignarlo a una variable pasada por referencia) y envolver la llamada del constructor en un bloque catch. Icky, pero no sé cómo hacerlo mejor. VB.net en realidad puede administrar un poco mejor que C# debido a cómo funcionan sus inicializadores.