Por que o paradigma destruidor de objetos nas linguagens coletadas de lixo está ausente de maneira generalizada?

27

Procurando informações sobre decisões sobre o design da linguagem coletada pelo lixo. Talvez um especialista em idiomas possa me esclarecer? Eu venho de um background em C ++, então essa área é desconcertante para mim.

Parece que quase todas as linguagens modernas de coleta de lixo com suporte a objetos OOPy, como Ruby, Javascript / ES6 / ES7, Actionscript, Lua etc., omitem completamente o paradigma destruidor / finalizado. Python parece ser o único com seu class __del__()método. Por que é isso? Existem limitações funcionais / teóricas em idiomas com coleta automática de lixo que impedem implementações efetivas de um método destruidor / finalizador em objetos?

Acho extremamente carente que essas linguagens considerem a memória como o único recurso que vale a pena gerenciar. E quanto a soquetes, identificadores de arquivo, estados de aplicativos? Sem a capacidade de implementar lógica personalizada para limpar recursos e estados que não sejam de memória na finalização de objetos, sou obrigado a agrupar meu aplicativo com myObject.destroy()chamadas de estilo personalizado , colocando a lógica de limpeza fora da minha "classe", interrompendo a tentativa de encapsulamento e relegando meu aplicativo para vazamentos de recursos devido a erro humano, em vez de ser tratado automaticamente pelo gc.

Quais são as decisões de design de linguagem que levam a essas linguagens não ter nenhuma maneira de executar lógica personalizada no descarte de objetos? Eu tenho que imaginar que há uma boa razão. Eu gostaria de entender melhor as decisões técnicas e teóricas que resultaram nessas linguagens sem suporte para destruição / finalização de objetos.

Atualizar:

Talvez seja uma maneira melhor de expressar minha pergunta:

Por que uma linguagem teria o conceito interno de instâncias de objetos com estruturas de classe ou de classe, juntamente com instanciação personalizada (construtores), mas omitir completamente a funcionalidade de destruição / finalização? Os idiomas que oferecem coleta automática de lixo parecem ser os principais candidatos para apoiar a destruição / finalização de objetos, pois sabem com 100% de certeza quando um objeto não está mais em uso. No entanto, a maioria desses idiomas não o suporta.

Eu não acho que seja um caso em que o destruidor nunca seja chamado, pois isso seria um vazamento de memória principal, que os gcs foram projetados para evitar. Pude ver um possível argumento: o destruidor / finalizador pode não ser chamado até um tempo indeterminado no futuro, mas isso não impediu o Java ou o Python de suportar a funcionalidade.

Quais são os principais motivos de design de linguagem para não oferecer suporte a nenhuma forma de finalização de objeto?

dbcb
fonte
9
Talvez porque finalize/ destroyé uma mentira? Não há garantia de que algum dia será executado. E, mesmo que você não saiba quando (dada a coleta automática de lixo), e se o contexto necessário ainda estiver lá (ele já pode ter sido coletado). Portanto, é mais seguro garantir um estado consistente de outras maneiras, e é possível forçar o programador a fazê-lo.
Raphael
11
Eu acho que essa pergunta é off-line. É uma questão de design de linguagem de programação do tipo que queremos entreter, ou é uma pergunta para um site mais orientado para a programação? Votos da comunidade, por favor.
Raphael
14
É uma boa pergunta no design de PL, vamos lá.
Andrej Bauer 21/01
3
Esta não é realmente uma distinção estática / dinâmica. Muitas linguagens estáticas não possuem finalizadores. De fato, os idiomas com finalizadores não são minoria?
Andrej Bauer 21/01
11
acho que há alguma pergunta aqui ... seria melhor se você definisse os termos um pouco mais. java tem um bloco finalmente que não está vinculado à destruição de objetos, mas à saída do método. também existem outras maneiras de lidar com recursos. por exemplo, em java, um pool de conexões pode lidar com conexões que não são usadas [x] muito tempo e recuperá-las. não é elegante, mas funciona. parte da resposta à sua pergunta é que a coleta de lixo é aproximadamente um processo não determinístico, não instantâneo e não é direcionada por objetos que não estão mais sendo usados, mas por restrições / tetos de memória serem acionados.
vzn

Respostas:

10

