Os desenvolvedores de Java conscientemente abandonaram o RAII?

82

Como programador de longa data em C #, recentemente vim a aprender mais sobre as vantagens da RAII ( Resource Acquisition Is Initialization ). Em particular, eu descobri que o idioma C #:

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

tem o equivalente em C ++:

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

significando que lembrar de incluir o uso de recursos como DbConnectionem um usingbloco é desnecessário no C ++! Essa parece ser uma grande vantagem do C ++. Isso é ainda mais convincente quando você considera uma classe que possui um membro de instância do tipo DbConnection, por exemplo

class Foo {
    DbConnection dbConn;

    // ...
}

Em C #, eu precisaria ter o Foo implementado IDisposablecomo tal:

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

e o que é pior, todos os usuários de Fooprecisam se lembrar de incluir Fooum usingbloco, como:

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

Agora, olhando para C # e suas raízes Java, estou me perguntando ... os desenvolvedores de Java apreciaram completamente o que estavam desistindo quando abandonaram a pilha em favor da pilha, abandonando o RAII?

(Da mesma forma, Stroustrup apreciou completamente o significado do RAII?)

JoelFan
fonte
5
Não tenho certeza do que você está falando com não incluir recursos em C ++. O objeto DBConnection provavelmente manipula o fechamento de todos os recursos em seu destruidor.
Maple_shaft
16
@ maple_shaft, exatamente o meu ponto! Essa é a vantagem do C ++ que estou abordando nesta questão. Em C #, você precisa colocar recursos em "using" ... em C ++, não.
JoelFan #
12
Meu entendimento é que o RAII, como estratégia, só foi entendido quando os compiladores C ++ foram bons o suficiente para realmente usar o modelo avançado, que está bem depois do Java. o C ++ que estava realmente disponível para uso quando o Java foi criado era um estilo "C com classes" muito primitivo, talvez com modelos básicos, se você tiver sorte.
Sean McMillan
6
"Meu entendimento é que o RAII, como estratégia, só foi entendido quando os compiladores C ++ foram bons o suficiente para realmente usar modelos avançados, que estão bem depois do Java". - Isso não está realmente correto. Construtores e destruidores têm sido os principais recursos do C ++ desde o primeiro dia, muito antes do uso generalizado de modelos e muito antes do Java.
Jim No Texas
8
@ JimInTexas: Eu acho que Sean tem uma semente básica da verdade em algum lugar (embora não modelos, mas exceções seja o ponto crucial). Os construtores / destruidores estavam lá desde o início, mas não havia importância e o conceito de RAII inicialmente (qual é a palavra que eu estou procurando) realizado. Levou alguns anos e algum tempo para os compiladores melhorarem antes de percebermos o quão crucial é toda a RAII.
Martin York

Respostas:

38

Agora, olhando para C # e suas raízes Java, estou me perguntando ... os desenvolvedores de Java apreciaram completamente o que estavam desistindo quando abandonaram a pilha em favor da pilha, abandonando o RAII?

(Da mesma forma, Stroustrup apreciou completamente o significado do RAII?)

Tenho certeza de que Gosling não entendeu o significado do RAII no momento em que projetou o Java. Em suas entrevistas, ele costumava falar sobre razões para deixar de fora os genéricos e a sobrecarga de operadores, mas nunca mencionou destruidores determinísticos e RAII.

Engraçado o bastante, mesmo Stroustrup não estava ciente da importância dos destruidores determinísticos no momento em que os projetou. Não consigo encontrar a citação, mas se você realmente gosta, pode encontrá-la nas entrevistas dele aqui: http://www.stroustrup.com/interviews.html

