
Padrão descartável (princípio do projeto descartável)
Eu acho que quase todo programador que usa .NET agora diz que esse padrão é um pedaço de bolo. Esse é o padrão mais conhecido usado na plataforma. No entanto, mesmo o domínio do problema mais simples e conhecido terá áreas secretas que você nunca viu. Então, vamos descrever tudo desde o início para os iniciantes e todo o resto (para que cada um de vocês se lembre do básico). Não pule esses parágrafos - estou observando você!
Se eu perguntar o que é IDisposable, você certamente dirá que é
public interface IDisposable { void Dispose(); }
Qual é o objetivo da interface? Quero dizer, por que precisamos limpar a memória, se temos um Garbage Collector inteligente que limpa a memória em vez de nós, para que nem precisemos pensar nisso. No entanto, existem alguns pequenos detalhes.
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 .
Existe um equívoco de que o IDisposable
serve para liberar recursos não gerenciados. Isso é apenas parcialmente verdadeiro e, para entendê-lo, basta lembrar dos exemplos de recursos não gerenciados. A classe File
é um recurso não gerenciado? Não. Talvez o DbContext
seja um recurso não gerenciado? Não de novo Um recurso não gerenciado é algo que não pertence ao sistema de tipos .NET. Algo que a plataforma não criou, algo que existe fora de seu escopo. Um exemplo simples é um identificador de arquivo aberto em um sistema operacional. Um identificador é um número que identifica exclusivamente um arquivo aberto - não, não por você - por um sistema operacional. Ou seja, todas as estruturas de controle (por exemplo, a posição de um arquivo em um sistema de arquivos, fragmentos de arquivo em caso de fragmentação e outras informações de serviço, os números de um cilindro, uma cabeça ou um setor de um HDD) estão dentro de um sistema operacional, mas não Plataforma .NET. O único recurso não gerenciado que é passado para a plataforma .NET é o número IntPtr. Esse número é agrupado por FileSafeHandle, que por sua vez é agrupado pela classe File. Isso significa que a classe File não é um recurso não gerenciado por si só, mas usa uma camada adicional na forma do IntPtr para incluir um recurso não gerenciado - o identificador de um arquivo aberto. Como você lê esse arquivo? Usando um conjunto de métodos no sistema operacional WinAPI ou Linux.
Primitivas de sincronização em programas multithread ou multiprocessador são o segundo exemplo de recursos não gerenciados. Aqui pertencem matrizes de dados que são passadas por P / Invoke e também mutexes ou semáforos.
Observe que o SO simplesmente não passa o identificador de um recurso não gerenciado para um aplicativo. Ele também salva esse identificador na tabela de identificadores abertos pelo processo. Assim, o sistema operacional pode fechar corretamente os recursos após o encerramento do aplicativo. Isso garante que os recursos sejam fechados de qualquer maneira depois que você sair do aplicativo. No entanto, o tempo de execução de um aplicativo pode ser diferente, o que pode causar um longo bloqueio de recursos.
Ok Agora, cobrimos recursos não gerenciados. Por que precisamos usar o IDisposable nesses casos? Porque o .NET Framework não tem idéia do que está acontecendo fora de seu território. Se você abrir um arquivo usando a API do SO, o .NET não saberá nada sobre ele. Se você alocar um intervalo de memória para suas próprias necessidades (por exemplo, usando o VirtualAlloc), o .NET também não saberá nada. Se não souber, não liberará a memória ocupada por uma chamada do VirtualAlloc. Ou não fechará um arquivo aberto diretamente por meio de uma chamada da API do SO. Isso pode causar consequências diferentes e inesperadas. Você pode obter o OutOfMemory se alocar muita memória sem liberá-la (por exemplo, apenas definindo um ponteiro como nulo). Ou, se você abrir um arquivo em um compartilhamento de arquivo através do SO sem fechá-lo, bloqueará o arquivo nesse compartilhamento por um longo período de tempo. O exemplo de compartilhamento de arquivos é especialmente bom, pois o bloqueio permanecerá no lado do IIS, mesmo depois que você fechar uma conexão com um servidor. Você não tem direitos para liberar o bloqueio e precisará solicitar aos administradores que executem o iisreset
ou fechem os recursos manualmente usando um software especial.
Esse problema em um servidor remoto pode se tornar uma tarefa complexa a ser resolvida.
Todos esses casos precisam de um protocolo universal e familiar para interação entre um sistema de tipos e um programador. Ele deve identificar claramente os tipos que requerem fechamento forçado. A interface IDisposable serve exatamente para esse propósito. Funciona da seguinte maneira: se um tipo contiver a implementação da interface IDisposable, você deverá chamar Dispose () após concluir o trabalho com uma instância desse tipo.
Portanto, existem duas maneiras padrão de chamá-lo. Geralmente, você cria uma instância da entidade para usá-la rapidamente em um método ou durante a vida útil da instância da entidade.
A primeira maneira é envolver uma instância no using(...){ ... }
. Isso significa que você instrui a destruir um objeto após o término do bloco relacionado ao uso, ou seja, chamar Dispose (). A segunda maneira é destruir o objeto, quando sua vida útil terminar, com uma referência ao objeto que queremos liberar. Mas o .NET não tem nada além de um método de finalização que implica a destruição automática de um objeto, certo? No entanto, a finalização não é adequada, pois não sabemos quando será chamada. Enquanto isso, precisamos liberar um objeto em um determinado momento, por exemplo, logo após terminarmos o trabalho com um arquivo aberto. É por isso que também precisamos implementar o IDisposable e chamar Dispose para liberar todos os recursos que possuímos. Assim, seguimos o protocolo , e é muito importante. Porque se alguém seguir, todos os participantes devem fazer o mesmo para evitar problemas.
Diferentes maneiras de implementar IDisposable
Vejamos as implementações do IDisposable de simples a complicadas. O primeiro e o mais simples é usar o IDisposable, pois é:
public class ResourceHolder : IDisposable { DisposableResource _anotherResource = new DisposableResource(); public void Dispose() { _anotherResource.Dispose(); } }
Aqui, criamos uma instância de um recurso que é lançado posteriormente por Dispose (). A única coisa que torna essa implementação inconsistente é que você ainda pode trabalhar com a instância após sua destruição por Dispose()
:
public class ResourceHolder : IDisposable { private DisposableResource _anotherResource = new DisposableResource(); private bool _disposed; public void Dispose() { if(_disposed) return; _anotherResource.Dispose(); _disposed = true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } }
CheckDisposed () deve ser chamado como uma primeira expressão em todos os métodos públicos de uma classe. A estrutura de classe ResourceHolder
obtida parece boa para destruir um recurso não gerenciado, que é DisposableResource
. No entanto, essa estrutura não é adequada para um recurso não gerenciado incorporado. Vejamos o exemplo com um recurso não gerenciado.
public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { CloseHandle(_handle); } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern IntPtr CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool CloseHandle(IntPtr hObject); }
Qual é a diferença no comportamento dos dois últimos exemplos? O primeiro descreve a interação de dois recursos gerenciados. Isso significa que, se um programa funcionar corretamente, o recurso será liberado de qualquer maneira. Como DisposableResource
é gerenciado, o .NET CLR sabe disso e liberará a memória se o seu comportamento estiver incorreto. Observe que conscientemente não suponho que tipo DisposableResource
encapsula. Pode haver qualquer tipo de lógica e estrutura. Ele pode conter recursos gerenciados e não gerenciados. Isso não deveria nos preocupar . Ninguém nos pede para descompilar as bibliotecas de terceiros a cada vez e ver se eles usam recursos gerenciados ou não gerenciados. E se nosso tipo usa um recurso não gerenciado, não podemos estar cientes disso. Fazemos isso na classe FileWrapper
. Então, o que acontece neste caso? Se usarmos recursos não gerenciados, teremos dois cenários. O primeiro é quando tudo está OK e Dispose é chamado. O segundo é quando algo dá errado e Dispose falhou.
Digamos imediatamente por que isso pode dar errado:
- Se usarmos
using(obj) { ... }
, uma exceção poderá aparecer em um bloco interno de código. Essa exceção é capturada pelo bloco finally
, que não podemos ver (este é o açúcar sintático do C #). Este bloco chama Dispose implicitamente. No entanto, há casos em que isso não acontece. Por exemplo, nem catch
nem finally
pegar StackOverflowException
. Você deve sempre se lembrar disso. Como se algum thread se tornar recursivo e o StackOverflowException
ocorrer em algum momento, o .NET esquecerá os recursos que ele usou, mas não liberou. Ele não sabe como liberar recursos não gerenciados. Eles permanecerão na memória até que o SO os libere, ou seja, quando você sair de um programa ou mesmo algum tempo após o encerramento de um aplicativo. - Se chamarmos Dispose () de outro Dispose (). Novamente, podemos não conseguir alcançá-lo. Este não é o caso de um desenvolvedor de aplicativos distraído que esqueceu de chamar Dispose (). É a questão das exceções. No entanto, essas não são apenas as exceções que travam um encadeamento de um aplicativo. Aqui, falamos sobre todas as exceções que impedirão um algoritmo de chamar um Dispose () externo que chamará nosso Dispose ().
Todos esses casos criarão recursos não gerenciados suspensos. Isso ocorre porque o Garbage Collector não sabe que deve coletá-los. Tudo o que pode ser feito na próxima verificação é descobrir que a última referência a um gráfico de objeto com o nosso tipo FileWrapper
foi perdida. Nesse caso, a memória será realocada para objetos com referências. Como podemos evitá-lo?
Nós devemos implementar o finalizador de um objeto. O 'finalizador' é nomeado desta maneira de propósito. Não é um destruidor, como pode parecer devido a maneiras semelhantes de chamar finalizadores em C # e destruidores em C ++. A diferença é que um finalizador será chamado de qualquer maneira , ao contrário de um destruidor (assim como Dispose()
). Um finalizador é chamado quando a Coleta de Lixo é iniciada (agora é suficiente saber disso, mas as coisas são um pouco mais complicadas). É usado para uma liberação garantida de recursos se algo der errado . Precisamos implementar um finalizador para liberar recursos não gerenciados. Novamente, como um finalizador é chamado quando o GC é iniciado, não sabemos quando isso acontece em geral.
Vamos expandir nosso código:
public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { InternalDispose(); GC.SuppressFinalize(this); } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
Aprimoramos o exemplo com o conhecimento sobre o processo de finalização e protegemos o aplicativo contra a perda de informações de recursos se Dispose () não for chamado. Também chamamos GC.SuppressFinalize para desativar a finalização da instância do tipo se Dispose () for chamado com êxito. Não há necessidade de liberar o mesmo recurso duas vezes, certo? Portanto, também reduzimos a fila de finalização liberando uma região aleatória do código que provavelmente será executada com a finalização em paralelo, algum tempo depois. Agora, vamos aprimorar o exemplo ainda mais.
public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { if(_disposed) return; _disposed = true; InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
Agora, nosso exemplo de um tipo que encapsula um recurso não gerenciado parece completo. Infelizmente, o segundo Dispose()
é de fato um padrão da plataforma e permitimos chamá-lo. Observe que as pessoas geralmente permitem que a segunda chamada de Dispose()
evite problemas com um código de chamada e isso está errado. No entanto, um usuário da sua biblioteca que analisa a documentação da MS pode não pensar e permitirá várias chamadas de Dispose (). Chamar outros métodos públicos destruirá a integridade de um objeto de qualquer maneira. Se destruímos o objeto, não podemos mais trabalhar com ele. Isso significa que devemos chamar CheckDisposed
no início de cada método público.
No entanto, este código contém um problema grave que o impede de funcionar como pretendido. Se lembrarmos como a coleta de lixo funciona, notaremos um recurso. Ao coletar lixo, o GC finaliza principalmente tudo o que é herdado diretamente do Object . Em seguida, lida com objetos que implementam CriticalFinalizerObject . Isso se torna um problema, pois as duas classes que projetamos herdam Object. Não sabemos em que ordem eles chegarão à "última milha". No entanto, um objeto de nível superior pode usar seu finalizador para finalizar um objeto com um recurso não gerenciado. Embora isso não pareça uma ótima idéia. A ordem de finalização seria muito útil aqui. Para defini-lo, o tipo de nível inferior com um recurso não gerenciado encapsulado deve ser herdado de CriticalFinalizerObject
.
A segunda razão é mais profunda. Imagine que você se atreveu a escrever um aplicativo que não cuida muito da memória. Aloca memória em grandes quantidades, sem descontar e outras sutilezas. Um dia, esse aplicativo falhará com o OutOfMemoryException. Quando isso ocorre, o código é executado especificamente. Ele não pode alocar nada, pois levará a uma exceção repetida, mesmo que a primeira seja capturada. Isso não significa que não devemos criar novas instâncias de objetos. Mesmo uma simples chamada de método pode gerar essa exceção, por exemplo, a finalização. Lembro que os métodos são compilados quando você os chama pela primeira vez. Esse é o comportamento usual. Como podemos evitar esse problema? Muito facilmente. Se o seu objeto for herdado do CriticalFinalizerObject , todos os métodos desse tipo serão compilados imediatamente após o carregamento na memória. Além disso, se você marcar métodos com o atributo [PrePrepareMethod] , eles também serão pré-compilados e terão segurança para chamar em uma situação de poucos recursos.
Por que isso é importante? Por que gastar muito esforço com aqueles que morrem? Porque os recursos não gerenciados podem ser suspensos em um sistema por muito tempo. Mesmo depois de reiniciar um computador. Se um usuário abrir um arquivo a partir de um compartilhamento de arquivos no seu aplicativo, o primeiro será bloqueado por um host remoto e liberado no tempo limite ou quando você liberar um recurso fechando o arquivo. Se o seu aplicativo travar quando o arquivo for aberto, ele não será lançado mesmo após a reinicialização. Você terá que esperar muito tempo até que o host remoto o libere. Além disso, você não deve permitir exceções nos finalizadores. Isso leva a uma falha acelerada do CLR e de um aplicativo, pois você não pode encerrar a chamada de um finalizador na tentativa ... captura . Quero dizer, quando você tenta liberar um recurso, deve ter certeza de que ele pode ser liberado. O último, mas não menos importante, fato: se o CLR descarregar um domínio de maneira anormal, os finalizadores dos tipos derivados de CriticalFinalizerObject também serão chamados, diferentemente dos herdados diretamente do Object .
Este charper traduzido do russo como idioma do autor por tradutores profissionais . Você pode nos ajudar a criar a versão traduzida deste texto para qualquer outro idioma, incluindo chinês ou alemão, usando as versões russa e inglesa do texto como fonte.
Além disso, se você quiser dizer "obrigado", a melhor maneira de escolher é dar uma estrela no repositório do github ou do fork
https://github.com/sidristij/dotnetbook