O padrão do qual você está falando, em que os objetos sabem como limpar seus recursos, se enquadra em três categorias relevantes. Não vamos confundir destruidores com finalizadores - apenas um está relacionado à coleta de lixo:

  • O padrão finalizador : método de limpeza declarado automaticamente, definido pelo programador, chamado automaticamente.

    Os finalizadores são chamados automaticamente antes da desalocação por um coletor de lixo. O termo se aplica se o algoritmo de coleta de lixo empregado puder determinar os ciclos de vida do objeto.

  • O método destructor pattern : cleanup declarado automaticamente, definido pelo programador, chamado automaticamente apenas algumas vezes.

    Os destruidores podem ser chamados automaticamente para objetos alocados à pilha (porque a vida útil do objeto é determinística), mas devem ser chamados explicitamente em todos os caminhos de execução possíveis para objetos alocados em heap (porque a vida útil do objeto não é determinística).

  • O padrão de descarte : método de limpeza declarado, definido e chamado pelo programador.

    Os programadores fazem um método de descarte e chamam a si mesmos - é aqui que o seu myObject.destroy()método personalizado se encaixa. Se o descarte for absolutamente necessário, os descartadores devem ser chamados em todos os caminhos de execução possíveis.

Os finalizadores são os dróides que você está procurando.

O padrão do finalizador (o padrão que sua pergunta está perguntando) é o mecanismo para associar objetos aos recursos do sistema (soquetes, descritores de arquivo etc.) para recuperação mútua por um coletor de lixo. Porém, os finalizadores estão fundamentalmente à mercê do algoritmo de coleta de lixo em uso.

Considere esta sua suposição:

Os idiomas que oferecem coleta automática de lixo ... sabem com 100% de certeza quando um objeto não está mais em uso.

Tecnicamente falso (obrigado, @babou). A coleta de lixo é fundamentalmente sobre memória, não objetos. Se ou quando um algoritmo de coleção percebe que a memória de um objeto não está mais em uso, depende do algoritmo e (possivelmente) de como seus objetos se referem um ao outro. Vamos falar sobre dois tipos de coletores de lixo em tempo de execução. Existem várias maneiras de alterar e aumentar essas técnicas básicas:

  1. Rastreamento GC. Esses rastreiam a memória, não os objetos. A menos que aumentado para isso, eles não mantêm referências anteriores a objetos da memória. A menos que aumentados, esses GCs não saberão quando um objeto pode ser finalizado, mesmo que saibam quando sua memória está inacessível. Portanto, as chamadas para finalizador não são garantidas.

  2. Contagem de referência GC . Eles usam objetos para rastrear a memória. Eles modelam a acessibilidade de objetos com um gráfico direcionado de referências. Se houver um ciclo no seu gráfico de referência de objetos, todos os objetos do ciclo nunca terão seu finalizador chamado (até o término do programa, obviamente). Novamente, as chamadas do finalizador não são garantidas.

TLDR

A coleta de lixo é difícil e diversificada. Uma chamada de finalizador não pode ser garantida antes do encerramento do programa.

kdbanman
fonte
Você está certo de que isso não é estático v. Dinâmico. É um problema com os idiomas coletados pelo lixo. A coleta de lixo é um problema complexo e provavelmente é o principal motivo, pois há muitos casos extremos a serem considerados (por exemplo, o que acontece se a lógica finalize()fizer com que o objeto que está sendo limpo seja referenciado novamente?). No entanto, não foi possível garantir a chamada do finalizador antes da finalização do programa não impediu o Java de suportá-lo. Não dizendo que sua resposta está incorreta, apenas incompleta. Ainda é um post muito bom. Obrigado.
dbcb
Obrigado pelo feedback. Aqui está uma tentativa de concluir minha resposta: omitindo explicitamente os finalizadores, um idioma força seus usuários a gerenciar seus próprios recursos. Para muitos tipos de problemas, isso provavelmente é uma desvantagem. Pessoalmente, prefiro a escolha do Java, porque tenho o poder de finalizadores e não há nada que me impeça de escrever e usar meu próprio triturador. Java está dizendo: "Ei, programador. Você não é um idiota, então aqui está um finalizador. Apenas tome cuidado".
kdbanman
11
Atualizei minha pergunta original para refletir que isso lida com os idiomas coletados pelo lixo. Aceitando sua resposta. Obrigado por reservar um tempo para responder.
Dbcb
Feliz por ajudar. O meu esclarecimento de comentários tornou minha resposta mais clara?
Kdbanman
2
É bom. Para mim, a resposta real aqui é que as linguagens optam por não implementá-lo porque o valor percebido não supera os problemas de implementação da funcionalidade. Não é impossível (como demonstram Java e Python), mas há uma troca que muitas linguagens optam por não fazer.
Dbcb
5

