Patrón desechable (Principio de diseño desechable) pt.3


Multithreading


Ahora hablemos de hielo delgado. En las secciones anteriores sobre IDisposable tocamos un concepto muy importante que subyace no solo a los principios de diseño de los tipos desechables, sino a cualquier tipo en general. Este es el concepto de integridad del objeto. Significa que en cualquier momento dado un objeto está en un estado estrictamente determinado y cualquier acción con este objeto convierte su estado en una de las opciones predeterminadas al diseñar un tipo de este objeto. En otras palabras, ninguna acción con el objeto debería convertirlo en un estado indefinido. Esto da como resultado un problema con los tipos diseñados en los ejemplos anteriores. No son seguros para subprocesos. Existe la posibilidad de que se invoquen los métodos públicos de este tipo cuando la destrucción de un objeto esté en progreso. Solucionemos este problema y decidamos si debemos resolverlo.


Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.

Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio github / sidristij / dotnetbook .

public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; object _disposingSync = new object(); public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Seek(int position) { lock(_disposingSync) { CheckDisposed(); // Seek API call } } public void Dispose() { lock(_disposingSync) { if(_disposed) return; _disposed = true; } InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { lock(_disposingSync) { if(_disposed) { throw new ObjectDisposedException(); } } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods } 

El código de validación _disposed en Dispose () debe inicializarse como una sección crítica. De hecho, todo el código de métodos públicos debe inicializarse como una sección crítica. Esto resolverá el problema del acceso concurrente a un método público de un tipo de instancia y a un método de su destrucción. Sin embargo, trae otros problemas que se convierten en una bomba de tiempo:


  • El uso intensivo de los métodos de instancia de tipo, así como la creación y destrucción de objetos reducirá significativamente el rendimiento. Esto se debe a que tomar un candado consume tiempo. Este tiempo es necesario para asignar tablas SyncBlockIndex, verificar el subproceso actual y muchas otras cosas (las trataremos en el capítulo sobre subprocesamiento múltiple). Eso significa que tendremos que sacrificar el rendimiento del objeto a lo largo de su vida útil durante la "última milla" de su vida.
  • Tráfico de memoria adicional para objetos de sincronización.
  • Pasos adicionales que GC debe tomar para recorrer un gráfico de objetos.

Ahora, nombremos el segundo y, en mi opinión, lo más importante. Permitimos la destrucción de un objeto y al mismo tiempo esperamos trabajar con él nuevamente. ¿Qué esperamos en esta situación? que va a fallar? Porque si Dispose se ejecuta primero, entonces el siguiente uso de métodos de objeto definitivamente dará como resultado ObjectDisposedException . Por lo tanto, debe delegar la sincronización entre llamadas Dispose () y otros métodos públicos de un tipo al lado del servicio, es decir, al código que creó la instancia de la clase FileWrapper . Es porque solo el lado creador sabe lo que hará con una instancia de una clase y cuándo destruirla. Por otro lado, una llamada Dispose debería producir solo errores críticos, como OutOfMemoryException , pero no IOException, por ejemplo. Esto se debe a los requisitos para la arquitectura de clases que implementan IDisposable. Significa que si se llama a Dispose desde más de un hilo a la vez, la destrucción de una entidad puede ocurrir desde dos hilos simultáneamente if(_disposed) return; la comprobación de if(_disposed) return; ). Depende de la situación: si un recurso se puede liberar varias veces, no hay necesidad de verificaciones adicionales. De lo contrario, la protección es necesaria:


 // I don't show the whole pattern on purpose as the example will be too long // and will not show the essence class Disposable : IDisposable { private volatile int _disposed; public void Dispose() { if(Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { // dispose } } } 

Dos niveles de principio de diseño desechable


¿Cuál es el patrón más popular para implementar IDisposable que puedes encontrar en libros de .NET e Internet? ¿Qué patrón se espera de usted durante las entrevistas para un nuevo trabajo potencial? Probablemente este:


 public class Disposable : IDisposable { bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if(disposing) { // here we release managed resources } // here we release unmanaged resources } protected void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } ~Disposable() { Dispose(false); } } 

¿Qué tiene de malo este ejemplo y por qué no hemos escrito así antes? De hecho, este es un buen patrón adecuado para todas las situaciones. Sin embargo, su uso ubicuo no es un buen estilo en mi opinión, ya que casi no tratamos con recursos no administrados en la práctica, lo que hace que la mitad del patrón no sirva para nada. Además, dado que gestiona simultáneamente recursos gestionados y no gestionados, viola el principio de división de responsabilidad. Creo que esto está mal. Veamos un enfoque ligeramente diferente. Principio de diseño desechable . En resumen, funciona de la siguiente manera:


La eliminación se divide en dos niveles de clases:


  • Los tipos de nivel 0 encapsulan directamente los recursos no administrados
    • Son abstractos o empaquetados.
    • Todos los métodos deben estar marcados:
      - PrePrepareMethod, para poder compilar un método al cargar un tipo
      • SecuritySafeCritical para proteger contra una llamada del código, trabajando bajo restricciones
      • ReliabilityContract (Consistency.WillNotCorruptState, Cer.Success / MayFail)] para colocar CER para un método y todas sus llamadas secundarias
        - Pueden hacer referencia a los tipos de Nivel 0, pero deben incrementar el contador de objetos de referencia para garantizar el orden correcto de entrar en la "última milla"
  • Los tipos de nivel 1 encapsulan solo los recursos administrados
    • Se heredan solo de los tipos de Nivel 1 o implementan directamente IDisposable
    • No pueden heredar tipos de Nivel 0 o CriticalFinalizerObject
    • Pueden encapsular tipos administrados de Nivel 1 y Nivel 0
    • Implementan IDisposable. Deseche destruyendo objetos encapsulados comenzando desde los tipos de Nivel 0 y pasando al Nivel 1
    • No implementan un finalizador ya que no manejan recursos no administrados
    • Deben contener una propiedad protegida que dé acceso a los tipos de Nivel 0.

Es por eso que utilicé la división en dos tipos desde el principio: el que contiene un recurso administrado y el que tiene un recurso no administrado. Deberían funcionar de manera diferente.


Otras formas de usar Dispose


La idea detrás de la creación de IDisposable era liberar recursos no administrados. Pero, como ocurre con muchos otros patrones, es muy útil para otras tareas, por ejemplo, publicar referencias a recursos administrados. Aunque liberar recursos administrados no suena muy útil. Quiero decir que se llaman administrados a propósito, por lo que nos relajaríamos con una sonrisa con respecto a los desarrolladores de C / C ++, ¿verdad? Sin embargo, no es así. Siempre puede haber una situación en la que perdemos una referencia a un objeto pero al mismo tiempo pensamos que todo está bien: GC recogerá basura, incluido nuestro objeto. Sin embargo, resulta que la memoria crece. Entramos en el programa de análisis de memoria y vemos que algo más contiene este objeto. La cuestión es que puede haber una lógica para la captura implícita de una referencia a su entidad tanto en la plataforma .NET como en la arquitectura de clases externas. Como la captura es implícita, un programador puede perder la necesidad de su lanzamiento y luego obtener una pérdida de memoria.


Delegados, eventos


Veamos este ejemplo sintético:


 class Secondary { Action _action; void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action(); } } class Primary { Secondary _foo = new Secondary(); public void PlanSayHello() { _foo.SaveForUseInFuture(Strategy); } public void SayHello() { _foo.CallAction(); } void Strategy() { Console.WriteLine("Hello!"); } } 

¿Qué problema muestra este código? La clase secundaria almacena el tipo de Action delegado en el campo _action que se acepta en el método SaveForUseInFuture . A continuación, el método PlanSayHello dentro de Primary clase Primary pasa el puntero al método de Strategy a Secondary clase Secondary . Es curioso, pero si, en este ejemplo, pasa a algún lugar un método estático o un método de instancia, el SaveForUseInFuture pasado no se cambiará, pero una instancia de clase Primary se referenciará implícitamente o no se hará ninguna referencia. Exteriormente parece que le indicó a qué método llamar. Pero, de hecho, un delegado se construye no solo usando un puntero de método sino también usando el puntero a una instancia de una clase. ¡Una parte que llama debe entender para qué instancia de una clase tiene que llamar el método de Strategy ! Esa es la instancia de Secondary clase Secondary ha aceptado implícitamente y mantiene el puntero a la instancia de Primary clase Primary , aunque no se indica explícitamente. Para nosotros, solo significa que si pasamos el puntero _foo otro lugar y perdemos la referencia a Primary , GC no recogerá el objeto Primary , ya que Secondary lo mantendrá. ¿Cómo podemos evitar tales situaciones? Necesitamos un enfoque determinado para publicar una referencia para nosotros. Un mecanismo que se adapta perfectamente a este propósito es IDisposable


 // This is a simplified implementation class Secondary : IDisposable { Action _action; public event Action<Secondary> OnDisposed; public void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action?.Invoke(); } void Dispose() { _action = null; OnDisposed?.Invoke(this); } } 

Ahora el ejemplo parece aceptable. Si se pasa una instancia de una clase a un tercero y la referencia al delegado de _action se perderá durante este proceso, la estableceremos en cero y se notificará al tercero sobre la destrucción de la instancia y eliminaremos la referencia a la misma. .
El segundo peligro del código que se ejecuta en los delegados son los principios de funcionamiento del event . Veamos en qué resultan:


  // a private field of a handler private Action<Secondary> _event; // add/remove methods are marked as [MethodImpl(MethodImplOptions.Synchronized)] // that is similar to lock(this) public event Action<Secondary> OnDisposed { add { lock(this) { _event += value; } } remove { lock(this) { _event -= value; } } } 

La mensajería de C # oculta el funcionamiento interno de los eventos y contiene todos los objetos que se suscribieron para actualizar a través del event . Si algo sale mal, una referencia a un objeto firmado permanece en OnDisposed y retendrá el objeto. Es una situación extraña ya que en términos de arquitectura obtenemos un concepto de "fuente de eventos" que no debería contener nada lógicamente. Pero, de hecho, los objetos suscritos a la actualización se mantienen implícitamente. Además, no podemos cambiar algo dentro de este conjunto de delegados aunque la entidad nos pertenezca. Lo único que podemos hacer es eliminar esta lista asignando nulo a una fuente de eventos.


La segunda forma es implementar métodos de add / remove explícitamente, para que podamos controlar una colección de delegados.


Otra situación implícita puede aparecer aquí. Puede parecer que si asigna nulo a una fuente de eventos, la siguiente suscripción a eventos causará NullReferenceException . Creo que esto sería más lógico.

Sin embargo, esto no es cierto. Si el código externo se suscribe a eventos después de que se borra un origen de eventos, FCL creará una nueva instancia de clase de Acción y la almacenará en OnDisposed . Esta implicidad en C # puede engañar a un programador: tratar con campos anulados debería producir una especie de alerta en lugar de calma. Aquí también demostramos un enfoque cuando el descuido de un programador puede provocar pérdidas de memoria.


Cierres lambdas


Usar azúcar sintáctico como lambdas es especialmente peligroso.


Me gustaría referirme al azúcar sintáctico en su conjunto. Creo que debería usarlo con bastante cuidado y solo si conoce el resultado exactamente. Ejemplos con expresiones lambda son los cierres, los cierres en Expresiones y muchas otras miserias que puedes infligirte a ti mismo.

Por supuesto, puede decir que sabe que una expresión lambda crea un cierre y puede provocar un riesgo de pérdida de recursos. Pero es tan ordenado, tan agradable que es difícil evitar usar lambda en lugar de asignar todo el método, que se describirá en un lugar diferente de donde se usará. De hecho, no debes aceptar esta provocación, aunque no todos pueden resistirse. Veamos el ejemplo:


  button.Clicked += () => service.SendMessageAsync(MessageType.Deploy); 

De acuerdo, esta línea se ve muy segura. Pero oculta un gran problema: ahora la variable del button referencia implícita al service y lo retiene. Incluso si decidimos que ya no necesitamos service , el button aún mantendrá la referencia mientras esta variable esté activa. Una de las formas de resolver este problema es usar un patrón para crear IDisposable desde cualquier Action ( System.Reactive.Disposables ):


 // Here we create a delegate from a lambda Action action = () => service.SendMessageAsync(MessageType.Deploy); // Here we subscribe button.Clicked += action; // We unsubscribe var subscription = Disposable.Create(() => button.Clicked -= action); // where it is necessary subscription.Dispose(); 

Admito, esto parece un poco largo y perdemos todo el propósito de usar expresiones lambda. Es mucho más seguro y sencillo usar métodos privados comunes para capturar variables implícitamente.


Protección contra amenazas


Cuando crea una biblioteca para un desarrollador externo, no puede predecir su comportamiento en una aplicación de terceros. A veces solo puedes adivinar lo que un programador le hizo a tu biblioteca que causó un resultado particular. Un ejemplo es el funcionamiento en un entorno multiproceso cuando la coherencia de la limpieza de recursos puede convertirse en un problema crítico. Tenga en cuenta que cuando escribimos el método Dispose() , podemos garantizar la ausencia de excepciones. Sin embargo, no podemos garantizar que mientras se ejecuta el método Dispose() no se produzca ThreadAbortException que inhabilite nuestro hilo de ejecución. Aquí debemos recordar que cuando se produce ThreadAbortException , todos los bloques catch / finally se ejecutan de todos modos (al final de un bloque catch / finally ThreadAbort se produce más adelante). Por lo tanto, para garantizar la ejecución de un determinado código mediante Thread.Abort, debe ajustar una sección crítica en el try { ... } finally { ... } , consulte el siguiente ejemplo:


 void Dispose() { if(_disposed) return; _someInstance.Unsubscribe(this); _disposed = true; } 

Uno puede abortar esto en cualquier momento usando Thread.Abort . Destruye parcialmente un objeto, aunque aún puede trabajar con él en el futuro. Al mismo tiempo, el siguiente código:


 void Dispose() { if(_disposed) return; // ThreadAbortException protection try {} finally { _someInstance.Unsubscribe(this); _disposed = true; } } 

está protegido de tal aborto y se ejecutará sin problemas y con seguridad, incluso si Thread.Abort aparece entre llamar al método Unsubscribe y ejecutar sus instrucciones.


Resultados


Ventajas


Bueno, aprendimos mucho sobre este patrón más simple. Vamos a determinar sus ventajas:


  1. La principal ventaja del patrón es la capacidad de liberar recursos de manera determinante, es decir, cuando los necesita.
  2. La segunda ventaja es la introducción de una forma comprobada de verificar si una instancia específica requiere destruir sus instancias después de su uso.
  3. Si implementa el patrón correctamente, un tipo diseñado funcionará de manera segura en términos de uso por parte de componentes de terceros, así como en términos de descarga y destrucción de recursos cuando un proceso falla (por ejemplo, por falta de memoria). Esta es la última ventaja.

Desventajas


En mi opinión, este patrón tiene más desventajas que ventajas.


  1. Por un lado, cualquier tipo que implemente este patrón instruye a otras partes que si lo usan, toman una especie de oferta pública. Esto es tan implícito que, como en el caso de las ofertas públicas, un usuario de un tipo no siempre sabe que el tipo tiene esta interfaz. Por lo tanto, debe seguir las indicaciones de IDE (escriba un punto, Dis ... y verifique si hay un método en la lista de miembros filtrados de una clase). Si ve un patrón Dispose, debe implementarlo en su código. A veces no sucede de inmediato y, en este caso, debe implementar un patrón a través de un sistema de tipos que agrega funcionalidad. Un buen ejemplo es que IEnumerator<T> implica IDisposable .
  2. Por lo general, cuando diseña una interfaz, es necesario insertar IDisposable en el sistema de las interfaces de un tipo cuando una de las interfaces tiene que heredar IDisposable. En mi opinión, esto daña las interfaces que diseñamos. Quiero decir, cuando diseñas una interfaz, primero creas un protocolo de interacción. Este es un conjunto de acciones que puede realizar con algo oculto detrás de la interfaz. Dispose() es un método para destruir una instancia de una clase. Esto contradice la esencia de un protocolo de interacción . De hecho, estos son los detalles de implementación que se infiltraron en la interfaz.
  3. A pesar de estar determinado, Dispose () no significa la destrucción directa de un objeto. El objeto seguirá existiendo después de su destrucción, pero en otro estado. Para hacerlo realidad, CheckDisposed () debe ser el primer comando de cada método público. Esto parece una solución temporal que alguien nos dio diciendo: "Ve y multiplica";
  4. También hay una pequeña posibilidad de obtener un tipo que implemente IDisposable través de una implementación explícita . O puede obtener un tipo que implemente ID disponible sin la posibilidad de determinar quién debe destruirlo: usted o la parte que se lo dio. Esto dio como resultado un antipatrón de múltiples llamadas de Dispose () que permite destruir un objeto destruido;
  5. La implementación completa es difícil y es diferente para los recursos administrados y no administrados. Aquí el intento de facilitar el trabajo de los desarrolladores a través de GC parece incómodo. Puede anular el método virtual void Dispose() e introducir algún tipo de DisposableObject que implemente todo el patrón, pero que no resuelva otros problemas relacionados con el patrón;
  6. Como regla, el método Dispose () se implementa al final de un archivo mientras que '.ctor' se declara al principio. Si modifica una clase o introduce nuevos recursos, es fácil olvidar agregar la eliminación para ellos.
  7. Finalmente, es difícil determinar el orden de destrucción en un entorno multiproceso cuando se usa un patrón para gráficos de objetos donde los objetos implementan total o parcialmente ese patrón. Me refiero a situaciones en las que Dispose () puede comenzar en diferentes extremos de un gráfico. Aquí es mejor usar otros patrones, por ejemplo, el patrón Lifetime.
  8. El deseo de los desarrolladores de plataformas de automatizar el control de la memoria combinado con las realidades: las aplicaciones interactúan con el código no administrado muy a menudo + necesita controlar la publicación de referencias a objetos para que Garbage Collector pueda recopilarlos. Esto agrega una gran confusión al comprender preguntas tales como: "¿Cómo debemos implementar un patrón correctamente"? "¿Hay algún patrón confiable en absoluto"? Tal vez llamando a delete obj; delete[] arr; delete obj; delete[] arr; es mas simple?

Descarga de dominio y salida de una aplicación


Si llegaste a esta parte, te volviste más confiado en el éxito de futuras entrevistas de trabajo. Sin embargo, no discutimos todas las preguntas relacionadas con este patrón simple, como puede parecer. La última pregunta es si el comportamiento de una aplicación difiere en caso de recolección de basura simple y cuando se recolecta basura durante la descarga del dominio y al salir de la aplicación. Esta pregunta simplemente toca Dispose() ... Sin embargo, Dispose() y la finalización van de la mano y raramente encontramos una implementación de una clase que tenga finalización pero no tenga el método Dispose() . Entonces, describamos la finalización en una sección separada. Aquí solo agregamos algunos detalles importantes.


Durante la descarga del dominio de la aplicación, usted descarga los ensamblajes cargados en el dominio de la aplicación y todos los objetos que se crearon como parte del dominio que se va a descargar. De hecho, esto significa la limpieza (recopilación por GC) de estos objetos y llamar a los finalizadores para ellos. Si la lógica de un finalizador espera la finalización de otros objetos para ser destruidos en el orden correcto, puede prestar atención a Environment.HasShutdownStarted propiedad Environment.HasShutdownStarted que indica que una aplicación se descarga de la memoria y al método AppDomain.CurrentDomain.IsFinalizingForUnload() que indica que esto el dominio se descarga, razón por la cual se finaliza. Si se producen estos eventos, el orden de finalización de los recursos generalmente no tiene importancia. No podemos retrasar la descarga del dominio o una aplicación, ya que debemos hacer todo lo más rápido posible.


Esta es la forma en que esta tarea se resuelve como parte de una clase LoaderAllocatorScout


 // Assemblies and LoaderAllocators will be cleaned up during AppDomain shutdown in // an unmanaged code // So it is ok to skip reregistration and cleanup for finalization during appdomain shutdown. // We also avoid early finalization of LoaderAllocatorScout due to AD unload when the object was inside DelayedFinalizationList. if (!Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload()) { // Destroy returns false if the managed LoaderAllocator is still alive. if (!Destroy(m_nativeLoaderAllocator)) { // Somebody might have been holding a reference on us via weak handle. // We will keep trying. It will be hopefully released eventually. GC.ReRegisterForFinalize(this); } } 

Fallos de implementación típicos


Como te mostré, no hay un patrón universal para implementar IDisposable. Además, cierta dependencia del control automático de la memoria induce a error a las personas y toman decisiones confusas al implementar un patrón. Todo .NET Framework está plagado de errores en su implementación. Para probar mi punto, veamos estos errores usando el ejemplo de .NET Framework exactamente. Todas las implementaciones están disponibles a través de: Usos IDisposable


FileEntry Class cmsinterop.cs


Este código está escrito a toda prisa solo para cerrar el problema. Obviamente, el autor quería hacer algo, pero cambió de opinión y mantuvo una solución defectuosa.

 internal class FileEntry : IDisposable { // Other fields // ... [MarshalAs(UnmanagedType.SysInt)] public IntPtr HashValue; // ... ~FileEntry() { Dispose(false); } // The implementation is hidden and complicates calling the *right* version of a method. void IDisposable.Dispose() { this.Dispose(true); } // Choosing a public method is a serious mistake that allows for incorrect destruction of // an instance of a class. Moreover, you CANNOT call this method from the outside public void Dispose(bool fDisposing) { if (HashValue != IntPtr.Zero) { Marshal.FreeCoTaskMem(HashValue); HashValue = IntPtr.Zero; } if (fDisposing) { if( MuiMapping != null) { MuiMapping.Dispose(true); MuiMapping = null; } System.GC.SuppressFinalize(this); } } } 

Sistema de clase SemaphoreSlim / Threading / SemaphoreSlim.cs


Este error se encuentra en la parte superior de los errores de .NET Framework con respecto a IDisposable: SuppressFinalize para clases donde no hay finalizador. Es muy común.

 public void Dispose() { Dispose(true); // As the class doesn't have a finalizer, there is no need in GC.SuppressFinalize GC.SuppressFinalize(this); } // The implementation of this pattern assumes the finalizer exists. But it doesn't. // It was possible to do with just public virtual void Dispose() protected virtual void Dispose(bool disposing) { if (disposing) { if (m_waitHandle != null) { m_waitHandle.Close(); m_waitHandle = null; } m_lockObj = null; m_asyncHead = null; m_asyncTail = null; } } 

Llamar a Cerrar + Eliminar algún código de proyecto de NativeWatcher


A veces las personas llaman tanto Cerrar como Desechar. Esto está mal, aunque no producirá un error ya que el segundo Dispose no genera una excepción.

De hecho, Cerrar es otro patrón para aclarar las cosas para las personas. Sin embargo, hizo que todo fuera más claro.


 public void Dispose() { if (MainForm != null) { MainForm.Close(); MainForm.Dispose(); } MainForm = null; } 

Resultados generales


  1. IDposable es un estándar de la plataforma y la calidad de su implementación influye en la calidad de toda la aplicación. Además, en alguna situación influye en la seguridad de su aplicación que puede ser atacada a través de recursos no administrados.
  2. La implementación de IDisposable debe ser máximamente productiva. Esto es especialmente cierto sobre la sección de finalización, que funciona en paralelo con el resto del código, cargando el recolector de basura.
  3. Al implementar IDisposable, no debe usar Dispose () simultáneamente con los métodos públicos de una clase. La destrucción no puede ir junto con el uso. Esto debe tenerse en cuenta al diseñar un tipo que utilizará un objeto IDisposable.
  4. Sin embargo, debe haber una protección contra la llamada 'Dispose ()' desde dos hilos simultáneamente. Esto resulta de la declaración de que Dispose () no debería producir errores.
  5. Los tipos que contienen recursos no administrados deben separarse de otros tipos. Es decir, si envuelve un recurso no administrado, debe asignarle un tipo separado. Este tipo debe contener finalización y debe heredarse de SafeHandle / CriticalHandle / CriticalFinalizerObject . Esta separación de responsabilidades dará como resultado un mejor soporte del sistema de tipos y simplificará la implementación para destruir instancias de tipos a través de Dispose (): los tipos con esta implementación no necesitarán implementar un finalizador.
  6. En general, este patrón no es cómodo de usar, así como en el mantenimiento del código. Probablemente, deberíamos usar el enfoque de Inversión de control cuando destruimos el estado de los objetos a través del patrón Lifetime . Sin embargo, hablaremos de ello en la siguiente sección.

Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.

Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio github / sidristij / dotnetbook .

Source: https://habr.com/ru/post/443962/


All Articles