Quem deve chamar Dispose on IDisposable objects quando passado para outro object?

Existe alguma orientação ou melhores práticas sobre quem deve chamar Dispose() em objects descartáveis ​​quando eles foram passados ​​para os methods ou constantes de outro object?

Aqui estão alguns exemplos sobre o que quero dizer.

Objeto IDisposable é passado para um método (deve descartá-lo uma vez feito?):

 public void DoStuff(IDisposable disposableObj) { // Do something with disposableObj CalculateSomething(disposableObj) disposableObj.Dispose(); } 

O object IDisposable é passado para um método e uma referência é mantida (Se ele for descartado quando o MyClass for descartado?):

 public class MyClass : IDisposable { private IDisposable _disposableObj = null; public void DoStuff(IDisposable disposableObj) { _disposableObj = disposableObj; } public void Dispose() { _disposableObj.Dispose(); } } 

Atualmente estou pensando que no primeiro exemplo o chamador de DoStuff() deve descartar o object como ele provavelmente criou o object. Mas no segundo exemplo, parece que MyClass deve dispor do object, pois ele mantém uma referência a ele. O problema com isso é que a class chamadora pode não saber que o MyClass manteve uma referência e, portanto, pode decidir descartar o object antes que o MyClass tenha terminado de usá-lo. Existem regras padrão para esse tipo de cenário? Se houver, eles diferem quando o object descartável está sendo passado para um construtor?

Uma regra geral é que, se você criou (ou adquiriu a propriedade de) o object, então é sua responsabilidade descartá-lo. Isso significa que, se você receber um object descartável como um parâmetro em um método ou construtor, geralmente não deve descartá-lo.

Observe que algumas classs na estrutura .NET descartam objects que receberam como parâmetros. Por exemplo, a disposição de um StreamReader também dispõe o Stream subjacente.

PS: Eu publiquei uma nova resposta (contendo um conjunto simples de regras que deveria chamar Dispose , e como projetar uma API que lida com objects IDisposable ). Embora a presente resposta contenha idéias valiosas, passei a acreditar que sua principal sugestão muitas vezes não funciona na prática: ocultar objects IDisposable em objects de “granulação mais grossa” geralmente significa que eles precisam se tornar IDisposable ; então um acaba onde um começou, e o problema permanece.


Existe alguma orientação ou melhores práticas sobre quem deve chamar Dispose() em objects descartáveis ​​quando eles foram passados ​​para os methods ou constantes de outro object?

Resposta curta:

Sim, há muitos conselhos sobre esse assunto, e o melhor que conheço é o conceito de Agregados no Design Dirigido por Domínio de Eric Evans . (Simplificando, a idéia central aplicada a IDisposable é a seguinte: encapsular o IDisposable em um componente de granulação mais grossa, de modo que ele não seja visto externamente e nunca seja passado para o consumidor do componente.)

Além disso, a ideia de que o criador de um object descartável também deva ser encarregado de descartá-lo é muito restritiva e muitas vezes não funcionará na prática.

O resto da minha resposta é mais detalhado em ambos os pontos, na mesma ordem. Eu terminarei minha resposta com algumas dicas para mais material relacionado ao mesmo tópico.

Resposta mais longa – O que é esta questão em termos gerais:

Conselhos sobre este tópico geralmente não são específicos para IDisposable . Sempre que as pessoas falam sobre as vidas dos objects e a propriedade, elas estão se referindo ao mesmo problema (mas em termos mais gerais).

Por que esse tópico quase nunca surge no ecossistema .NET? Como o ambiente de tempo de execução do .NET (o CLR) executa garbage collection automática, que faz todo o trabalho para você: Se você não precisar mais de um object, pode simplesmente esquecê-lo e o coletor de lixo recuperará sua memory.

Por que, então, a pergunta surge com objects IDisposable ? Porque IDisposable é tudo sobre o controle explícito e determinístico de uma vida útil (frequentemente esparsa ou dispendiosa): objects supostamente IDisposable devem ser liberados assim que eles não são mais necessários – e a garantia indeterminada do coletor de lixo (“eu eventualmente recuperarei” a memory usada por você! “) simplesmente não é boa o suficiente.

Sua pergunta, reformulada nos termos mais amplos de tempo de vida e propriedade do object:

Qual object O deve ser responsável por terminar o tempo de vida de um object (descartável) D , que também é passado para objects X,Y,Z ?

Vamos estabelecer algumas suposições:

  • Chamar D.Dispose() para um object IDisposable D basicamente termina sua vida útil.

  • Logicamente, a vida útil de um object só pode ser finalizada uma vez. (Não importa o momento em que isso está em oposição ao protocolo IDisposable , que explicitamente permite que várias chamadas sejam Dispose .)

  • Portanto, por uma questão de simplicidade, exatamente um object O deve ser responsável por descartar D Vamos chamar O o dono.

Agora chegamos ao cerne da questão: nem a linguagem C # nem o VB.NET fornecem um mecanismo para impor relacionamentos de propriedade entre objects. Isso se transforma em um problema de design: todos os objects O,X,Y,Z que recebem uma referência a outro object D devem seguir e aderir a uma convenção que regula exatamente quem tem propriedade sobre D

Simplifique o problema com Agregados!

O melhor conselho que eu encontrei sobre este tópico vem do livro de 2004 de Eric Evans , Domain-Driven Design . Deixe-me citar do livro:

Digamos que você estivesse excluindo um object Person de um database. Junto com a pessoa vai um nome, data de nascimento e uma descrição do trabalho. Mas e o endereço? Pode haver outras pessoas no mesmo endereço. Se você excluir o endereço, esses objects Person terão referências a um object excluído. Se você deixá-lo, você acumula endereços de lixo eletrônico no database. A coleta automática de lixo poderia eliminar os endereços de lixo eletrônico, mas essa correção técnica, mesmo se disponível em seu sistema de database, ignora um problema básico de modelagem. (p. 125)

Veja como isso está relacionado ao seu problema? Os endereços deste exemplo são equivalentes aos objects descartáveis ​​e as perguntas são as mesmas: quem deve excluí-los? Quem “possui” eles?

Evans continua sugerindo Aggregates como uma solução para esse problema de design. Do livro novamente:

Um Aggregate é um cluster de objects associados que tratamos como uma unidade para fins de alterações de dados. Cada agregado tem uma raiz e um limite. O limite define o que está dentro do Agregado. A raiz é uma entidade única e específica contida no Agregado. A raiz é o único membro do Aggregate ao qual objects externos podem manter referências, embora objects dentro do limite possam conter referências uns aos outros. (pp. 126-127)

A mensagem principal aqui é que você deve restringir a passagem do seu object IDisposable para um conjunto estritamente limitado (“agregado”) de outros objects. Objetos fora desse limite agregado nunca devem obter uma referência direta ao seu IDisposable . Isso simplifica muito as coisas, já que você não precisa mais se preocupar se a maior parte de todos os objects, ou seja, aqueles fora do agregado, podem Dispose seu object. Tudo o que você precisa fazer é certificar-se de que os objects dentro do limite todos sabem quem é o responsável por descartá-lo. Este deve ser um problema bastante fácil de resolver, já que normalmente você os implementa em conjunto e toma cuidado para manter os limites agregados razoavelmente “curtos”.

E quanto à sugestão de que o criador de um object IDisposable também deveria descartá-lo?

Esta diretriz soa razoável e há uma simetria atraente, mas por si só, muitas vezes não funcionará na prática. Indiscutivelmente, significa o mesmo que dizer: “Nunca passe uma referência a um object IDisposable para algum outro object”, porque assim que você fizer isso, você corre o risco de que o object receptor assuma sua propriedade e o dispense sem o seu conhecimento.

Vamos examinar dois tipos de interface proeminentes da Biblioteca de Classes Base .NET (BCL) que claramente violam esta regra básica: IEnumerable e IObservable . Ambos são essencialmente fábricas que retornam objects IDisposable :

  • IEnumerator IEnumerable.GetEnumerator()
    (Lembre-se que o IEnumerator herda de IDisposable .)

  • IDisposable IObservable.Subscribe(IObserver observer)

Em ambos os casos, espera-se que o responsável pela chamada elimine o object retornado. Indiscutivelmente, nossa diretriz simplesmente não faz sentido no caso de fábricas de objects … a menos que, talvez, exijamos que o solicitante (não seu criador imediato) do IDisposable libere.

Incidentalmente, esse exemplo também demonstra os limites da solução agregada descrita acima: Tanto IEnumerable quanto IObservable são de natureza geral demais para fazer parte de um agregado. Agregados são geralmente muito específicos do domínio.

Outros resources e ideias:

  • Em UML, “tem um” relacionamento entre objects pode ser modelado de duas maneiras: como agregação (diamante vazio) ou como composição (diamante preenchido). A composição difere da agregação em que o tempo de vida do object contido / referido termina com o do contêiner / referenciador. Sua pergunta original implicou em agregação (“propriedade transferível”), enquanto eu direcionei principalmente para soluções que usam composição (“propriedade fixa”). Veja o artigo da Wikipedia sobre “Composição de objects” .

  • O Autofac (um contêiner .NET IoC ) resolve esse problema de duas maneiras: comunicando-se, usando o chamado tipo de relacionamento Owned , que adquire a propriedade sobre um IDisposable ; ou através do conceito de unidades de trabalho, chamadas escopos vitalícios no Autofac.

  • Em relação a este último, Nicholas Blumhardt, o criador do Autofac, escreveu “Um Primer Autofac Lifetime” , que inclui uma seção “IDisposable e propriedade”. O artigo completo é um excelente tratado sobre questões de propriedade e vida útil no .NET. Eu recomendo lê-lo, mesmo para quem não está interessado em Autofac.

  • Em C ++, o idioma do RAI (Resource Acquisition Is Initialization) (em geral) e os tipos de ponteiro inteligente (em particular) ajudam o programador a obter os problemas de tempo de vida e propriedade do object. Infelizmente, eles não são transferíveis para o .NET, porque o .NET não possui o suporte elegante do C ++ para a destruição de objects determinísticos.

  • Veja também esta resposta à pergunta sobre o Stack Overflow, “Como responder às necessidades de implementação discrepantes?” , que (se eu entendi corretamente) segue um pensamento semelhante ao da minha resposta baseada em Agregado: Construindo um componente grosseiro em torno do IDisposable tal forma que ele seja completamente contido (e oculto do consumidor do componente) dentro dele.

Em geral, quando você está lidando com um object Descartável, não está mais no mundo ideal de código gerenciado, em que a propriedade vitalícia é um ponto discutível. Resultantly, você precisa considerar o object logicamente “possui”, ou é responsável pelo tempo de vida do seu object descartável.

Geralmente, no caso de um object descartável que é passado para um método, eu diria que não, o método não deve descartar o object porque é muito raro que um object assuma a propriedade de outro object e seja feito com ele no mesmo método. O chamador deve ser responsável pelo descarte nesses casos.

Não há resposta automática que diga “Sim, descarte sempre” ou “Não, nunca descarte” ao falar sobre os dados do membro. Em vez disso, você precisa pensar sobre os objects em cada caso específico e se perguntar: “Este object é responsável pela vida útil do object descartável?”

A regra prática é que o object responsável pela criação de um descartável o possui e, portanto, é responsável por descartá-lo posteriormente. Isso não é válido se houver uma transferência de propriedade. Por exemplo:

 public class Foo { public MyClass BuildClass() { var dispObj = new DisposableObj(); var retVal = new MyClass(dispObj); return retVal; } } 

Foo é claramente responsável por criar o dispObj , mas está passando a propriedade para a instância do MyClass .

Este é um follow-up para a minha resposta anterior ; veja sua observação inicial para saber por que estou postando outra.

Minha resposta anterior tem uma coisa certa: cada IDisposable deve ter um “dono” exclusivo, que será responsável por descartá-lo exatamente uma vez. O gerenciamento de objects IDisposable se torna muito comparável ao gerenciamento de memory em cenários de código não gerenciados.

A tecnologia predecessora do .NET, o Component Object Model (COM), usava o seguinte protocolo para responsabilidades de gerenciamento de memory entre objects:

  • “Os parâmetros internos devem ser alocados e liberados pelo chamador.
  • “Out-parâmetros devem ser alocados pelo chamado; eles são liberados pelo chamador […].
  • “In-out-parâmetros são inicialmente alocados pelo chamador e, em seguida, liberados e realocados pelo chamado, se necessário. Como é verdade para os parâmetros out, o chamador é responsável por liberar o valor final retornado.”

(Existem regras adicionais para casos de erro; consulte a página vinculada acima para detalhes.)

Se fôssemos adaptar essas diretrizes para IDisposable , poderíamos estabelecer o seguinte…

Regras relativas à propriedade IDisposable :

  1. Quando um IDisposable é passado para um método por meio de um parâmetro regular, não há transferência de propriedade. O método chamado pode usar o IDisposable , mas não deve Dispose lo (nem passar a propriedade; consulte a regra 4 abaixo).
  2. Quando um IDisposable é retornado de um método por meio de um parâmetro out ou do valor de retorno, a propriedade é transferida do método para o responsável pela chamada. O chamador terá que Dispose lo (ou passar a propriedade sobre o IDisposable da mesma maneira).
  3. Quando um IDisposable é fornecido a um método por meio de um parâmetro ref , a propriedade sobre ele é transferida para esse método. O método deve copiar o IDisposable em uma variável local ou campo de object e, em seguida, definir o parâmetro ref como null .

Uma regra possivelmente importante é a seguinte:

  1. Se você não tiver propriedade, não deverá transmiti-la. Isso significa que, se você recebeu um object IDisposable por meio de um parâmetro regular, não coloque o mesmo object em um parâmetro ref IDisposable nem o exponha por meio de um valor de retorno ou parâmetro out .

Exemplo:

 sealed class LineReader : IDisposable { public static LineReader Create(Stream stream) { return new LineReader(stream, ownsStream: false); } public static LineReader Create(ref TStream stream) where TStream : Stream { try { return new LineReader(stream, ownsStream: true); } finally { stream = null; } } private LineReader(Stream stream, bool ownsStream) { this.stream = stream; this.ownsStream = ownsStream; } private Stream stream; // note: must not be exposed via property, because of rule (2) private bool ownsStream; public void Dispose() { if (ownsStream) { stream?.Dispose(); } } public bool TryReadLine(out string line) { throw new NotImplementedException(); // read one text line from `stream` } } 

Esta class tem dois methods estáticos de fábrica e, portanto, permite que o cliente escolha se deseja manter ou transmitir a propriedade:

  • Um aceita um object Stream por meio de um parâmetro regular. Isso sinaliza ao chamador que a propriedade não será assumida. Assim, o chamador precisa Dispose :

     using (var stream = File.OpenRead("Foo.txt")) using (var reader = LineReader.Create(stream)) { string line; while (reader.TryReadLine(out line)) { Console.WriteLine(line); } } 
  • Aquele que aceita um object Stream por meio de um parâmetro ref . Isso sinaliza ao chamador que a propriedade será transferida, portanto, o chamador não precisa Dispose :

     var stream = File.OpenRead("Foo.txt"); using (var reader = LineReader.Create(ref stream)) { string line; while (reader.TryReadLine(out line)) { Console.WriteLine(line); } } 

    Curiosamente, se o stream foi declarado como uma variável de using : using (var stream = …) , a compilation falharia porque o using variables ​​não pode ser passado como parâmetros ref , portanto, o compilador C # ajuda a impor nossas regras neste caso específico.

Finalmente, note que File.OpenRead é um exemplo para um método que retorna um object IDisposable (ou seja, um Stream ) através do valor de retorno, de modo que a propriedade sobre o stream retornado é transferida para o chamador.

Desvantagem:

A principal desvantagem desse padrão é que o AFAIK, noone, o utiliza (ainda). Portanto, se você interagir com qualquer API que não siga as regras acima (por exemplo, a Biblioteca de Classes Base do .NET Framework), ainda precisará ler a documentação para descobrir quem deve chamar Dispose em objects IDisposable .

Uma coisa que decidi fazer antes de saber muito sobre programação em .NET, mas ainda parece ser uma boa idéia, é ter um construtor que aceita um IDisposable também aceitar um booleano que diga se a propriedade do object também será transferida. Para objects que podem existir inteiramente dentro do escopo de using instruções, isso geralmente não será muito importante (já que o object externo será descartado dentro do escopo do bloco Using do object Inner, não há necessidade de o object externo descartar o interior um, na verdade, pode ser necessário que não o faça). Essa semântica pode se tornar essencial, no entanto, quando o object externo será passado como uma class de interface ou base para um código que não conhece a existência do object interno. Nesse caso, o object interno deve viver até que o object externo seja destruído, e a coisa que sabe que o object interno deve morrer quando o object externo faz é o object externo em si, então o object externo tem que ser capaz de destruir o interior.

Desde então, tive algumas idéias adicionais, mas não as experimentei. Eu ficaria curioso sobre o que as outras pessoas pensam:

  1. Um wrapper de contagem de referência para um object IDisposable . Eu realmente não descobri o padrão mais natural para fazer isso, mas se um object usa contagem de referência com incrementos / decrementos intertravados, e se (1) todo o código que manipula o object usa-o corretamente e (2) não há referências cíclicas são criados usando o object, eu esperaria que deveria ser possível ter um object IDisposable compartilhado que é destruído quando o último uso é ignorado. Provavelmente, o que deve acontecer é que a class public deve ser um wrapper para uma class contada de referência privada, e deve suportar um construtor ou método factory que criará um novo wrapper para a mesma instância base (aumentando a contagem de referência da instância em um ). Ou, se a class precisar ser limpa mesmo quando os wrappers forem abandonados e se a class tiver alguma rotina de pesquisa periódica, a class poderá manter uma lista de WeakReference s em seus wrappers e verificar se pelo menos alguns deles ainda existem .
  2. Faça com que o construtor de um object IDisposable aceite um delegado que será chamado na primeira vez em que o object for descartado (um object IDisposable deve usar Interlocked.Exchange no sinalizador isDisposed para garantir que ele seja descartado exatamente uma vez). Esse delegado poderia então cuidar de descartar quaisquer objects nesteds (possivelmente com um cheque para ver se alguém ainda os mantinha).

Algum desses parece um bom padrão?