Em poucas palavras

A finalização não é uma questão simples de ser tratada pelos coletores de lixo. É fácil de usar com o GC de contagem de referência, mas essa família de GC geralmente é incompleta, exigindo que as perdas de memória sejam compensadas pelo acionamento explícito da destruição e finalização de alguns objetos e estruturas. O rastreamento de coletores de lixo é muito mais eficaz, mas torna muito mais difícil identificar o objeto a ser finalizado e destruído, em vez de apenas identificar a memória não utilizada, exigindo, portanto, um gerenciamento mais complexo, com um custo no tempo e no espaço e na complexidade de a implementação.

Introdução

Suponho que o que você está perguntando é por que as linguagens de coleta de lixo não tratam automaticamente a destruição / finalização no processo de coleta de lixo, conforme indicado pela observação:

Acho extremamente carente que essas linguagens considerem a memória como o único recurso que vale a pena gerenciar. E quanto a soquetes, identificadores de arquivo, estados de aplicativos?

Não concordo com a resposta aceita dada por kdbanman . Embora os fatos declarados sejam na maioria corretos, embora fortemente tendenciosos à contagem de referências, não creio que eles expliquem adequadamente a situação reclamada na pergunta.

Não acredito que a terminologia desenvolvida nessa resposta seja um problema, e é mais provável que confunda as coisas. De fato, como apresentado, a terminologia é determinada principalmente pela maneira como os procedimentos são ativados, e não pelo que eles fazem. O ponto é que, em todos os casos, há a necessidade de finalizar um objeto que não é mais necessário com algum processo de limpeza e liberar quaisquer recursos que esteja utilizando, sendo a memória apenas um deles. Idealmente, tudo isso deve ser feito automaticamente quando o objeto não for mais utilizado, por meio de um coletor de lixo. Na prática, o GC pode estar ausente ou apresentar deficiências, e isso é compensado pelo disparo explícito pelo programa de finalização e recuperação.

A exploração explícita pelo programa é um problema, pois pode permitir erros de programação difíceis de analisar, quando um objeto ainda em uso está sendo explicitamente finalizado.

Portanto, é muito melhor contar com a coleta automática de lixo para recuperar recursos. Mas há dois problemas:

  • alguma técnica de coleta de lixo permitirá vazamentos de memória que impedem a recuperação completa de recursos. Isso é bem conhecido pela referência na contagem de GC, mas pode aparecer para outras técnicas de GC ao usar algumas organizações de dados sem cuidados (ponto não discutido aqui).

  • embora a técnica de GC possa ser boa para identificar recursos de memória que não são mais usados, a finalização de objetos contidos nela pode não ser simples e isso complica o problema de recuperar outros recursos usados ​​por esses objetos, que geralmente é o objetivo da finalização.

Finalmente, um ponto importante frequentemente esquecido é que os ciclos de GC podem ser acionados por qualquer coisa, não apenas pela falta de memória, se os ganchos adequados forem fornecidos e se o custo de um ciclo de GC for considerado valioso. Portanto, não há problema em iniciar um GC quando estiver faltando algum tipo de recurso, na esperança de liberar alguns.

Referência contando coletores de lixo

A contagem de referência é uma técnica fraca de coleta de lixo , que não manipula os ciclos adequadamente. Seria, de fato, fraco na destruição de estruturas obsoletas e na recuperação de outros recursos simplesmente porque é fraco na recuperação de memória. Porém, os finalizadores podem ser usados ​​com mais facilidade com um coletor de lixo de contagem de referência (GC), pois um GC de ref-count recupera uma estrutura quando seu número de ref desce para 0, quando seu endereço é conhecido juntamente com seu tipo, estaticamente ou dinamicamente. Portanto, é possível recuperar a memória precisamente após a aplicação do finalizador adequado e a chamada recursiva do processo em todos os objetos apontados (possivelmente através do procedimento de finalização).

Em poucas palavras, a finalização é fácil de implementar com o GC de contagem de referência, mas sofre com a "incompletude" desse GC, devido a estruturas circulares, exatamente na mesma extensão que a recuperação de memória sofre. Em outras palavras, com contagem de referência, a memória é precisamente tão mal gerenciada quanto outros recursos , como soquetes, identificadores de arquivo, etc.