Nemanja Trifunovic
fonte
11
@ maple_shaft: Em suma, não é possível. Exceto se você inventou uma maneira de ter uma coleta de lixo determinística (o que parece impossível em geral e invalida todas as otimizações de GC das últimas décadas em qualquer caso), seria necessário introduzir objetos alocados à pilha, mas isso abre várias latas de worms: esses objetos precisam de semântica, "problema de fatiamento" com subtipagem (e, portanto, sem polimorfismo), indicadores pendentes, a menos que, talvez, se você colocar restrições significativas ou fazer alterações maciças no sistema de tipos incompatíveis. E isso está no topo da minha cabeça.
13
@DeadMG: Então você sugere que voltemos ao gerenciamento manual de memória. Essa é uma abordagem válida para a programação em geral e, é claro, permite a destruição determinística. Mas isso não responde a essa pergunta, que se preocupa com uma configuração somente de GC que deseja fornecer segurança à memória e comportamento bem definido, mesmo se todos agirmos como idiotas. Isso requer GC para tudo e nenhuma maneira de iniciar a destruição de objetos manualmente (e todo o código Java existente depende, pelo menos, do anterior), para que você faça o GC determinístico ou fique sem sorte.
26
@delan. Eu não chamaria o manualgerenciamento de memória de ponteiros inteligentes em C ++ . Eles são mais como um coletor de lixo determinístico determinável. Se usado corretamente, ponteiros inteligentes são os joelhos das abelhas.
Martin York
10
@LokiAstari: Bem, eu diria que eles são um pouco menos automáticos do que o GC completo (você precisa pensar sobre o tipo de inteligência que realmente deseja) e implementá-los como biblioteca requer ponteiros brutos (e, portanto, gerenciamento manual de memória) para desenvolver . Além disso, não conheço nenhum ponteiro inteligente que lide automaticamente com as referências cíclicas, um requisito estrito para a coleta de lixo nos meus livros. Os indicadores inteligentes são certamente incrivelmente legais e úteis, mas você deve enfrentar que eles não podem fornecer algumas garantias (sejam elas úteis ou não) de uma linguagem completa e exclusiva do GC.
11
@ Delan: Eu tenho que discordar lá. Eu acho que eles são mais automáticos que o GC, pois são determinísticos. ESTÁ BEM. Para ser eficiente, você precisa garantir o uso correto (eu darei a você). O std :: weak_ptr lida com os ciclos perfeitamente. Ciclos é sempre traçado, mas na realidade quase nunca é um problema, porque o objeto base geralmente é baseado em pilha e, quando isso ocorre, arruma o resto. Nos casos raros, pode haver um problema std :: weak_ptr.
Martin York
60

Sim, os designers de C # (e, tenho certeza, Java) decidiram especificamente contra a finalização determinística. Perguntei a Anders Hejlsberg sobre isso várias vezes entre 1999 e 2002.

Primeiro, a idéia de semântica diferente para um objeto com base em sua pilha ou pilha é certamente contrária ao objetivo de design unificador de ambas as linguagens, que era aliviar os programadores exatamente de tais problemas.

Segundo, mesmo que você reconheça que há vantagens, existem complexidades e ineficiências significativas na implementação envolvidas na contabilidade. Você não pode realmente colocar objetos do tipo pilha na pilha em um idioma gerenciado. Você fica dizendo "semântica de pilha" e se comprometendo com um trabalho significativo (os tipos de valor já são difíceis o suficiente, pense em um objeto que é uma instância de uma classe complexa, com referências entrando e retornando à memória gerenciada).

Por isso, você não deseja finalização determinística em todos os objetos de um sistema de programação em que "(quase) tudo seja um objeto". Então você não tem que apresentar algum tipo de sintaxe controlado pelo programador para separar um objeto normalmente acompanhados de um que tem a finalização determinística.

No C #, você tem a usingpalavra - chave, que surgiu bastante tarde no design do que se tornou o C # 1.0. A IDisposablecoisa toda é bastante miserável, e se pergunta se seria mais elegante usingtrabalhar com a sintaxe do destruidor C ++ ~marcando aquelas classes às quais o IDisposablepadrão da placa da caldeira poderia ser aplicado automaticamente?

Larry OBrien
fonte
2
E o que o C ++ / CLI (.NET) fez, onde os objetos no heap gerenciado também têm um "identificador" baseado em pilha, que fornece RIAA?
JoelFan #
3
O C ++ / CLI possui um conjunto muito diferente de decisões e restrições de design. Algumas dessas decisões significam que você pode exigir mais reflexão sobre a alocação de memória e as implicações de desempenho dos programadores: o conjunto "dá a corda suficiente para se enforcar". E imagino que o compilador C ++ / CLI seja consideravelmente mais complexo que o do C # (especialmente em suas primeiras gerações).
quer
5
+1 é a única resposta correta até agora - é porque o Java intencionalmente não possui objetos baseados em pilha (não primitivos).
precisa saber é o seguinte
8
@ Peter Taylor - certo. Mas acho que o destruidor não determinístico do C # vale muito pouco, pois você não pode confiar nele para gerenciar qualquer tipo de recurso restrito. Então, na minha opinião, poderia ter sido melhor usar a ~sintaxe para ser um açúcar sintático paraIDisposable.Dispose()
Larry OBrien
3
@ Larry: eu concordo. C ++ / CLI não usar ~como açúcar sintático para IDisposable.Dispose(), e é muito mais conveniente do que a sintaxe C #.
dan04
41

Lembre-se de que o Java foi desenvolvido em 1991-1995, quando o C ++ era uma linguagem muito diferente. Exceções (que tornavam o RAII necessário ) e modelos (que tornavam mais fácil a implementação de ponteiros inteligentes) eram recursos "novos". A maioria dos programadores de C ++ vinha do C e estava acostumada a gerenciar manualmente a memória.

