Padrão descartável (Princípio do projeto descartável) pt.3


Multithreading


Agora vamos falar sobre gelo fino. Nas seções anteriores sobre IDisposable, tocamos em um conceito muito importante subjacente não apenas aos princípios de design de tipos descartáveis, mas a qualquer tipo em geral. Este é o conceito de integridade do objeto. Isso significa que, em um dado momento, um objeto está em um estado estritamente determinado e qualquer ação com esse objeto transforma seu estado em uma das opções pré-determinadas ao projetar um tipo desse objeto. Em outras palavras, nenhuma ação com o objeto deve transformá-lo em um estado indefinido. Isso resulta em um problema com os tipos projetados nos exemplos acima. Eles não são seguros para threads. Há uma chance de os métodos públicos desses tipos serem chamados quando a destruição de um objeto estiver em andamento. Vamos resolver esse problema e decidir se devemos resolvê-lo.


Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.

Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório 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 } 

O código de validação _disposed em Dispose () deve ser inicializado como uma seção crítica. De fato, todo o código de métodos públicos deve ser inicializado como uma seção crítica. Isso resolverá o problema do acesso simultâneo a um método público de um tipo de instância e a um método de destruição. No entanto, traz outros problemas que se tornam uma bomba de tempo:


  • O uso intensivo de métodos de instância de tipo, bem como a criação e destruição de objetos, reduzirão significativamente o desempenho. Isso ocorre porque a trava consome tempo. Esse tempo é necessário para alocar tabelas SyncBlockIndex, verificar o thread atual e muitas outras coisas (trataremos deles no capítulo sobre multithreading). Isso significa que teremos que sacrificar o desempenho do objeto ao longo de sua vida útil pela “última milha” de sua vida.
  • Tráfego de memória adicional para objetos de sincronização.
  • Etapas adicionais que o GC deve seguir para passar por um gráfico de objeto.

Agora, vamos citar o segundo e, na minha opinião, o mais importante. Permitimos a destruição de um objeto e, ao mesmo tempo, esperamos trabalhar com ele novamente. O que esperamos nesta situação? que isso irá falhar? Como se o Dispose for executado primeiro, o uso dos métodos de objeto a seguir resultará definitivamente em ObjectDisposedException . Portanto, você deve delegar a sincronização entre as chamadas Dispose () e outros métodos públicos de um tipo para o lado do serviço, ou seja, para o código que criou a instância da classe FileWrapper . Isso ocorre porque apenas o lado criador sabe o que fará com uma instância de uma classe e quando destruí-la. Por outro lado, uma chamada Dispose deve produzir apenas erros críticos, como OutOfMemoryException , mas não IOException por exemplo. Isso ocorre devido aos requisitos para a arquitetura de classes que implementam IDisposable. Isso significa que, se Dispose for chamado de mais de um encadeamento de cada vez, a destruição de uma entidade pode ocorrer a partir de dois encadeamentos simultaneamente if(_disposed) return; a verificação do if(_disposed) return; ). Depende da situação: se um recurso puder ser liberado várias vezes, não haverá necessidade de verificações adicionais. Caso contrário, a proteção é necessária:


 // 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 } } } 

Dois níveis de Princípio de Design Descartável


Qual é o padrão mais popular para implementar IDisposable que você pode encontrar nos livros do .NET e na Internet? Que padrão é esperado de você durante as entrevistas para um novo emprego em potencial? Provavelmente 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); } } 

O que há de errado com este exemplo e por que não escrevemos assim antes? De fato, esse é um bom padrão adequado para todas as situações. No entanto, seu uso onipresente não é um bom estilo, na minha opinião, pois quase não lidamos com recursos não gerenciados na prática, o que faz com que metade do padrão não sirva para nada. Além disso, como gerencia simultaneamente recursos gerenciados e não gerenciados, viola o princípio da divisão de responsabilidades. Eu acho que isso está errado. Vamos olhar para uma abordagem um pouco diferente. Princípio do projeto descartável . Em resumo, funciona da seguinte maneira:


O descarte é dividido em dois níveis de classes:


  • Os tipos de nível 0 encapsulam diretamente recursos não gerenciados
    • Eles são abstratos ou compactados.
    • Todos os métodos devem ser marcados:
      - PrePrepareMethod, para que um método possa ser compilado ao carregar um tipo
      • SecuritySafeCritical para se proteger contra uma chamada do código, trabalhando sob restrições
      • ReliabilityContract (Consistency.WillNotCorruptState, Cer.Success / MayFail)] para colocar o CER em um método e em todas as chamadas filho
        - Eles podem fazer referência aos tipos de nível 0, mas devem incrementar o contador de objetos de referência para garantir a ordem correta de entrada na "última milha"
  • Os tipos de nível 1 encapsulam apenas recursos gerenciados
    • Eles são herdados apenas dos tipos de Nível 1 ou implementam diretamente IDisposable
    • Eles não podem herdar tipos de Nível 0 ou CriticalFinalizerObject
    • Eles podem encapsular os tipos gerenciados de Nível 1 e Nível 0
    • Eles implementam o IDisposable.Dispose destruindo objetos encapsulados a partir dos tipos de nível 0 e indo para o nível 1
    • Eles não implementam um finalizador, pois não lidam com recursos não gerenciados
    • Eles devem conter uma propriedade protegida que dê acesso aos tipos de nível 0.

É por isso que usei a divisão em dois tipos desde o início: o que contém um recurso gerenciado e o que possui um recurso não gerenciado. Eles devem funcionar de maneira diferente.


Outras maneiras de usar Dispose


A idéia por trás da criação do IDisposable era liberar recursos não gerenciados. Mas, como em muitos outros padrões, é muito útil para outras tarefas, por exemplo, liberar referências a recursos gerenciados. Embora liberar recursos gerenciados não pareça muito útil. Quero dizer, eles são chamados de gerenciados de propósito, para que possamos relaxar com um sorriso em relação aos desenvolvedores de C / C ++, certo? No entanto, não é assim. Sempre pode haver uma situação em que perdemos uma referência a um objeto, mas ao mesmo tempo pensamos que está tudo bem: o GC coletará lixo, incluindo o nosso objeto. No entanto, verifica-se que a memória cresce. Entramos no programa de análise de memória e vemos que outra coisa contém esse objeto. O problema é que pode haver uma lógica para a captura implícita de uma referência à sua entidade na plataforma .NET e na arquitetura de classes externas. Como a captura está implícita, um programador pode perder a necessidade de seu lançamento e depois obter um vazamento de memória.


Delegados, eventos


Vejamos este exemplo 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!"); } } 

Qual problema esse código mostra? A classe secundária armazena o tipo de Action delegado no campo _action que é aceito no método SaveForUseInFuture . Em seguida, o método PlanSayHello dentro Primary classe Primary passa o ponteiro para o método Strategy para Secondary classe Secondary . É curioso, mas se, neste exemplo, você passar em algum lugar um método estático ou um método de instância, o SaveForUseInFuture transmitido não será alterado, mas uma instância de classe Primary será referenciada implicitamente ou de nenhuma maneira. Externamente, parece que você instruiu qual método chamar. Mas, de fato, um delegado é construído não apenas usando um ponteiro de método, mas também usando o ponteiro para uma instância de uma classe. Um interlocutor deve entender para qual instância de uma classe deve chamar o método Strategy ! Essa é a instância da classe Secondary que implicitamente aceitou e mantém o ponteiro para a instância da classe Primary , embora ela não seja indicada explicitamente. Para nós, significa apenas que, se _foo ponteiro _foo para outro lugar e perdermos a referência ao Primary , o GC não coletará o objeto Primary , pois o Secondary o manterá. Como podemos evitar tais situações? Precisamos de uma abordagem determinada para divulgar uma referência para nós. Um mecanismo que se encaixa perfeitamente a esse propósito é 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); } } 

Agora o exemplo parece aceitável. Se uma instância de uma classe for passada para terceiros e a referência ao delegado _action for perdida durante esse processo, definiremos como zero e o terceiro será notificado sobre a destruição da instância e excluiremos a referência a ela. .
O segundo perigo de código executado nos delegados são os princípios de funcionamento do event . Vejamos o que eles resultam:


  // 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; } } } 