De fato, a incapacidade do GC de ref count para recuperar estruturas em loop (em geral) pode ser vista como vazamento de memória . Você não pode esperar que todo o GC evite vazamentos de memória. Depende do algoritmo do GC e das informações da estrutura de tipos disponíveis dinamicamente (por exemplo, no GC conservador ).

Rastreando coletores de lixo

A família mais poderosa do GC, sem esses vazamentos, é a família de rastreamento que explora as partes vivas da memória, começando por indicadores de raiz bem identificados. Todas as partes da memória que não são visitadas nesse processo de rastreamento (que podem realmente ser decompostas de várias maneiras, mas preciso simplificar) são partes não utilizadas da memória que podem ser recuperadas 1 . Esses coletores recuperam todas as partes da memória que não podem mais ser acessadas pelo programa, não importa o que ele faça. Ele recupera estruturas circulares, e o GC mais avançado é baseado em alguma variação desse paradigma, às vezes altamente sofisticado. Pode ser combinado com a contagem de referência em alguns casos e compensar suas fraquezas.

Um problema é que a sua declaração (no final da pergunta):

Os idiomas que oferecem coleta automática de lixo parecem ser os principais candidatos para apoiar a destruição / finalização de objetos, pois sabem com 100% de certeza quando um objeto não está mais em uso.

é tecnicamente incorreto para rastrear coletores.

O que se sabe com 100% de certeza é que partes da memória não estão mais em uso . (Mais precisamente, deve-se dizer que eles não estão mais acessíveis , porque algumas partes, que não podem mais ser usadas de acordo com a lógica do programa, ainda são consideradas em uso se ainda houver um ponteiro inútil para elas no programa .) Mas são necessários processamento adicional e estruturas apropriadas para saber quais objetos não utilizados podem ter sido armazenados nessas partes da memória que agora não são usadas . Isso não pode ser determinado pelo que é conhecido do programa, pois o programa não está mais conectado a essas partes da memória.

Assim, após um passo na coleta de lixo, você fica com fragmentos de memória que contêm objetos que não estão mais em uso, mas não há como, a priori, saber o que são esses objetos para aplicar a finalização correta. Além disso, se o coletor de rastreamento for do tipo de marcação e varredura, pode ser que alguns dos fragmentos possam conter objetos que já foram finalizados em uma passagem anterior do GC, mas não foram utilizados desde então por motivos de fragmentação. No entanto, isso pode ser resolvido usando a digitação explícita estendida.

Embora um coletor simples recupere esses fragmentos de memória, sem mais delongas, a finalização exige uma passagem específica para explorar essa memória não utilizada, identificar os objetos ali contidos e aplicar procedimentos de finalização. Mas tal exploração requer determinação do tipo de objetos que foram armazenados lá e também é necessária a determinação do tipo para aplicar a finalização adequada, se houver.

Isso implica custos extras no tempo do GC (a passagem extra) e possivelmente custos extras de memória para disponibilizar informações de tipo adequadas durante essa passagem por diversas técnicas. Esses custos podem ser significativos, pois muitas vezes é necessário finalizar apenas alguns objetos, enquanto a sobrecarga de tempo e espaço pode envolver todos os objetos.

Outro ponto é que a sobrecarga de tempo e espaço pode estar relacionada à execução do código do programa, e não apenas à execução do GC.

Não posso dar uma resposta mais precisa, apontando para questões específicas, porque não conheço as especificidades de muitos dos idiomas listados. No caso de C, a digitação é uma questão muito difícil que leva ao desenvolvimento de colecionadores conservadores. Meu palpite seria que isso também afeta C ++, mas eu não sou especialista em C ++. Isso parece ser confirmado por Hans Boehm, que fez grande parte da pesquisa sobre GC conservador. O GC conservador não pode recuperar sistematicamente toda a memória não utilizada precisamente porque pode não ter informações precisas sobre o tipo de dados. Pelo mesmo motivo, não seria possível aplicar sistematicamente os procedimentos de finalização.

Portanto, é possível fazer o que você está perguntando, como você sabe em alguns idiomas. Mas não vem de graça. Dependendo do idioma e de sua implementação, isso pode acarretar um custo, mesmo quando você não usa o recurso. Várias técnicas e trade-offs podem ser consideradas para resolver esses problemas, mas isso está além do escopo de uma resposta de tamanho razoável.