Portanto, duvido que os desenvolvedores de Java tenham decidido deliberadamente abandonar o RAII. Foi, no entanto, uma decisão deliberada para Java preferir semântica de referência em vez de semântica de valor. Destruição determinística é difícil de implementar em uma linguagem semântica de referência.

Então, por que usar semântica de referência em vez de semântica de valor?

Porque torna a linguagem muito mais simples.

  • Não há necessidade de uma distinção sintática entre Fooe Foo*ou entre foo.bare foo->bar.
  • Não há necessidade de atribuição sobrecarregada, quando toda a atribuição é copiar um ponteiro.
  • Não há necessidade de construtores de cópia. ( Ocasionalmente, é necessária uma função de cópia explícita como clone(). Muitos objetos simplesmente não precisam ser copiados. Por exemplo, imutáveis ​​não.)
  • Não há necessidade de declarar privateconstrutores de cópia e operator=tornar uma classe não copiável. Se você não deseja copiar objetos de uma classe, simplesmente não escreve uma função para copiá-la.
  • Não há necessidade de swapfunções. (A menos que você esteja escrevendo uma rotina de classificação.)
  • Não há necessidade de referências de rvalue no estilo C ++ 0x.
  • Não há necessidade de (N) RVO.
  • Não há problema de fatiamento.
  • É mais fácil para o compilador determinar layouts de objetos, porque as referências têm um tamanho fixo.

A principal desvantagem da referência à semântica é que, quando todo objeto tem várias referências a ele, fica difícil saber quando excluí-lo. Você praticamente precisa ter um gerenciamento automático de memória.

Java escolheu usar um coletor de lixo não determinístico.

O GC não pode ser determinístico?

Sim pode. Por exemplo, a implementação C do Python usa contagem de referência. E, mais tarde, adicionou o GC de rastreamento para lidar com o lixo cíclico no qual refcounts falham.

Mas recontar é terrivelmente ineficiente. Muitos ciclos de CPU passaram atualizando as contagens. Pior ainda em um ambiente multithread (como o tipo para o qual o Java foi projetado) em que essas atualizações precisam ser sincronizadas. Muito melhor usar o coletor de lixo nulo até precisar mudar para outro.

Você poderia dizer que o Java optou por otimizar o caso comum (memória) à custa de recursos não fungíveis, como arquivos e soquetes. Hoje, à luz da adoção do RAII no C ++, isso pode parecer a escolha errada. Mas lembre-se de que grande parte do público-alvo do Java eram programadores C (ou "C com classes") que estavam acostumados a fechar explicitamente essas coisas.

Mas e os "objetos de pilha" em C ++ / CLI?

Eles são apenas açúcar sintático paraDispose ( link original ), muito parecido com C # using. No entanto, ele não resolve o problema geral de destruição determinística, porque você pode criar um anônimo gcnew FileStream("filename.ext")e o C ++ / CLI não o descartará automaticamente.

dan04
fonte
3
Além disso, links interessantes (especialmente o primeiro, que é altamente relevante para esta discussão) .
BlueRaja - Danny Pflughoeft
A usingdeclaração lida com muitos problemas relacionados à limpeza, mas muitos outros permanecem. Eu sugeriria que a abordagem correta para uma linguagem e estrutura seria distinguir declarativamente entre os locais de armazenamento que "possuem" um referenciado IDisposabledaqueles que não possuem; substituir ou abandonar um local de armazenamento que possui um referenciado IDisposabledeve descartar o destino na ausência de uma diretiva em contrário.
Supercat
1
"Não há necessidade de construtores de cópia" parece bom, mas falha na prática. java.util.Date e Calendar são talvez os exemplos mais notórios. Nada mais adorável que new Date(oldDate.getTime()).
Kevin cline #
2
iow RAII não foi "abandonado", simplesmente não existia para ser abandonado :) Quanto a copiar construtores, eu nunca gostei deles, é fácil demais de errar, eles são uma fonte constante de dores de cabeça quando em algum lugar no fundo de alguém (mais) esqueceu de fazer uma cópia profunda, fazendo com que os recursos sejam compartilhados entre cópias que não deveriam ser.
Jwenting
20

O Java7 introduziu algo semelhante ao C # using: A instrução try-with-resources

uma trydeclaração que declara um ou mais recursos. Um recurso é como um objeto que deve ser fechado após a conclusão do programa. A tryinstrução -with-resources garante que cada recurso seja fechado no final da instrução. Qualquer objeto implementado java.lang.AutoCloseable, que inclui todos os objetos implementados java.io.Closeable, pode ser usado como um recurso ...

Então eu acho que eles não escolheram conscientemente não implementar o RAII ou mudaram de idéia enquanto isso.