O sistema de mensagens C # oculta as partes internas dos eventos e mantém todos os objetos que se inscreveram para atualizar através do event . Se algo der errado, uma referência a um objeto assinado permanecerá em OnDisposed e o manterá. É uma situação estranha, pois em termos de arquitetura obtemos um conceito de "fonte de eventos" que não deve conter nada logicamente. Mas, de fato, os objetos inscritos na atualização são mantidos implicitamente. Além disso, não podemos alterar algo dentro dessa matriz de delegados, embora a entidade nos pertença. A única coisa que podemos fazer é excluir esta lista atribuindo null a uma fonte de eventos.


A segunda maneira é implementar métodos add / remove explicitamente, para que possamos controlar uma coleção de delegados.


Outra situação implícita pode aparecer aqui. Pode parecer que, se você atribuir nulo a uma fonte de eventos, a seguinte assinatura de eventos causará NullReferenceException . Eu acho que isso seria mais lógico.

No entanto, isso não é verdade. Se o código externo se inscrever nos eventos após a OnDisposed uma fonte de eventos, o FCL criará uma nova instância da classe Action e a armazenará no OnDisposed . Essa implicação em C # pode enganar um programador: lidar com campos nulos deve produzir um tipo de alerta, em vez de calma. Aqui também demonstramos uma abordagem quando o descuido de um programador pode levar a vazamentos de memória.


Fechamentos Lambdas


Usar açúcar sintático como lambdas é especialmente perigoso.


Eu gostaria de abordar o açúcar sintático como um todo. Eu acho que você deve usá-lo com bastante cuidado e somente se souber exatamente o resultado. Exemplos com expressões lambda são fechamentos, fechamentos em Expressões e muitas outras misérias que você pode infligir a si mesmo.

Obviamente, você pode dizer que sabe que uma expressão lambda cria um fechamento e pode resultar em risco de vazamento de recursos. Mas é tão elegante e agradável que é difícil evitar o uso do lambda em vez de alocar todo o método, que será descrito em um local diferente do local em que será usado. De fato, você não deve aceitar essa provocação, embora nem todos possam resistir. Vejamos o exemplo:


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

Concordo, esta linha parece muito segura. Mas ele esconde um grande problema: agora button variável button refere-se implicitamente ao service e o mantém. Mesmo se decidirmos que não precisamos mais de service , o button ainda manterá a referência enquanto essa variável estiver ativa. Uma das maneiras de resolver esse problema é usar um padrão para criar IDisposable partir de qualquer 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(); 

Admita, isso parece um pouco demorado e perdemos todo o propósito de usar expressões lambda. É muito mais seguro e simples usar métodos privados comuns para capturar variáveis ​​implicitamente.


Proteção Threadabort


Quando você cria uma biblioteca para um desenvolvedor de terceiros, não pode prever o comportamento dele em um aplicativo de terceiros. Às vezes, você pode apenas adivinhar o que um programador fez na sua biblioteca que causou um resultado específico. Um exemplo está funcionando em um ambiente multithread quando a consistência da limpeza dos recursos pode se tornar um problema crítico. Observe que, quando escrevemos o método Dispose() , podemos garantir a ausência de exceções. No entanto, não podemos garantir que, durante a execução do método Dispose() , não ocorra ThreadAbortException que desabilite nosso encadeamento de execução. Aqui devemos lembrar que, quando o ThreadAbortException ocorre, todos os blocos catch / finalmente são executados de qualquer maneira (no final de um bloco catch / finalmente o ThreadAbort ocorre mais adiante). Portanto, para garantir a execução de um determinado código usando o Thread.Abort, você precisa agrupar uma seção crítica na try { ... } finally { ... } , veja o exemplo abaixo:


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

Pode-se abortar isso a qualquer momento usando o Thread.Abort . Destrói parcialmente um objeto, embora você ainda possa trabalhar com ele no futuro. Ao mesmo tempo, o seguinte código:


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

está protegido contra essa interrupção e será executado sem problemas e com certeza, mesmo se Thread.Abort aparecer entre a chamada do método Unsubscribe e a execução de suas instruções.


Resultados


Vantagens