1 - esta é uma apresentação abstrata da coleção de rastreio (abrangendo o GC de copiar e marcar e varrer), as coisas variam de acordo com o tipo de coletor de rastreio, e explorar a parte não utilizada da memória é diferente, dependendo de copiar ou marcar e varredura é usada.

babou
fonte
Você fornece muitos detalhes excelentes sobre a coleta de lixo. No entanto, sua resposta não está de acordo com a minha - seu resumo e meu TLDR estão basicamente dizendo a mesma coisa. E para o que vale a pena, minha resposta usa o GC de contagem de referência como exemplo, não um "forte viés".
Kdbanman
Depois de ler mais detalhadamente, vejo o desacordo. Vou editar de acordo. Além disso, minha terminologia era inequívoca. A pergunta era finalizadores e destruidores conflitantes, e até mencionavam descartadores na mesma respiração. Vale a pena espalhar as palavras certas.
Kdbanman
@kdbanman A dificuldade era que eu estava abordando vocês dois, já que sua resposta estava como referência. Você não pode usar ref count como um exemplo paradigmático porque é um GC fraco, raramente usado em idiomas (verifique os idiomas citados pelo OP), para os quais a adição de finalizadores seria realmente fácil (mas com uso limitado). Os coletores de rastreamento são quase sempre usados. Mas os finalizadores são difíceis de prendê-los, porque os objetos moribundos não são conhecidos (ao contrário da afirmação que você considera correta). A distinção entre tipagem estática e dinâmica é irrelevante, pois a digitação dinâmica do armazenamento de dados é essencial.
babou 5/02/2015
@kdbanman Em relação à terminologia, é útil em geral, pois corresponde a diferentes situações. Mas aqui isso não ajuda, pois a questão é transferir a finalização para o GC. O GC básico deve fazer apenas a destruição. O que é necessário é uma terminologia que distingue getting memory recycled, que eu chamo reclamation, e faça alguma limpeza antes disso, como recuperar outros recursos ou atualizar algumas tabelas de objetos, que eu chamofinalization . Essas questões me pareciam relevantes, mas posso ter perdido um ponto em sua terminologia que era novo para mim.
babou 5/02/2015
11
Obrigado @kdbanman, babou. Boa discussão. Acho que as duas postagens abordam pontos semelhantes. Como vocês apontam, o problema principal parece ser a categoria de coletor de lixo empregado no tempo de execução do idioma. Encontrei este artigo , que esclarece alguns conceitos errados para mim. Parece que os gcs mais robustos lidam apenas com memória bruta de baixo nível, o que torna os tipos de objetos de nível superior opacos ao gc. Sem o conhecimento das memórias internas, o gc não pode destruir objetos. Qual parece ser sua conclusão.
Dbcb
4

O padrão destruidor de objetos é fundamental para o tratamento de erros na programação de sistemas, mas não tem nada a ver com a coleta de lixo. Em vez disso, tem a ver com corresponder a vida útil do objeto a um escopo e pode ser implementado / usado em qualquer idioma que possua funções de primeira classe.

Exemplo (pseudocódigo). Suponha que você tenha um tipo de "arquivo bruto", como o tipo de descritor de arquivo Posix. Há quatro operações fundamentais, open(), close(), read(), write(). Você gostaria de implementar um tipo de arquivo "seguro" que sempre limpa depois de si próprio. (Ou seja, que possui um construtor e destruidor automático.)

Assumirei que nosso idioma tem tratamento de exceção com throw, trye finally(em idiomas sem tratamento de exceção, você pode configurar uma disciplina em que o usuário do seu tipo retorna um valor especial para indicar um erro.)

Você configura uma função que aceita uma função que faz o trabalho. A função worker aceita um argumento (um identificador para o arquivo "seguro").

with_file_opened_for_read (string:   filename,
                           function: worker_function(safe_file f)):
  raw_file rf = open(filename, O_RDONLY)
  if rf == error:
    throw File_Open_Error

  try:
    worker_function(rf)
  finally:
    close(rf)

Você também fornece implementações de read()e write()parasafe_file (que apenas chamam raw_file read()e write()). Agora o usuário usa o safe_filetipo assim:

...
with_file_opened_for_read ("myfile.txt",
                           anonymous_function(safe_file f):
                             mytext = read(f)
                             ... (including perhaps throwing an error)
                          )

Um destruidor de C ++ é realmente apenas açúcar sintático para um try-finally bloco. Praticamente tudo o que fiz aqui é converter o que uma safe_fileclasse C ++ com um construtor e destruidor compilaria. Observe que o C ++ não possui finallysuas exceções, especificamente porque Stroustrup achava que o uso de um destruidor explícito era melhor sintaticamente (e ele o introduziu no idioma antes que o idioma tivesse funções anônimas).

(Essa é uma simplificação de uma das maneiras pelas quais as pessoas lidam com erros em linguagens do tipo Lisp há muitos anos. Acho que a encontrei no final dos anos 80 ou início dos anos 90, mas não me lembro onde.)

Lógica Errante
fonte
Isso descreve os elementos internos do padrão destruidor baseado em pilha no C ++, mas não explica por que uma linguagem de coleta de lixo não implementaria essa funcionalidade. Você pode estar certo de que isso não tem nada a ver com a coleta de lixo, mas está relacionado à destruição / finalização geral de objetos, que parece ser difícil ou ineficiente nos idiomas de coleta de lixo. Portanto, se a destruição geral não for suportada, a destruição baseada em pilha também será omitida.
dbcb
Como eu disse no começo: qualquer linguagem de coleta de lixo que possua funções de primeira classe (ou alguma aproximação das funções de primeira classe) oferece a capacidade de fornecer interfaces "à prova de balas" como safe_filee with_file_opened_for_read(um objeto que se fecha quando sai do escopo ) O importante é que não tem a mesma sintaxe que os construtores é irrelevante. Lisp, Scheme, Java, Scala, Go, Haskell, Rust, Javascript, Clojure suportam funções suficientes de primeira classe, portanto, não precisam de destruidores para fornecer o mesmo recurso útil.
Wandering Logic
Eu acho que vejo o que você está dizendo. Como as linguagens fornecem os blocos de construção básicos (try / catch / finalmente, funções de primeira classe etc.) para implementar manualmente a funcionalidade do tipo destruidor, eles não precisam de destruidores? Pude ver alguns idiomas seguindo esse caminho por razões de simplicidade. Embora, pareça improvável, esse seja o principal motivo de todos os idiomas listados, mas talvez seja isso. Talvez eu seja apenas a grande minoria que ama destruidores de C ++ e ninguém mais se importa, o que poderia muito bem ser a razão pela qual a maioria das linguagens não implementa destruidores. Eles simplesmente não se importam.
dbcb 9/02/2015
2

Essa não é uma resposta completa para a pergunta, mas eu gostaria de adicionar algumas observações que não foram abordadas nas outras respostas ou comentários.

  1. A pergunta supõe implicitamente que estamos falando de uma linguagem orientada a objetos no estilo Simula, que é ela mesma limitadora. Na maioria dos idiomas, mesmo aqueles com objetos, nem tudo é um objeto. O mecanismo para implementar destruidores imporia um custo que nem todo implementador de linguagem está disposto a pagar.

  2. O C ++ tem algumas garantias implícitas sobre a ordem de destruição. Se você tiver uma estrutura de dados semelhante a uma árvore, por exemplo, os filhos serão destruídos antes do pai. Esse não é o caso nas linguagens do GC, portanto, os recursos hierárquicos podem ser liberados em uma ordem imprevisível. Para recursos que não sejam de memória, isso pode importar.

Pseudônimo
fonte
2

Quando as duas estruturas de GC mais populares (Java e .NET) estavam sendo projetadas, acho que os autores esperavam que a finalização funcionasse bem o suficiente para evitar a necessidade de outras formas de gerenciamento de recursos. Muitos aspectos do design de linguagem e estrutura podem ser bastante simplificados se não houver necessidade de todos os recursos necessários para acomodar um gerenciamento de recursos 100% confiável e determinístico. No C ++, é necessário distinguir entre os conceitos de:

  1. Ponteiro / referência que identifica um objeto que pertence exclusivamente ao detentor da referência e que não é identificado por nenhum ponteiro / referência que o proprietário não conheça.

  2. Ponteiro / referência que identifica um objeto compartilhável que não pertence exclusivamente a ninguém.

  3. Ponteiro / referência que identifica um objeto que pertence exclusivamente ao detentor da referência, mas ao qual pode ser acessado através de "visualizações", o proprietário não tem como rastrear.

  4. Ponteiro / referência que identifica um objeto que fornece uma visão de um objeto que pertence a outra pessoa.

