2009-11-18 12 views
10

Estaba haciendo una clase RAII que toma un control System.Windows.Form y establece su cursor. Y en el destructor coloca el cursor nuevamente en lo que era.¿Es seguro usar RAII en C#? Y otros lenguajes de recolección de basura?

¿Pero esta es una mala idea? ¿Puedo confiar en que se llamará al destructor cuando los objetos de esta clase salgan del alcance?

+2

FYI, C# no tiene destructores en el mismo sentido que C++. Tiene finalizadores, pero se llaman en el momento de la recolección de elementos no utilizados, no cuando el objeto queda fuera del alcance. –

Respuesta

19

Esta es una idea muy, muy mala.

finalizadores son no llamada cuando las variables salir de su ámbito. Se llaman en algún momento antes de que el objeto sea basura recolectada, lo que podría ser mucho tiempo después.

En su lugar, se desea implementar IDisposable, y luego las personas que llaman pueden utilizar:

using (YourClass yc = new YourClass()) 
{ 
    // Use yc in here 
} 

que llamará Dispose automáticamente.

Los finalizadores son muy raramente necesarios en C#; solo son necesarios cuando usted directamente posee un recurso no administrado (por ejemplo, un identificador de Windows). De lo contrario, normalmente tiene alguna clase de contenedor gestionado (por ejemplo, FileStream) que tendrá un finalizador si lo necesita.

Tenga en cuenta que sólo se necesita ninguna de esto cuando usted tiene los recursos para ser limpiado - la mayoría de las clases de .NET No aplicar IDisposable.

EDIT: sólo para responder a los comentarios acerca de anidación, estoy de acuerdo que puede ser un poco feo, pero que no es necesario using declaraciones muy a menudo en mi experiencia. También puede usings nido verticalmente como éste, en el caso de que usted tiene dos directamente adyacentes:

using (TextReader reader = File.OpenText("file1.txt")) 
using (TextWriter writer = File.CreateText("file2.txt")) 
{ 
    ... 
} 
+0

¿Está usando 'usar' lo suficiente para garantizar que se llame al destructor como @JaredPar se sugiere a continuación? Si lo garantiza, ¿por qué implementaría IDispose en su lugar? –

+0

por IDspose Quise decir IDisposable –

+0

@Net Citizen - no, no llamará al destructor/finalizador. Simplemente llamará a IDisposable. No afecta la finalización (a menos que llame a GC.SuppressFinalize en Dispose por supuesto!) –

1

utilizar el patrón IDisposable si quieres hacer cosas RAII-como en C#

11

Ya sabes, mucha gente inteligente dice "use IDisposable si quiere implementar RAII en C#", y simplemente no lo compro. Sé que soy una minoría aquí, pero cuando veo "using (blah) {foo (blah);}" automáticamente pienso "blah contiene un recurso no administrado que debe limpiarse agresivamente tan pronto como foo termine (o throws) para que otra persona pueda usar ese recurso ". No creo que "blah no contenga nada interesante, pero hay un poco de semánticamente mutación importante que debe suceder, y vamos a representar esa operación semánticamente importante del personaje '}'": se debe realizar alguna mutación como alguna pila reventado o alguna bandera tiene que ser reiniciado o lo que sea.

Digo que si tiene una operación semánticamente importante que tiene que hacerse cuando algo se completa, tenemos una palabra para eso y la palabra es "finalmente". Si la operación es importante, entonces se debe representar como una declaración que se puede ver allí y poner un punto de interrupción, no un efecto secundario invisible de un corsé derecho.

Así que pensemos en su operación particular.Que desea representar:

var oldPosition = form.CursorPosition; 
form.CursorPosition = newPosition; 
blah; 
blah; 
blah; 
form.CursorPosition = oldPosition; 

Ese código es perfectamente claro . Todos los efectos secundarios están ahí, visibles para el programador de mantenimiento que quiere entender qué está haciendo su código.

Ahora tiene un punto de decisión. ¿Qué pasa si blah lanza? Si blah arroja entonces sucedió algo inesperado. Ahora no tienes ni idea de qué es la "forma" de estado; podría haber sido un código en "forma" que arrojó. Pudo haber estado a la mitad de alguna mutación y ahora está en un estado completamente loco.

Teniendo en cuenta esa situación, ¿qué quieres hacer?

1) Señale el problema a otra persona. Hacer nada. Espero que alguien más en la pila de llamadas sepa cómo manejar esta situación. Motivo de que el formulario ya está en mal estado; el hecho de que su cursor no está en el lugar correcto es la menor de sus preocupaciones. No toque algo que ya es tan frágil, se informó una excepción una vez.