Bem, aprendemos muito sobre esse padrão mais simples. Vamos determinar suas vantagens:


  1. A principal vantagem do padrão é a capacidade de liberar recursos determinadamente, ou seja, quando você precisar deles.
  2. A segunda vantagem é a introdução de uma maneira comprovada de verificar se uma instância específica precisa destruir suas instâncias após o uso.
  3. Se você implementar o padrão corretamente, um tipo projetado funcionará com segurança em termos de uso por componentes de terceiros, bem como em termos de descarregamento e destruição de recursos quando um processo falha (por exemplo, devido à falta de memória). Esta é a última vantagem.

Desvantagens


Na minha opinião, esse padrão tem mais desvantagens do que vantagens.


  1. Por um lado, qualquer tipo que implemente esse padrão instrui outras partes que, se o usarem, receberão uma espécie de oferta pública. Isso é tão implícito que, como no caso de ofertas públicas, um usuário de um tipo nem sempre sabe que o tipo tem essa interface. Portanto, você deve seguir as instruções do IDE (digite um ponto, Dis ... e verifique se há um método na lista de membros filtrados de uma classe). Se você vir um padrão Dispose, deverá implementá-lo em seu código. Às vezes, isso não acontece imediatamente e, nesse caso, você deve implementar um padrão por meio de um sistema de tipos que adiciona funcionalidade. Um bom exemplo é que IEnumerator<T> implica IDisposable .
  2. Geralmente, quando você cria uma interface, é necessário inserir IDisposable no sistema das interfaces de um tipo quando uma delas precisa herdar IDisposable. Na minha opinião, isso danifica as interfaces que projetamos. Quero dizer, quando você cria uma interface, primeiro cria um protocolo de interação. Este é um conjunto de ações que você pode executar com algo oculto por trás da interface. Dispose() é um método para destruir uma instância de uma classe. Isso contradiz a essência de um protocolo de interação . De fato, esses são os detalhes de implementação que se infiltraram na interface.
  3. Apesar de determinado, Dispose () não significa destruição direta de um objeto. O objeto ainda existirá após sua destruição, mas em outro estado. Para torná-lo verdadeiro, CheckDisposed () deve ser o primeiro comando de cada método público. Isso parece uma solução temporária que alguém nos deu dizendo: "Vá em frente e multiplique";
  4. Há também uma pequena chance de obter um tipo que implemente IDisposable por meio de implementação explícita . Ou você pode obter um tipo que implementa o ID disponível, sem chance de determinar quem deve destruí-lo: você ou a parte que o deu. Isso resultou em um antipadrão de várias chamadas de Dispose () que permitem destruir um objeto destruído;
  5. A implementação completa é difícil e é diferente para recursos gerenciados e não gerenciados. Aqui, a tentativa de facilitar o trabalho dos desenvolvedores por meio do GC parece estranha. Você pode substituir o método virtual void Dispose() e introduzir algum tipo DisposableObject que implementa todo o padrão, mas que não resolve outros problemas relacionados ao padrão;
  6. Como regra geral, o método Dispose () é implementado no final de um arquivo enquanto '.ctor' é declarado no início. Se você modificar uma classe ou introduzir novos recursos, é fácil esquecer de adicionar disposição para eles.
  7. Finalmente, é difícil determinar a ordem de destruição em um ambiente multithread quando você usa um padrão para gráficos de objetos em que objetos implementam total ou parcialmente esse padrão. Quero dizer situações em que Dispose () pode começar em extremidades diferentes de um gráfico. Aqui é melhor usar outros padrões, por exemplo, o padrão Lifetime.
  8. O desejo dos desenvolvedores de plataformas de automatizar o controle de memória combinado com as realidades: os aplicativos interagem com o código não gerenciado com muita frequência + você precisa controlar o lançamento de referências a objetos para que o Garbage Collector possa coletá-los. Isso acrescenta grande confusão ao entender questões como: “Como devemos implementar um padrão corretamente”? "Existe algum padrão confiável"? Talvez chamando delete obj; delete[] arr; delete obj; delete[] arr; é mais simples?

Descarregamento de domínio e saída de um aplicativo


Se você chegou a essa parte, ficou mais confiante no sucesso de futuras entrevistas de emprego. No entanto, não discutimos todas as perguntas relacionadas a esse padrão simples, como pode parecer. A última pergunta é se o comportamento de um aplicativo é diferente no caso de coleta simples de lixo e quando o lixo é coletado durante o descarregamento do domínio e ao sair do aplicativo. Essa questão se refere apenas a Dispose() ... No entanto, Dispose() e finalização andam de mãos dadas e raramente encontramos uma implementação de uma classe que tenha finalização, mas não tem o método Dispose() . Então, vamos descrever a finalização em uma seção separada. Aqui apenas adicionamos alguns detalhes importantes.