Se uma linguagem / estrutura do GC não precisar se preocupar com o gerenciamento de recursos, todas as opções acima podem ser substituídas por um único tipo de referência.

Eu achava ingênua a ideia de que a finalização eliminaria a necessidade de outras formas de gerenciamento de recursos, mas, se essa expectativa era razoável ou não na época, a história mostrou desde então que existem muitos casos que exigem gerenciamento de recursos mais preciso do que a finalização fornece. . Por acaso, acho que as recompensas de reconhecer a propriedade no nível da linguagem / estrutura seriam suficientes para justificar o custo (a complexidade precisa existir em algum lugar, e movê-la para a linguagem / estrutura simplificaria o código do usuário), mas reconheço que existem diferenças significativas. os benefícios de design de ter um único "tipo" de referência - algo que só funciona se a linguagem / estrutura não for independente de problemas de limpeza de recursos.

supercat
fonte
2

Por que o paradigma destruidor de objetos nas linguagens coletadas de lixo está ausente de maneira generalizada?

Eu venho de um background em C ++, então essa área é desconcertante para mim.

O destruidor em C ++ realmente faz duas coisas combinadas. Ele libera RAM e identificações de recursos.

Outros idiomas separam essas preocupações, fazendo com que o GC seja responsável por liberar RAM, enquanto outro recurso de idioma se encarrega de liberar IDs de recursos.

Acho extremamente carente que essas linguagens considerem a memória como o único recurso que vale a pena gerenciar.

É disso que se trata o GC. Eles só possuem uma coisa e é garantir que você não fique sem memória. Se a RAM for infinita, todos os GCs serão aposentados, pois não há mais motivo real para sua existência.

E quanto a soquetes, identificadores de arquivo, estados de aplicativos?

Os idiomas podem fornecer diferentes maneiras de liberar os IDs de recursos:

  • manual .CloseOrDispose()espalhado pelo código

  • manual .CloseOrDispose()espalhado dentro do " finallybloco" manual

  • manuais "blocos de ID de recurso" (ou seja using, with, try-com-recursos , etc.) que automatiza .CloseOrDispose()após o bloco é feito

  • garantidos "blocos de ID de recurso" que automatiza.CloseOrDispose() após o bloco é feito

Muitos idiomas usam mecanismos manuais (e não garantidos), o que cria uma oportunidade para a má administração de recursos. Pegue este código simples do NodeJS:

require('fs').openSync('file1.txt', 'w');
// forget to .closeSync the opened file

..onde o programador esqueceu de fechar o arquivo aberto.

Enquanto o programa continuar em execução, o arquivo aberto ficará preso no limbo. Isso é fácil de verificar, tentando abrir o arquivo usando o HxD e verificando se não pode ser feito:

insira a descrição da imagem aqui

A liberação de identificações de recursos nos destruidores de C ++ também não é garantida. Você pode pensar que o RAII opera como "blocos de identificação de recurso" garantidos, mas, diferentemente de "blocos de identificação de recurso", a linguagem C ++ não impede que o objeto que fornece o vazamento do bloco RAII, portanto, o bloco RAII pode nunca ser feito .


Parece que quase todas as linguagens modernas de coleta de lixo com suporte a objetos OOPy, como Ruby, Javascript / ES6 / ES7, Actionscript, Lua etc., omitem completamente o paradigma destruidor / finalizado. Python parece ser o único com seu __del__()método de classe . Por que é isso?

Porque eles gerenciam os IDs de recursos de outras maneiras, conforme mencionado acima.

Quais são as decisões de design de linguagem que levam a essas linguagens não ter nenhuma maneira de executar lógica personalizada no descarte de objetos?

Porque eles gerenciam os IDs de recursos de outras maneiras, conforme mencionado acima.

Por que uma linguagem teria o conceito interno de instâncias de objetos com estruturas de classe ou de classe, juntamente com instanciação personalizada (construtores), mas omitir completamente a funcionalidade de destruição / finalização?

Porque eles gerenciam os IDs de recursos de outras maneiras, conforme mencionado acima.

