c# exception-handling raii

c# - ¿Es abusivo usar IDisposable y "usar" como un medio para obtener un "comportamiento de alcance" para la seguridad de excepciones?



exception-handling raii (12)

Considero que esto es un abuso de la declaración de uso. Soy consciente de que soy una minoría en este puesto.

Considero que esto es un abuso por tres razones.

Primero, porque espero que "usar" se use para usar un recurso y deshacerse de él cuando haya terminado con él . Cambiar el estado del programa no es usar un recurso y volver a cambiarlo no es deshacerse de nada. Por lo tanto, "usar" para mutar y restaurar el estado es un abuso; el código es engañoso para el lector casual.

Segundo, porque espero que "usar" se use por cortesía, no por necesidad . La razón por la que usa "usar" para deshacerse de un archivo cuando termina con esto no es porque sea necesario hacerlo, sino porque es cortés : alguien más podría estar esperando usar ese archivo, por lo que dice "hecho ahora "es lo moralmente correcto para hacer. Espero que pueda refactorizar un "uso" para que el recurso utilizado se mantenga durante más tiempo, y se elimine más tarde, y que el único impacto de hacerlo sea incomodar levemente a otros procesos . Un bloque de "uso" que tiene un impacto semántico en el estado del programa es abusivo porque oculta una mutación importante y requerida del estado del programa en un constructo que parece que está ahí por conveniencia y cortesía, no por necesidad.

Y tercero, las acciones de su programa están determinadas por su estado; la necesidad de una cuidadosa manipulación del estado es precisamente la razón por la que estamos teniendo esta conversación en primer lugar. Consideremos cómo podríamos analizar su programa original.

Si llevara esto a una revisión de código en mi oficina, la primera pregunta que haría sería "¿es realmente correcto bloquear el problema si se lanza una excepción?" Es evidentemente obvio de su programa que esta cosa vuelve a bloquear agresivamente la conversación sin importar lo que pase. ¿Está bien? Una excepción ha sido arrojada. El programa está en un estado desconocido. No sabemos si Foo, Fiddle o Bar lanzaron, por qué lanzaron o qué mutaciones realizaron a otro estado que no se limpiaron. ¿Puedes convencerme de que en esa terrible situación siempre es lo correcto para volver a cerrar?

Tal vez lo sea, tal vez no lo es. Mi punto es que con el código tal como fue escrito originalmente, el revisor del código sabe hacer la pregunta . Con el código que usa "usar", no sé hacer la pregunta; Supongo que el bloque "usar" asigna un recurso, lo usa por un tiempo y lo elimina cortésmente cuando lo hace, no porque el parche de cierre del bloque "usar" muta mi estado de programa en una circunstancia excepcional cuando arbitrariamente muchos las condiciones de coherencia del estado del programa han sido violadas.

El uso del bloque "usar" para tener un efecto semántico hace que este fragmento de programa:

}

extremadamente significativo. Cuando miro ese paréntesis único, no creo inmediatamente que "ese aparato tenga efectos secundarios que tengan un impacto de largo alcance en el estado global de mi programa". Pero cuando abusas de "usar" de esta manera, de repente lo hace.

La segunda cosa que preguntaría si vi el código original es "¿Qué sucede si se lanza una excepción después del Desbloqueo pero antes de que se ingrese el intento?" Si está ejecutando un ensamblado no optimizado, el compilador podría haber insertado una instrucción no-operativa antes de la prueba, y es posible que ocurra una excepción de aborto de subprocesos en el no-op. Esto es raro, pero ocurre en la vida real, particularmente en servidores web. En ese caso, el desbloqueo ocurre pero el bloqueo nunca ocurre, porque la excepción se lanzó antes del intento. Es muy posible que este código sea vulnerable a este problema, y ​​en realidad debería escribirse