2) Restablezca el cursor en un bloque finally y luego informe el problema a alguien más. Espero, sin ninguna evidencia de que se realice su esperanza, que restablecer el cursor en un formulario que sabe que está en un estado frágil no arroje una excepción. Porque, ¿qué pasa en ese caso? La excepción original, que tal vez alguien sabía cómo manejar, se pierde. Tiene información destruida sobre la causa original del problema, información que podría haber sido útil. Y lo ha reemplazado con otra información sobre un fallo de movimiento del cursor que no sirve a nadie.

3) Escriba la lógica compleja que maneja los problemas de (2): capte la excepción y luego intente restablecer la posición del cursor en un bloque try-catch-finally separado que suprima la nueva excepción y vuelva a lanzar el excepción original. Puede ser complicado acertar, pero puedes hacerlo.

Francamente, mi creencia es que la respuesta correcta es casi siempre (1). Si algo horrible salió mal, entonces no se puede razonar con seguridad sobre qué otras mutaciones al estado frágil son legales. Si no sabes cómo manejar la excepción, DÉ EN VUELTA.

(2) es la solución que te ofrece RAII con un bloque de uso. Una vez más, la razón por la que tenemos que usar bloques en primer lugar es para limpiar de manera agresiva los recursos vitales cuando ya no se pueden usar. Independientemente de si se produce una excepción o no, esos recursos deben limpiarse rápidamente, por lo que los bloques "que usan" tienen una semántica "finalmente". Pero la semántica "finalmente" no es necesariamente apropiada para operaciones que no son operaciones de limpieza de recursos; como hemos visto, los bloques finalmente cargados de semántica de programa suponen implícitamente que siempre es seguro realizar la limpieza; pero el hecho de que estamos en una situación de manejo de excepción indica que podría no ser seguro.

(3) parece mucho trabajo.

Así que, en resumen, digo que dejes de intentar ser tan inteligente. Desea mutar el cursor, hacer algo de trabajo y quitar el mudo del cursor. Entonces escribe tres líneas de código que hagan eso, listo. No se necesitan pantalones RAII de fantasía. simplemente agrega una indirección innecesaria, hace que sea más difícil leer el programa, y ​​lo hace potencialmente más frágil en situaciones excepcionales, no menos frágil.

+5

'finalmente' es un desorden. Lo que la gente quiere es una forma concisa de expresar RAII. El diseño de C#/.NET simplemente no tuvo en cuenta la enorme utilidad de RAII, en lugar de tratar de proporcionar construcciones especiales para casos especiales ('lock',' using'). Por esa razón, las personas intentan no depender de RAII en .NET, pero a veces es la manera más natural de expresar una operación emparejada. –

+0

Pero RAII con frecuencia * está mal * cuando se usa para administrar operaciones semánticas en lugar de la limpieza de recursos. La suposición de que la mutación semántica en el destructor es siempre lo correcto, incluso en una situación excepcional, no está garantizada; en una situación excepcional * si no sabes qué es lo correcto, no intentes *. Lo empeorarás. –

+2

Estoy de acuerdo con @Konrad. Además, encuentro 'finalmente' bastante antinatural. Si tiene una declaración de devolución dentro de su intento que sale de su función, la mayoría de la gente espera que el código regrese de inmediato. Pero, en cambio, saltará al final y luego regresará. Del mismo modo, si está mirando un bloque finally, espera que la siguiente línea se ejecute fuera del final, pero en el escenario descrito anteriormente, a veces también puede regresar. Esto es como ir pero con código oculto. –

5

Edit: Claramente Eric y yo estamos en un desacuerdo sobre este uso.: O

Puede usar algo como esto:

public sealed class CursorApplier : IDisposable 
{ 
    private Control _control; 
    private Cursor _previous; 

    public CursorApplier(Control control, Cursor cursor) 
    { 
     _control = control; 
     _previous = _control.Cursor; 
     ApplyCursor(cursor); 
    } 

    public void Dispose() 
    { 
     ApplyCursor(_previous); 
    } 

    private void ApplyCursor(Cursor cursor) 
    { 
     if (_control.Disposing || _control.IsDisposed) 
      return; 

     if (_control.InvokeRequired) 
      _control.Invoke(new MethodInvoker(() => _control.Cursor = cursor)); 
     else 
      _control.Cursor = cursor; 
    } 
} 

// then... 

using (new CursorApplier(control, Cursors.WaitCursor)) 
{ 
    // some work here 
} 
+3

De los comentarios a la respuesta de Eric: "La declaración de uso, por otro lado, es mucho más limpia que la RIAA" - No estoy de acuerdo. Si bien 'using' ciertamente es más * explícito *, RAII es una expresión tan establecida en C++ o (digamos) VB6 que su uso se ha convertido en una expectativa en lugar de una acción oculta inesperada. Siempre que el destructor del objeto no viole el principio de menor sorpresa haciendo algo completamente inapropiado, indocumentado e impredecible, su uso es tan claro y limpio como 'using', y menos detallado (ya que * cada * bloque es implícito' using' sobre sus variables de pila local) –

Cuestiones relacionadas