Pude ver um possível argumento: o destruidor / finalizador pode não ser chamado até um tempo indeterminado no futuro, mas isso não impediu o Java ou o Python de suportar a funcionalidade.

Java não tem destruidores.

Os documentos Java mencionam :

o objetivo usual de finalizar, no entanto, é executar ações de limpeza antes que o objeto seja descartado irrevogavelmente. Por exemplo, o método finalize para um objeto que representa uma conexão de entrada / saída pode executar transações explícitas de E / S para interromper a conexão antes que o objeto seja descartado permanentemente.

..mas colocar o código de gerenciamento de identificação de recurso Object.finalizeré amplamente considerado um antipadrão ( cf. ). Esse código deve ser escrito no site da chamada.

Para as pessoas que usam o antipadrão, a justificativa é que elas podem ter esquecido de liberar os IDs de recurso no site de chamada. Assim, eles fazem isso novamente no finalizador, apenas por precaução.

Quais são os principais motivos de design de linguagem para não oferecer suporte a nenhuma forma de finalização de objeto?

Não há muitos casos de uso para finalizadores, pois eles são para executar um pedaço de código entre o momento em que não há mais referências fortes ao objeto e o momento em que sua memória é recuperada pelo GC.

Um possível caso de uso é quando você deseja manter um registro do tempo entre o objeto coletado pelo GC e o momento em que não há mais nenhuma referência forte ao objeto, como tal:

finalize() {
    Log(TimeNow() + ". Obj " + toString() + " is going to be memory-collected soon!"); // "soon"
}
Pacerier
fonte
-1

encontrou uma referência a isso no Dr. Dobbs wrt c ++ que tem idéias mais gerais que argumentam que os destruidores são problemáticos em uma linguagem em que são implementados. uma idéia aproximada aqui parece ser que o principal objetivo dos destruidores é lidar com a desalocação de memória, e isso é difícil de realizar corretamente. a memória é alocada por partes, mas objetos diferentes são conectados e as responsabilidades / limites da desalocação não são tão claras.

portanto, a solução para isso de um coletor de lixo evoluiu anos atrás, mas a coleta de lixo não se baseia em objetos que desaparecem do escopo nas saídas do método (que é uma idéia conceitual difícil de implementar), mas em um coletor executando periodicamente, de maneira um tanto indeterminada, quando o aplicativo tiver "pressão de memória" (ou seja, ficar sem memória).

em outras palavras, o mero conceito humano de um "objeto recentemente não utilizado" é, de certo modo, uma abstração enganosa no sentido de que nenhum objeto pode "instantaneamente" tornar-se não utilizado. objetos não utilizados só podem ser "descobertos" executando um algoritmo de coleta de lixo que atravessa o gráfico de referência de objeto e os algoritmos com melhor desempenho são executados de forma intermitente.

é possível que um algoritmo de coleta de lixo melhor esteja esperando para ser descoberto, capaz de identificar quase instantaneamente objetos não utilizados, o que poderia levar a um código de chamada de destruidor consistente, mas um não foi encontrado após muitos anos de pesquisa na área.

a solução para áreas de gerenciamento de recursos, como arquivos ou conexões, parece ter "gerenciadores" de objetos que tentam lidar com seu uso.

vzn
fonte
2
Achado interessante. Obrigado. O argumento do autor é baseado no destruidor ser chamado no momento errado, devido à passagem de instâncias de classe por valor, onde a classe não possui um construtor de cópia adequado (o que é um problema real). No entanto, esse cenário realmente não existe na maioria das linguagens dinâmicas modernas (se não todas), porque tudo é passado por referência, o que evita a situação do autor. Embora essa seja uma perspectiva interessante, acho que não explica por que a maioria das linguagens de coleta de lixo optou por omitir a funcionalidade destruidor / finalização.
dbcb
2
Esta resposta deturpa o artigo do Dr. Dobb: o artigo não argumenta que os destruidores são problemáticos em geral. O artigo realmente argumenta o seguinte: As primitivas de gerenciamento de memória são como instruções goto, porque são simples, mas poderosas demais. Da mesma maneira que as instruções goto são melhor encapsuladas em "estruturas de controle apropriadamente limitadas" (Veja: Dijktsra), as primitivas de gerenciamento de memória são melhor encapsuladas em "estruturas de dados adequadamente limitadas". Os destruidores são um passo nessa direção, mas não o suficiente. Decida por si mesmo se isso é verdade ou não.
kdbanman