Patrick
fonte
Interessante, mas parece que isso funciona apenas com objetos implementados java.lang.AutoCloseable. Provavelmente não é grande coisa, mas não gosto de como isso se sente um pouco restrito. Talvez eu tenha algum outro objeto que deve ser relased automaticamente, mas é muito semanticamente estranho para torná-lo implementar AutoCloseable...
FrustratedWithFormsDesigner
9
@ Patrick: Er, então? usingnão é o mesmo que RAII - em um caso, o chamador se preocupa com a disposição dos recursos; no outro, o chamado lida com isso.
precisa saber é o seguinte
1
+1 eu não sabia sobre tentar com recursos; deve ser útil no descarte de mais clichês.
jprete
3
-1 para using/ try-with-resources não sendo o mesmo que RAII.
9788 Sean McMillan #
4
@ Sean: concordou. usinge é mal não estão perto RAII.
91311 DeadMG
18

O Java intencionalmente não possui objetos baseados em pilha (também conhecidos como objetos de valor). Isso é necessário para que o objeto seja destruído automaticamente no final do método assim.

Devido a isso e ao fato de o Java ser coletado pelo lixo, a finalização determinística é mais ou menos impossível (por exemplo, e se meu objeto "local" se referenciar em outro lugar? Então, quando o método termina, não queremos que ele seja destruído. ) .

No entanto, isso é bom para a maioria de nós, porque quase nunca há necessidade de finalização determinística, exceto ao interagir com recursos nativos (C ++)!


Por que o Java não possui objetos baseados em pilha?

(Diferente de primitivos ..)

Como os objetos baseados em pilha têm semânticas diferentes das referências baseadas em heap. Imagine o seguinte código em C ++; O que isso faz?

return myObject;
  • Se myObjectfor um objeto baseado em pilha local, o construtor de cópia será chamado (se o resultado for atribuído a algo).
  • Se myObjecté um objeto baseado em pilha local e estamos retornando uma referência, o resultado é indefinido.
  • Se myObjectfor um membro / objeto global, o construtor de cópia é chamado (se o resultado for atribuído a algo).
  • Se myObjectfor um membro / objeto global e retornarmos uma referência, a referência será retornada.
  • Se myObjecté um ponteiro para um objeto baseado em pilha local, o resultado é indefinido.
  • Se myObjectfor um ponteiro para um membro / objeto global, esse ponteiro será retornado.
  • Se myObjectfor um ponteiro para um objeto baseado em heap, esse ponteiro será retornado.

Agora, o que o mesmo código faz em Java?

return myObject;
  • A referência a myObjecté retornada. Não importa se a variável é local, membro ou global; e não há objetos baseados em pilha ou casos de ponteiro para se preocupar.

O exemplo acima mostra por que os objetos baseados em pilha são uma causa muito comum de erros de programação em C ++. Por isso, os designers de Java os retiraram; e sem eles, não faz sentido usar RAII em Java.

BlueRaja - Danny Pflughoeft
fonte
6
Não sei o que você quer dizer com "não há sentido em RAII" ... acho que você quer dizer "não há capacidade de fornecer RAII em Java" ... RAII é independente de qualquer linguagem ... não tornar "inútil", porque uma língua particular não fornecê-la
JoelFan
4
Essa não é uma razão válida. Um objeto não precisa estar na pilha para usar o RAII baseado em pilha. Se existir uma "referência única", o destruidor poderá ser acionado assim que sair do escopo. Ver, por exemplo, como funciona com a linguagem de programação D: d-programming-language.org/exception-safe.html
Nemanja Trifunovic
3
@ Nemanja: Um objeto não precisa viver na pilha para ter semântica baseada em pilha, e eu nunca disse que sim. Mas esse não é o problema; o problema, como mencionei, é a própria semântica baseada em pilha.
precisa saber é o seguinte
4
@Aaronaught: O diabo está em "quase sempre" e "na maioria das vezes". Se você não fechar a conexão db e deixar o GC acionar o finalizador, ele funcionará bem com seus testes de unidade e quebrará severamente quando implantado na produção. A limpeza determinística é importante, independentemente do idioma.
Nemanja Trifunovic
8
@NemanjaTrifunovic: Por que você está testando em uma conexão de banco de dados ao vivo? Isso não é realmente um teste de unidade. Não, desculpe, não estou comprando. Você não deve criar conexões de banco de dados em todo o lado, deve passar por construtores ou propriedades, e isso significa que você não deseja uma semântica de autodestruição em pilha. Muito poucos objetos que dependem de uma conexão com o banco de dados devem ser os proprietários. Se a limpeza não determinística o incomoda com tanta frequência, com tanta dificuldade, é por causa do design inadequado do aplicativo, e não do design incorreto da linguagem.
precisa saber é o seguinte
17