bool needsLock = false; try { // must be carefully written so that needsLock is set // if and only if the unlock happened: this.Frobble.AtomicUnlock(ref needsLock); blah blah blah } finally { if (needsLock) this.Frobble.Lock(); }

Nuevamente, tal vez sí lo haga, tal vez no, pero sé que debo hacerte la pregunta . Con la versión "usar", es susceptible al mismo problema: podría lanzarse una excepción de aborto de hilo después de que el Frobble esté bloqueado pero antes de que se ingrese la región de prueba protegida asociada con el uso. Pero con la versión "usar", supongo que este es un "¿y qué?" situación. Es desafortunado si eso sucede, pero supongo que el "uso" solo está ahí para ser cortés, no para mutar un estado del programa vitalmente importante. Supongo que si se produce una excepción de interrupción de un hilo terrible en el momento incorrecto, entonces, bueno, el recolector de basura limpiará ese recurso eventualmente ejecutando el finalizador.

Algo que solía usar en C ++ era dejar que una clase A manejara una condición de entrada y salida del estado para otra clase B , a través del constructor y el destructor A, para asegurarse de que si algo en ese alcance lanzaba una excepción, B tendría un indicar cuándo se salió del alcance. Esto no es puro RAII en lo que respecta al acrónimo, pero es un patrón establecido, sin embargo.

En C #, a menudo quiero hacer

class FrobbleManager { ... private void FiddleTheFrobble() { this.Frobble.Unlock(); Foo(); // Can throw this.Frobble.Fiddle(); // Can throw Bar(); // Can throw this.Frobble.Lock(); } }

Que debe hacerse así

private void FiddleTheFrobble() { this.Frobble.Unlock(); try { Foo(); // Can throw this.Frobble.Fiddle(); // Can throw Bar(); // Can throw } finally { this.Frobble.Lock(); } }

si quiero garantizar el estado de FiddleTheFrobble cuando vuelva FiddleTheFrobble . El código sería más agradable con

private void FiddleTheFrobble() { using (var janitor = new FrobbleJanitor(this.Frobble)) { Foo(); // Can throw this.Frobble.Fiddle(); // Can throw Bar(); // Can throw } }

donde FrobbleJanitor ve más o menos como

class FrobbleJanitor : IDisposable { private Frobble frobble; public FrobbleJanitor(Frobble frobble) { this.frobble = frobble; this.frobble.Unlock(); } public void Dispose() { this.frobble.Lock(); } }

Y así es como quiero hacerlo. Ahora la realidad se pone al día, ya que lo que quiero usar requiere que FrobbleJanitor se use con el using . Podría considerar esto como un problema de revisión del código, pero algo me molesta.

Pregunta: ¿Se consideraría lo anterior como un uso abusivo del using y el IDisposable ?


Creo que la respuesta a su pregunta es no, esto no sería un abuso de IDisposable .

La forma en que entiendo la interfaz IDisposable es que no debes trabajar con un objeto una vez que ha sido eliminado (excepto que puedes llamar a su método Dispose tantas veces como quieras).

Dado que crea un nuevo FrobbleJanitor explícitamente cada vez que accede a la instrucción using , nunca utiliza el mismo objeto FrobbeJanitor dos veces. Y dado que su propósito es administrar otro objeto, Dispose parece apropiado para la tarea de liberar este recurso ("administrado").

(Por cierto, el código de muestra estándar que demuestra la implementación adecuada de Dispose casi siempre sugiere que los recursos administrados también deberían liberarse, no solo los recursos no administrados, como los identificadores del sistema de archivos).

Lo único que personalmente me preocupa es que no está tan claro qué está sucediendo con el using (var janitor = new FrobbleJanitor()) que con un try..finally más explícito ... por try..finally bloquear donde las operaciones de Lock y Unlock están directamente visibles. Pero qué enfoque se toma probablemente se reduce a una cuestión de preferencias personales.


Creo que lo hiciste bien. Overloading Dispose () sería un problema, la misma clase más tarde tuvo la limpieza que realmente tenía que hacer, y la vida útil de esa limpieza cambió para ser diferente al momento en que espera mantener un bloqueo. Pero ya que creó una clase separada (FrobbleJanitor) que es responsable solo de bloquear y desbloquear el Frobble, las cosas están desacopladas lo suficiente como para no afectar ese problema.

Sin embargo, cambiaría el nombre de FrobbleJanitor, probablemente a algo así como FrobbleLockSession.


Eric Gunnerson, que estaba en el equipo de diseño del lenguaje C #, dio esta respuesta a la misma pregunta:

Doug pregunta:

re: una instrucción de bloqueo con tiempo de espera ...

He hecho este truco antes para tratar patrones comunes en numerosos métodos. Por lo general, bloquear la adquisición , pero hay algunos otros . El problema es que siempre se siente como un hack ya que el objeto no es realmente desechable sino " call-back-at-the-end-of-a-scope-able ".

Doug,

Cuando decidimos [sic] el enunciado de uso, decidimos llamarlo "uso" en lugar de algo más específico para deshacerse de objetos, de modo que se pudiera usar exactamente para este escenario.


Es una pendiente resbaladiza. IDisposable tiene un contrato, uno respaldado por un finalizador. Un finalizador es inútil en su caso. No puede obligar al cliente a usar la instrucción de uso, solo anímelo a hacerlo. Puedes forzarlo con un método como este:

void UseMeUnlocked(Action callback) { Unlock(); try { callback(); } finally { Lock(); } }

Pero eso tiende a ser un poco incómodo sin lamdas. Dicho eso, he usado IDisposable como lo hiciste.

Sin embargo, hay un detalle en su publicación que lo hace peligrosamente cercano a un anti-patrón. Mencionaste que esos métodos pueden arrojar una excepción. Esto no es algo que la persona que llama puede ignorar. Él puede hacer tres cosas al respecto:

  • No hacer nada, la excepción no es recuperable. El caso normal. Llamar Desbloquear no importa.
  • Atrapa y maneja la excepción
  • Restaure el estado en su código y permita que la excepción pase por alto la cadena de llamadas.

Los dos últimos requieren que el que llama escribe explícitamente un bloque try. Ahora la declaración de uso se interpone en el camino. Bien puede inducir a un cliente a un estado de coma que le haga creer que su clase se está ocupando del estado y no es necesario realizar ningún trabajo adicional. Eso casi nunca es exacto.


No es abusivo. Los estás usando para lo que fueron creados. Pero es posible que deba considerar uno sobre otro de acuerdo a sus necesidades. Por ejemplo, si opta por ''arte'', entonces puede usar ''usar'', pero si su fragmento de código se ejecuta muchas veces, entonces, por razones de rendimiento, puede utilizar las construcciones ''probar'' ... ''finalmente''. Porque ''usar'' usualmente involucra creaciones de un objeto.


No lo creo, necesariamente. IDisposable técnicamente está destinado a ser usado para cosas que tienen recursos no administrados, pero luego la directiva using es solo una manera ordenada de implementar un patrón común de try .. finally { dispose } .

Un purista argumentaría ''sí, es abusivo'', y en el sentido purista lo es; pero la mayoría de nosotros no codificamos desde una perspectiva purista, sino desde una perspectiva semiartística. Usar el constructo ''usar'' de esta manera es bastante artístico, en mi opinión.

Probablemente deberías agregar otra interfaz encima de IDisposable para empujarla un poco más lejos, explicando a otros desarrolladores por qué esa interfaz implica IDisposable.

Hay muchas otras alternativas para hacer esto, pero, en última instancia, no puedo pensar en ninguna que sea tan buena como esta, ¡así que adelante!


Partiendo de esto: estoy de acuerdo con la mayoría de aquí en que esto es frágil, pero útil. Me gustaría System.Transaction.TransactionScope a la clase System.Transaction.TransactionScope , que hace algo como lo que quieres hacer.

Generalmente me gusta la sintaxis, elimina mucho desorden de la carne real. Por favor, considere darle a la clase de ayuda un buen nombre, tal vez ... Alcance, como el ejemplo anterior. El nombre debe revelar que encapsula una pieza de código. * Ámbito, * Bloque o algo similar debería hacerlo.


Si solo quieres un código limpio y con alcance, también puedes usar lambdas, á la

myFribble.SafeExecute(() => { myFribble.DangerDanger(); myFribble.LiveOnTheEdge(); });

donde el .SafeExecute(Action fribbleAction) envuelve el bloque try - catch - finally .


Tenemos mucho uso de este patrón en nuestra base de códigos, y lo he visto por todas partes antes. Estoy seguro de que también se debatió aquí. En general, no veo qué hay de malo en hacer esto, proporciona un patrón útil y no causa ningún daño real.


Un ejemplo del mundo real es el BeginForm de ASP.net MVC. Básicamente puedes escribir:

Html.BeginForm(...); Html.TextBox(...); Html.EndForm();

o

using(Html.BeginForm(...)){ Html.TextBox(...); }

Html.EndForm llama a Dispose y Dispose simplemente genera la etiqueta </form> . Lo bueno de esto es que los {} corchetes crean un "alcance" visible que hace que sea más fácil ver lo que está dentro del formulario y lo que no.

No lo usaría demasiado, pero esencialmente IDisposable es solo una forma de decir "TIENES que llamar a esta función cuando hayas terminado con ella". MvcForm lo usa para asegurarse de que el formulario esté cerrado, Stream lo usa para asegurarse de que la transmisión esté cerrada, puede usarlo para asegurarse de que el objeto esté desbloqueado.

Personalmente, solo lo usaría si las dos reglas siguientes son verdaderas, pero las establezco arbitrariamente:

  • Dispose debe ser una función que siempre debe ejecutarse, por lo que no debe haber ninguna condición excepto Null-Checks
  • Después de Dispose (), el objeto no debe ser reutilizable. Si quisiera un objeto reutilizable, prefiero darle los métodos de apertura / cierre en lugar de eliminarlo. Entonces tiro una InvalidOperationException cuando trato de usar un objeto eliminado.

Al final, todo se trata de expectativas. Si un objeto implementa IDisposable, supongo que necesita hacer algo de limpieza, así que lo llamo. Creo que generalmente es mejor que tener una función de "Apagado".

Dicho esto, no me gusta esta línea:

this.Frobble.Fiddle();

Como el FrobbleJanitor "posee" el Frobble ahora, me pregunto si no sería mejor llamar a Fiddle en el Frobble en el conserje.


Nota: Mi punto de vista probablemente esté sesgado desde mi fondo C ++, por lo que el valor de mi respuesta debe ser evaluado contra ese posible sesgo ...

¿Qué dice la especificación del lenguaje C #?

Citando la especificación del lenguaje C # :

8.13 La declaración que usa

[...]

Un recurso es una clase o estructura que implementa System.IDisposable, que incluye un único método sin parámetros llamado Dispose. El código que usa un recurso puede llamar a Dispose para indicar que el recurso ya no es necesario. Si no se llama a Dispose, la eliminación automática se produce como consecuencia de la recolección de basura.

El Código que está utilizando un recurso es, por supuesto, el código que comienza por la palabra clave using y va hasta el ámbito adjunto al using .

Así que supongo que esto está bien porque el bloqueo es un recurso.

Quizás la palabra clave using fue mal elegida. Tal vez debería haber sido llamado scoped .

Entonces, podemos considerar casi cualquier cosa como un recurso. Un manejador de archivo. Una conexión de red ... ¿Un hilo?

¿¿¿Un hilo???

¿Usar (o abusar) la palabra clave using ?

¿Sería brillante utilizar (ab) la palabra clave using para asegurarse de que el trabajo del hilo finaliza antes de salir del alcance?

Herb Sutter parece pensar que es brillante , ya que ofrece un uso interesante del patrón IDispose para esperar a que finalice el trabajo de un hilo:

http://www.drdobbs.com/go-parallel/article/showArticle.jhtml?articleID=225700095

Aquí está el código, copiado y pegado del artículo:

// C# example using( Active a = new Active() ) { // creates private thread … a.SomeWork(); // enqueues work … a.MoreWork(); // enqueues work … } // waits for work to complete and joins with private thread

Aunque no se proporciona el código C # para el objeto activo, se escribe que C # usa el patrón IDispose para el código cuya versión de C ++ está contenida en el destructor. Y al observar la versión de C ++, vemos un destructor que espera que el subproceso interno finalice antes de salir, como se muestra en este otro extracto del artículo:

~Active() { // etc. thd->join(); }

Entonces, en lo que respecta a Herb, es brillante .