Durante o descarregamento do domínio do aplicativo, você descarrega os dois assemblies carregados no domínio do aplicativo e todos os objetos que foram criados como parte do domínio a serem descarregados. De fato, isso significa a limpeza (coleta pelo GC) desses objetos e a chamada de finalizadores para eles. Se a lógica de um finalizador aguarda a finalização de outros objetos serem destruídos na ordem correta, você deve prestar atenção à propriedade Environment.HasShutdownStarted indicando que um aplicativo está descarregado da memória e ao método AppDomain.CurrentDomain.IsFinalizingForUnload() indicando que isso o domínio é descarregado, e é esse o motivo da finalização. Se esses eventos ocorrerem, a ordem de finalização dos recursos geralmente se torna sem importância. Não podemos atrasar o descarregamento do domínio ou de um aplicativo, pois devemos fazer tudo o mais rápido possível.


É assim que esta tarefa é resolvida como parte de uma classe 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); } } 

Falhas típicas de implementação


Como mostrei a você, não há um padrão universal para implementar IDisposable. Além disso, a confiança no controle automático de memória engana as pessoas e elas tomam decisões confusas ao implementar um padrão. Todo o .NET Framework está cheio de erros em sua implementação. Para provar meu argumento, vejamos esses erros usando exatamente o exemplo do .NET Framework. Todas as implementações estão disponíveis em: IDisposable Usages


Classe FileEntry cmsinterop.cs


Este código é escrito com pressa apenas para fechar o problema. Obviamente, o autor queria fazer algo, mas mudou de idéia e manteve uma solução falha

 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 classes SemaphoreSlim / Threading / SemaphoreSlim.cs


Este erro está no topo dos erros do .NET Framework em relação ao IDisposable: SuppressFinalize para classes em que não há finalizador. Isso é muito comum.

 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; } } 

Chamando Close + Dispose Some NativeWatcher código de projeto


Às vezes, as pessoas chamam de Fechar e Dispor. Isso está errado, mas não produzirá um erro, pois o segundo Dispose não gera uma exceção.

De fato, Close é outro padrão para tornar as coisas mais claras para as pessoas. No entanto, tornou tudo mais claro.


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

Resultados gerais


  1. O IDposable é um padrão da plataforma e a qualidade de sua implementação influencia a qualidade de todo o aplicativo. Além disso, em algumas situações, isso influencia a segurança do seu aplicativo que pode ser atacada por recursos não gerenciados.
  2. A implementação do IDisposable deve ser maximamente produtiva. Isso é especialmente verdade na seção de finalização, que funciona em paralelo com o restante do código, carregando o Garbage Collector.
  3. Ao implementar o IDisposable, você não deve usar Dispose () simultaneamente com métodos públicos de uma classe. A destruição não pode acompanhar o uso. Isso deve ser considerado ao projetar um tipo que usará o objeto IDisposable.
  4. No entanto, deve haver uma proteção contra a chamada 'Dispose ()' de dois threads simultaneamente. Isso resulta da declaração de que Dispose () não deve produzir erros.
  5. Os tipos que contêm recursos não gerenciados devem ser separados de outros tipos. Quero dizer, se você agrupar um recurso não gerenciado, aloque um tipo separado para ele. Esse tipo deve conter a finalização e deve ser herdado de SafeHandle / CriticalHandle / CriticalFinalizerObject . Essa separação de responsabilidades resultará em suporte aprimorado ao sistema de tipos e simplificará a implementação para destruir instâncias de tipos via Dispose (): os tipos com esta implementação não precisarão implementar um finalizador.
  6. Em geral, esse padrão não é confortável tanto em uso quanto em manutenção de código. Provavelmente, devemos usar a abordagem Inversion of Control quando destruirmos o estado dos objetos via padrão Lifetime . No entanto, falaremos sobre isso na próxima seção.

Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.

Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório github / sidristij / dotnetbook .

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


All Articles