Sua descrição dos orifícios de usingestá incompleta. Considere o seguinte problema:

interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?

Na minha opinião, não ter RAII e GC foi uma má ideia. Quando se trata de fechamento de arquivos em Java, é malloc()e free()ali.

DeadMG
fonte
2
Concordo que RAII são os joelhos das abelhas. Mas a usingcláusula é um grande avanço para o C # sobre Java. Ele permite a destruição determinística e, portanto, o gerenciamento correto de recursos (não é tão bom quanto o RAII quanto você precisa lembrar, mas é definitivamente uma boa ideia).
Martin York
8
“Quando se trata de fechar arquivos em Java, é malloc () e free () por lá.” - Absolutamente.
Konrad Rudolph
9
@ KonradRudolph: É pior do que malloc e grátis. Pelo menos em C você não tem exceções.
Nemanja Trifunovic
1
@ Nemanja: Vamos ser justos, você pode free()no finally.
91311 DeadMG
4
@Loki: O problema da classe base é muito mais importante como um problema. Por exemplo, o original IEnumerablenão foi herdado IDisposablee havia vários iteradores especiais que nunca puderam ser implementados como resultado.
91311 DeadMG
14

Eu sou bem velha. Eu estive lá e vi e bati minha cabeça sobre isso muitas vezes.

Eu estava em uma conferência em Hursley Park, onde os garotos da IBM estavam nos dizendo o quão maravilhosa essa nova linguagem Java era, apenas alguém perguntou ... por que não existe um destruidor para esses objetos? Ele não quis dizer o que conhecemos como destruidor em C ++, mas também não havia finalizador (ou ele tinha finalizadores, mas eles basicamente não funcionaram). Isso está de volta e decidimos que o Java era um pouco de uma linguagem de brinquedo naquele momento.

agora eles adicionaram finalizadores à especificação da linguagem e o Java viu alguma adoção.

Obviamente, mais tarde todos foram instruídos a não colocar finalizadores em seus objetos, porque isso diminuiu tremendamente o GC. (como era necessário não apenas bloquear a pilha, mas mover os objetos a serem finalizados para uma área temporária, pois esses métodos não podiam ser chamados porque o GC interrompeu a execução do aplicativo. Em vez disso, eles seriam chamados imediatamente antes da próxima Ciclo do GC) (e pior, às vezes o finalizador nunca seria chamado quando o aplicativo estava sendo desligado. Imagine nunca ter o identificador de arquivos fechado)

Em seguida, tínhamos C #, e lembro-me do fórum de discussão no MSDN, onde nos disseram como era maravilhosa essa nova linguagem C #. Alguém perguntou por que não havia finalização determinística e os meninos do MS nos disseram que não precisávamos de tais coisas, depois nos disseram que precisávamos mudar nossa maneira de projetar aplicativos, depois nos disseram o quão incrível era o GC e como todos os nossos aplicativos antigos eram. lixo e nunca funcionou por causa de todas as referências circulares. Então eles cederam à pressão e nos disseram que haviam adicionado esse padrão IDispose às especificações que poderíamos usar. Eu pensei que estava praticamente voltando ao gerenciamento manual de memória para nós em aplicativos C # naquele momento.

É claro que os garotos do MS descobriram mais tarde que tudo o que haviam nos dito era ... bem, eles fizeram do IDispose um pouco mais do que apenas uma interface padrão e depois adicionaram a instrução using. W00t! Eles perceberam que a finalização determinística era algo que faltava na linguagem, afinal. Claro, você ainda precisa se lembrar de colocá-lo em qualquer lugar, por isso ainda é um pouco manual, mas é melhor.

Então, por que eles fizeram isso quando poderiam ter colocado semânticas de estilo de uso automaticamente colocadas em cada bloco de escopo desde o início? Provavelmente eficiência, mas gosto de pensar que eles simplesmente não perceberam. Assim como acabaram percebendo que você ainda precisa de indicadores inteligentes no .NET (google SafeHandle), eles pensaram que o GC realmente resolveria todos os problemas. Eles esqueceram que um objeto é mais do que apenas memória e que o GC foi projetado principalmente para lidar com o gerenciamento de memória. eles se envolveram com a idéia de que o GC lidaria com isso e esqueceram que você colocava outras coisas lá, um objeto não é apenas uma bolha de memória que não importa se você não o excluir por um tempo.

Mas também acho que a falta de um método finalize no Java original tinha um pouco mais - que os objetos que você criou eram todos sobre memória e se você queria excluir outra coisa (como um identificador de banco de dados, um soquete ou qualquer outra coisa) ), esperava-se que você o fizesse manualmente .

Lembre-se de que o Java foi projetado para ambientes incorporados em que as pessoas estavam acostumadas a escrever código C com muitas alocações manuais, portanto, não ter o acesso automático gratuito não era um grande problema - eles nunca fizeram isso antes, então por que você precisaria dele em Java? O problema não tinha nada a ver com threads ou pilha / pilha, provavelmente estava lá apenas para facilitar a alocação de memória (e, portanto, a desalocação). Ao todo, a instrução try / finalmente é provavelmente um lugar melhor para lidar com recursos que não são de memória.

Então, IMHO, a maneira como o .NET simplesmente copiou a maior falha de Java é sua maior fraqueza. O .NET deveria ter sido um C ++ melhor, não um Java melhor.

gbjbaanb
fonte
IMHO, coisas como 'usar' blocos são a abordagem correta para a limpeza determinística, mas também são necessárias mais algumas coisas: (1) um meio de garantir que os objetos sejam descartados se seus destruidores lançarem uma exceção; (2) um meio de gerar automaticamente um método de rotina para chamar Disposetodos os campos marcados com uma usingdiretiva e especificar se IDisposable.Disposedeve chamá-lo automaticamente; (3) uma diretiva semelhante a using, mas que somente chamaria Disposeem caso de exceção; (4) uma variação do IDisposablequal levaria um Exceptionparâmetro, e ...
supercat 13/07/12
... que seria usado automaticamente usingse apropriado; o parâmetro seria nullse o usingbloco saísse normalmente, ou então indicaria qual exceção estava pendente se saísse pela exceção. Se essas coisas existissem, seria muito mais fácil gerenciar recursos com eficiência e evitar vazamentos.
Supercat
11

Bruce Eckel, autor de "Thinking in Java" e "Thinking in C ++" e membro do C ++ Standards Committee, é de opinião que, em muitas áreas (não apenas na RAII), Gosling e a equipe Java não cumpriram suas promessas. dever de casa.

... Para entender como a linguagem pode ser desagradável e complicada e bem projetada ao mesmo tempo, lembre-se da principal decisão de design na qual tudo em C ++ dependia: a compatibilidade com C. Stroustrup decidiu - e corretamente , parece - que a maneira de fazer com que as massas de programadores C se movessem para os objetos era tornar a mudança transparente: permitir que eles compilassem seu código C inalterado em C ++. Essa foi uma grande restrição, e sempre foi a maior força do C ++ ... e seu problema. É o que tornou o C ++ tão bem-sucedido como era e tão complexo quanto é.

Também enganou os designers de Java que não entendiam C ++ o suficiente. Por exemplo, eles pensaram que a sobrecarga do operador era muito difícil para os programadores usarem corretamente. O que é basicamente verdadeiro no C ++, porque o C ++ possui alocação de pilha e alocação de heap e você deve sobrecarregar seus operadores para lidar com todas as situações e não causar vazamentos de memória. Difícil mesmo. Java, no entanto, possui um único mecanismo de alocação de armazenamento e um coletor de lixo, o que torna a sobrecarga do operador trivial - como foi mostrado em C # (mas já havia sido mostrado em Python, que era anterior a Java). Mas por muitos anos, a linha parcialmente da equipe Java foi "A sobrecarga do operador é muito complicada". Esta e muitas outras decisões em que alguém claramente não

Existem muitos outros exemplos. Os primitivos "precisavam ser incluídos para obter eficiência". A resposta certa é permanecer fiel a "tudo é um objeto" e fornecer um alçapão para realizar atividades de nível inferior quando a eficiência for necessária (isso também permitiria que as tecnologias de hotspots tornassem as coisas transparentemente mais eficientes, como eventualmente ter). Ah, e o fato de você não poder usar o processador de ponto flutuante diretamente para calcular funções transcendentais (é feito em software). Eu escrevi sobre questões como essa o máximo que posso suportar, e a resposta que ouvi sempre foi uma resposta tautológica ao efeito de que "esse é o caminho do Java".

Quando escrevi sobre como os genéricos foram mal projetados, obtive a mesma resposta, juntamente com "devemos ser compatíveis com as decisões anteriores (ruins) tomadas em Java". Ultimamente, mais e mais pessoas adquirem experiência suficiente com os Genéricos para ver que são realmente muito difíceis de usar - na verdade, os modelos C ++ são muito mais poderosos e consistentes (e muito mais fáceis de usar agora que as mensagens de erro do compilador são toleráveis). As pessoas têm levado a sério a reificação - algo que seria útil, mas não prejudicaria tanto um design que é prejudicado por restrições auto-impostas.

A lista continua até o ponto em que é apenas entediante ...

Gnawme
fonte
5
Isso soa como uma resposta Java versus C ++, em vez de focar no RAII. Eu acho que C ++ e Java são linguagens diferentes, cada uma com seus pontos fortes e fracos. Além disso, os designers de C ++ não fizeram a lição de casa em muitas áreas (princípio do KISS não aplicado, mecanismo simples de importação para as classes ausentes etc.). Mas o foco da questão era RAII: isso está faltando em Java e você deve programá-lo manualmente.
Giorgio
4
@ Giorgio: O objetivo do artigo é que Java parece ter perdido o barco em várias questões, algumas das quais diretamente relacionadas ao RAII. Em relação ao C ++ e seu impacto no Java, Eckels observa: "Você deve ter em mente a principal decisão de design sobre a qual tudo dependia do C ++: compatibilidade com C. Essa era uma grande restrição e sempre foi a maior força do C ++ ... e sua Também enganou os designers de Java que não entendiam C ++ o suficiente. " O design do C ++ influenciou diretamente o Java, enquanto o C # teve a oportunidade de aprender com ambos. (Se ele fez isso é outra questão.)
Gnawme
2
@Giorgio O estudo de idiomas existentes em um paradigma e família de idiomas específicos é de fato uma parte da lição de casa necessária para o desenvolvimento de novos idiomas. Este é um exemplo em que eles simplesmente o misturam com Java. Eles tinham C ++ e Smalltalk para examinar. O C ++ não tinha o Java para analisar quando foi desenvolvido.
Jeremy
1
@Gnawme: "Java parece ter perdido o barco em vários problemas, alguns dos quais diretamente relacionados ao RAII": você pode mencionar esses problemas? O artigo que você postou não menciona RAII.
Giorgio
2
@Giorgio Claro, houve inovações desde o desenvolvimento do C ++ que são responsáveis ​​por muitos dos recursos que você acha que faltam lá. Algum desses recursos que eles deveriam ter encontrado em linguagens estabelecidas antes do desenvolvimento do C ++? Esse é o tipo de dever de casa sobre o qual estamos falando com Java - não há razão para eles não considerarem todos os recursos de C ++ no desenvolvimento de Java. Alguns, como herança múltipla, deixaram de fora intencionalmente; outros, como a RAII, parecem ter esquecido.
Jeremy
10

A melhor razão é muito mais simples do que a maioria das respostas aqui.

Você não pode passar objetos alocados da pilha para outros segmentos.

Pare e pense sobre isso. Continue pensando ... Agora, o C ++ não tinha threads quando todo mundo ficou tão interessado no RAII. Até o Erlang (pilhas separadas por segmento) fica nojento quando você passa objetos demais. O C ++ obteve apenas um modelo de memória no C ++ 2011; agora você pode quase raciocinar sobre simultaneidade em C ++ sem precisar consultar a "documentação" do seu compilador.

O Java foi projetado desde (quase) o primeiro dia para vários encadeamentos.

Ainda tenho minha cópia antiga da "linguagem de programação C ++", na qual o Stroustrup me garante que não precisarei de threads.

A segunda razão dolorosa é evitar cortar.

Tim Williscroft
fonte
1
O Java sendo projetado para vários encadeamentos também explica por que o GC não se baseia na contagem de referências.
dan04
4
@NemanjaTrifunovic: Você não pode comparar C ++ / CLI com Java ou C #, ele foi projetado quase com o objetivo expresso de interoperar com código C / C ++ não gerenciado; é mais como uma linguagem não gerenciada que fornece acesso à estrutura .NET do que vice-versa.
Aaronaught 8/11
3
@NemanjaTrifunovic: Sim, o C ++ / CLI é um exemplo de como isso pode ser feito de uma maneira totalmente inadequada para aplicativos normais . É útil apenas para interoperabilidade C / C ++. Não apenas os desenvolvedores normais não precisam se deixar levar por uma decisão totalmente irrelevante de "empilhamento ou pilha", mas se você tentar refatorá-la, será trivialmente fácil criar acidentalmente um ponteiro nulo / erro de referência e / ou vazamento de memória. Desculpe, mas tenho que me perguntar se você realmente programou em Java ou C #, porque não acho que alguém que realmente queira a semântica usada em C ++ / CLI.
Aaronaught 9/11/11
2
@Aaronaught: programei com Java (um pouco) e C # (muito) e meu projeto atual é praticamente todo em C #. Acredite, eu sei do que estou falando e não tem nada a ver com "pilha versus pilha" - tem tudo a ver com garantir que todos os seus recursos sejam liberados assim que você não precisar deles. Automaticamente. Se não estiverem - você terá problemas.
Nemanja Trifunovic
4
@NemanjaTrifunovic: Isso é ótimo, muito bom, mas tanto o C # quanto o C ++ / CLI exigem que você indique explicitamente quando deseja que isso aconteça, eles apenas usam uma sintaxe diferente. Ninguém está contestando o ponto essencial sobre o qual você está divagando no momento (que "os recursos são liberados assim que você não precisa deles"), mas você está dando um salto lógico gigantesco para "todos os idiomas gerenciados devem ter um modo automático, mas apenas disposição determinística baseada na pilha de chamadas ". Apenas não retém água.
Aaronaught 9/11/11
5

No C ++, você usa recursos de linguagem de nível mais baixo de uso geral (destruidores chamados automaticamente em objetos baseados em pilha) para implementar um de nível superior (RAII), e essa abordagem é algo que o pessoal de C # / Java parece não ser gosta demais. Eles preferem projetar ferramentas específicas de alto nível para necessidades específicas e fornecê-las aos programadores prontos, incorporados à linguagem. O problema com essas ferramentas específicas é que elas geralmente são impossíveis de serem personalizadas (em parte é isso que as torna tão fáceis de aprender). Ao construir a partir de blocos menores, uma solução melhor pode surgir com o tempo, enquanto se você tiver apenas construções internas de alto nível, isso é menos provável.

Então, sim, acho que (na verdade não estava lá ...) foi uma decisão consciente, com o objetivo de facilitar o aprendizado dos idiomas, mas, na minha opinião, foi uma decisão ruim. Por outro lado, geralmente prefiro a filosofia C ++ de dar aos programadores uma chance de criar sua própria filosofia, por isso sou um pouco tendenciosa.

imre
fonte
7
A "filosofia de dar aos programadores a chance de criar sua própria filosofia" funciona bem até que você precise combinar bibliotecas escritas por programadores que cada um deles criou suas próprias classes de strings e indicadores inteligentes.
dan04
@ dan04, para que os idiomas gerenciados que lhe dão classes de string predefinidas, permitam que você faça um patch com macaco, o que é uma receita para o desastre se você é o tipo de cara que não consegue lidar com uma string personalizada diferente classe.
Gbjbaanb # 9/13
-1

Você já chamou o equivalente aproximado disso em C # com o Disposemétodo Java também tem finalize. OBSERVAÇÃO: Eu percebo que o finalize do Java é não determinístico e diferente Dispose, apenas estou apontando que ambos têm um método de limpeza de recursos ao lado do GC.

Se alguma coisa em C ++ se torna mais dolorosa, porque um objeto precisa ser fisicamente destruído. Em linguagens de nível superior, como C # e Java, dependemos de um coletor de lixo para limpá-lo quando não houver mais referências a ele. Não existe essa garantia de que o objeto DBConnection no C ++ não tenha referências ou ponteiros desonestos.

Sim, o código C ++ pode ser mais intuitivo de ler, mas pode ser um pesadelo para depuração, porque os limites e as limitações que linguagens como Java colocam em prática excluem alguns dos erros mais agravantes e difíceis, além de proteger outros desenvolvedores de erros comuns de novatos.

Talvez isso se deva a preferências, algumas como o baixo nível de poder, controle e pureza do C ++, onde outras pessoas como eu preferem uma linguagem mais restrita e muito mais explícita.

maple_shaft
fonte
12
Primeiro de tudo Java de "finalizar" é não-determinista ... ele é não o equivalente de 's 'descarte' ou de C ++' destruidores s ... também, C ++ também tem um coletor de lixo se você usar o .NET C #
JoelFan
2
@DeadMG: O problema é que você pode não ser um idiota, mas esse outro cara que acabou de sair da empresa (e que escreveu o código que você agora mantém) pode ter sido.
Kevin
7
Esse cara vai escrever código de merda, o que quer que você faça. Você não pode pegar um programador ruim e fazê-lo escrever um bom código. Ponteiros pendurados são a menor das minhas preocupações quando se lida com idiotas. Os bons padrões de codificação usam ponteiros inteligentes para a memória que precisa ser rastreada; portanto, o gerenciamento inteligente deve tornar óbvio como desalocar e acessar com segurança a memória.
91311 DeadMG
3
O que DeadMG disse. Há muitas coisas ruins sobre C ++. Mas o RAII não é um deles por muito tempo. De fato, a falta de Java e .NET para explicar adequadamente o gerenciamento de recursos (porque a memória é o único recurso, certo?) É um dos maiores problemas.
amigos estão dizendo sobre Konrad Rudolph
8
O finalizador, na minha opinião, é um projeto de desastre. Como você está forçando o uso correto de um objeto do designer para o usuário do objeto (não em termos de gerenciamento de memória, mas de gerenciamento de recursos). Em C ++, é de responsabilidade do designer da classe corrigir o gerenciamento de recursos (feito apenas uma vez). Em Java, é de responsabilidade do usuário da classe obter o gerenciamento de recursos correto e, portanto, deve ser feito sempre que a classe que usamos. stackoverflow.com/questions/161177/…
Martin York '