Os desenvolvedores de Java abandonaram conscientemente o RAII?

80

Como um programador C # de longa data, recentemente aprendi mais sobre as vantagens de Aquisição de recursos é inicialização (RAII). Em particular, 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
}

significa que lembrar de incluir o uso de recursos como DbConnection em um bloco using é desnecessário em C ++! Isso parece 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 que Foo implementasse IDisposable como tal:

class Foo : IDisposable {
    DbConnection dbConn;

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

e o que é pior, todos os usuários de Foo precisariam se lembrar de incluir Foo em um bloco using , como:

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

Agora, observando o C # e suas raízes Java, estou imaginando ... os desenvolvedores de Java apreciaram completamente o que estavam desistindo quando abandonaram a pilha em favor do heap, abandonando o RAII?

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

    
por JoelFan 07.11.2011 / 15:45
fonte

11 respostas

38

Now looking at C# and its Java roots I am wondering... did the developers of Java fully appreciate what they were giving up when they abandoned the stack in favor of the heap, thus abandoning RAII?

(Similarly, did Stroustrup fully appreciate the significance of RAII?)

Tenho certeza de que Gosling não obteve o significado de RAII no momento em que projetou o Java. Em suas entrevistas, ele frequentemente falava sobre razões para deixar de fora genéricos e sobrecarga de operadores, mas nunca mencionou destruidores deterministas e RAII.

Engraçado o suficiente, mesmo a Stroustrup não estava ciente da importância dos destruidores deterministas no momento em que os projetou. Não consigo encontrar a citação, mas se você estiver realmente interessada, pode encontrá-la entre as entrevistas dele aqui: link

    
por 07.11.2011 / 16:41
fonte
58

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

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

Em segundo lugar, mesmo que você reconheça que há vantagens, há complexidades de implementação significativas e ineficiências envolvidas na escrituração. Você não pode realmente colocar objetos semelhantes a pilha na pilha em uma linguagem gerenciada. Você fica com "semântica de pilha" e se compromete com um trabalho significativo (os tipos de valor já são bastante difíceis, pense em um objeto que é uma instância de uma classe complexa, com referências entrando e voltando à memória gerenciada).

Por causa disso, você não quer uma finalização determinística em cada objeto em um sistema de programação onde "(quase) tudo é um objeto". Então você faz tem que introduzir algum tipo de sintaxe controlada por programador para separar um objeto normalmente rastreado de um que tenha finalização determinística.

Em C #, você tem a palavra-chave using , que chegou bem atrasada no design do que se tornou C # 1.0. A coisa IDisposable inteira é muito ruim, e alguém se pergunta se seria mais elegante ter using trabalhando com a sintaxe do destruidor C ++ ~ marcando aquelas classes para as quais o padrão IDisposable da placa boiler poderia ser aplicado automaticamente ?

    
por 07.11.2011 / 19:43
fonte
39

Tenha em mente que o Java foi desenvolvido em 1991-1995 quando o C ++ era uma linguagem muito diferente. Exceções (que tornaram o RAII ) e modelos (o que facilitou a implementação de ponteiros inteligentes) eram recursos "novos e emaranhados". A maioria dos programadores de C ++ vieram de C e estavam acostumados a fazer gerenciamento manual de memória.

Então eu duvido que os desenvolvedores de Java deliberadamente decidam abandonar o RAII. Foi, no entanto, uma decisão deliberada de Java preferir semântica de referência em vez de semântica de valor. A destruição determinista é difícil de implementar em uma linguagem de semântica de referência.

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

Porque isso torna a linguagem muito mais simples.

  • Não há necessidade de uma distinção sintática entre Foo e Foo* ou entre foo.bar e foo->bar .
  • Não há necessidade de atribuição sobrecarregada, quando toda atribuição é copiada de um ponteiro.
  • Não há necessidade de construtores de cópia. (Há ocasionalmente a necessidade de 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 é necessário declarar private copy construtores e operator= para tornar uma classe não copiável. Se você não quer que objetos de uma classe sejam copiados, você simplesmente não escreve uma função para copiá-lo.
  • Não há necessidade de swap funções. (A menos que você esteja escrevendo uma rotina de classificação.)
  • Não há necessidade de referências de valor de R ++ no estilo C ++.
  • 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 semântica de referência é que, quando cada objeto potencialmente tem várias referências a ele, fica difícil saber quando excluí-lo. Você praticamente tem para ter gerenciamento automático de memória.

O Java optou por usar um coletor de lixo não determinista.

O GC não pode ser determinista?

Sim, pode. Por exemplo, a implementação em C do Python usa a contagem de referência. Mais tarde, foi adicionado o rastreamento do GC para lidar com o lixo cíclico, em que as refcontagens falham.

Mas a contabilidade é terrivelmente ineficiente. Muitos ciclos da CPU passaram a atualizar as contagens. Ainda pior em um ambiente multi-threaded (como o tipo Java foi projetado para) onde 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 em C ++, isso pode parecer uma escolha errada. Mas lembre-se que muito do público-alvo para Java era programadores C (ou "C com classes") que estavam acostumados a fechar explicitamente essas coisas.

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

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

    
por 08.11.2011 / 06:51
fonte
20

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

a try statement that declares one or more resources. A resource is as an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource...

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

    
por 07.11.2011 / 16:52
fonte
17

Sua descrição dos furos de using está 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 fechar arquivos em Java, é malloc() e free() .

    
por 07.11.2011 / 16:43
fonte
17

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

Devido a isso e ao fato de Java ser coletado como lixo, a finalização determinística é mais ou menos impossível (ex. E se meu objeto "local" for referenciado em outro lugar? Então quando o método terminar, nós não quero que seja destruído) .

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

Por que Java não tem 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 myObject for um objeto local baseado em pilha, o construtor de cópia será chamado (se o resultado for atribuído a algo).
  • Se myObject for um objeto local baseado em pilha e retornarmos uma referência, o resultado será indefinido.
  • Se myObject for um objeto membro / global, o construtor de cópia será chamado (se o resultado for atribuído a algo).
  • Se myObject for um objeto membro / global e retornarmos uma referência, a referência será retornada.
  • Se myObject for um ponteiro para um objeto baseado em pilha local, o resultado será indefinido.
  • Se myObject for um ponteiro para um objeto membro / global, esse ponteiro será retornado.
  • Se myObject for um ponteiro para um objeto baseado em heap, esse ponteiro será retornado.

Agora, o que faz o mesmo código 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 porque os objetos baseados em pilha são uma causa muito comum de erros de programação em C ++. Por causa disso, os projetistas de Java os eliminaram; e sem eles, não faz sentido usar o RAII em Java.

    
por 07.11.2011 / 21:50
fonte
14

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

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

agora eles adicionaram Finalisers à especificação de idioma e o Java viu alguma adoção.

É claro que depois foi dito a todos que não colocassem os finalizadores em seus objetos porque isso reduziu a velocidade do GC tremendamente. (como era necessário não apenas bloquear o heap, mas também mover os objetos para serem finalizados para uma área temporária, pois esses métodos não poderiam ser chamados porque o GC pausou a execução do aplicativo. Em vez disso, eles seriam chamados imediatamente antes do próximo GC cycle) (e pior, às vezes o finalizador nunca seria chamado quando o aplicativo estava sendo desligado. Imagine não ter seu identificador de arquivo fechado, nunca)

Em seguida, tivemos o C # e lembro-me do fórum de discussão no MSDN, onde nos foi dito como essa nova linguagem C # era maravilhosa. Alguém perguntou por que não havia finalização determinista e os garotos do MS nos disseram como não precisávamos de tais coisas, depois nos disseram que precisávamos mudar nossa maneira de projetar aplicativos, depois nos disseram como a GC era incrível 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 acrescentado esse padrão IDispose às especificações que poderíamos usar. Eu pensei que era praticamente de volta ao gerenciamento de memória manual para nós em aplicativos C # naquele momento.

Naturalmente, os meninos da MS descobriram mais tarde que tudo o que eles nos disseram era ... bem, eles fizeram IDispor um pouco mais do que apenas uma interface padrão, e mais tarde adicionaram a declaração using. W00t! Eles perceberam que a finalização determinista era algo que faltava na língua, afinal. Claro, você ainda tem que lembrar de colocá-lo em todos os lugares, então ainda é um pouco manual, mas é melhor.

Então, por que eles fizeram isso quando poderiam ter semânticas no estilo de uso colocadas automaticamente em cada bloco de escopo desde o início? Provavelmente eficiência, mas gosto de pensar que eles simplesmente não percebem. Assim como eventualmente eles perceberam que você ainda precisa de ponteiros inteligentes no .NET (google SafeHandle) eles achavam que o GC realmente resolveria todos os problemas. Eles esqueceram que um objeto é mais do que apenas memória e que o GC é projetado principalmente para gerenciar o gerenciamento de memória. eles ficaram presos na idéia de que o GC lidaria com isso, e esqueceram que você colocou 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 eu também acho que a falta de um método finalize no Java original tinha um pouco mais - que os objetos que você criava eram tudo sobre memória, e se você quisesse apagar alguma outra coisa (como um manipulador DB ou um soquete ou qualquer outra coisa), então era esperado que você fizesse manualmente .

Lembre-se de que o Java foi projetado para ambientes incorporados onde as pessoas estavam acostumadas a escrever código C com muitas alocações manuais, portanto, não ter acesso automático não era um grande problema - nunca o fizeram antes, então por que você precisa dele? Java? O problema não tem nada a ver com threads, ou stack / heap, ele provavelmente estava lá apenas para facilitar a alocação de memória (e, portanto, desalocar). No geral, a instrução try / finally é provavelmente um lugar melhor para lidar com recursos que não são de memória.

Então IMHO, a maneira com que 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.

    
por 20.03.2012 / 17:36
fonte
11

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

...To understand how the language can be both unpleasant and complicated, and well designed at the same time, you must keep in mind the primary design decision upon which everything in C++ hung: compatibility with C. Stroustrup decided -- and correctly so, it would appear -- that the way to get the masses of C programmers to move to objects was to make the move transparent: to allow them to compile their C code unchanged under C++. This was a huge constraint, and has always been C++'s greatest strength ... and its bane. It's what made C++ as successful as it was, and as complex as it is.

It also fooled the Java designers who didn't understand C++ well enough. For example, they thought operator overloading was too hard for programmers to use properly. Which is basically true in C++, because C++ has both stack allocation and heap allocation and you must overload your operators to handle all situations and not cause memory leaks. Difficult indeed. Java, however, has a single storage allocation mechanism and a garbage collector, which makes operator overloading trivial -- as was shown in C# (but had already been shown in Python, which predated Java). But for many years, the partly line from the Java team was "Operator overloading is too complicated." This and many other decisions where someone clearly didn't do their homework is why I have a reputation for disdaining many of the choices made by Gosling and the Java team.

There are plenty of other examples. Primitives "had to be included for efficiency." The right answer is to stay true to "everything is an object" and provide a trap door to do lower-level activities when efficiency was required (this would also have allowed for the hotspot technologies to transparently make things more efficient, as they eventually would have). Oh, and the fact that you can't use the floating point processor directly to calculate transcendental functions (it's done in software instead). I've written about issues like this as much as I can stand, and the answer I hear has always been some tautological reply to the effect that "this is the Java way."

When I wrote about how badly generics were designed, I got the same response, along with "we must be backwards compatible with previous (bad) decisions made in Java." Lately more and more people have gained enough experience with Generics to see that they really are very hard to use -- indeed, C++ templates are much more powerful and consistent (and much easier to use now that compiler error messages are tolerable). People have even been taking reification seriously -- something that would be helpful but won't put that much of a dent in a design that is crippled by self-imposed constraints.

The list goes on to the point where it's just tedious...

    
por 07.11.2011 / 20:00
fonte
10

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

Você não pode passar objetos alocados em pilha para outros tópicos.

Pare e pense sobre isso. Continue pensando ... Agora o C ++ não tinha threads quando todos estavam tão interessados em RAII. Mesmo Erlang (pilhas separadas por thread) fica nojento quando você passa muitos objetos ao redor. C ++ só tem um modelo de memória em C ++ 2011; agora você pode quase argumentar sobre a simultaneidade em C ++ sem ter que se referir à "documentação" do seu compilador.

O Java foi projetado a partir do (quase) dia um para vários threads.

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

O segundo motivo doloroso é evitar o corte.

    
por 07.11.2011 / 22:25
fonte
5

Em C ++, você usa mais recursos de linguagem de nível geral e mais gerais (destrutores chamados automaticamente em objetos baseados em pilha) para implementar um RAII (nível mais alto), e essa abordagem é algo que o pessoal C # / Java parece não gostar muito de. Eles preferem projetar ferramentas específicas de alto nível para necessidades específicas e fornecê-las aos programadores prontos, embutidos na linguagem. O problema com essas ferramentas específicas é que muitas vezes elas são impossíveis de personalizar (em parte, é isso que as torna tão fáceis de aprender). Ao construir a partir de blocos menores, uma solução melhor pode aparecer com o tempo, enquanto se você tiver somente tem construções internas de alto nível, isso é menos provável.

Então, sim, eu acho que (eu não estava realmente lá ...) foi uma decisão consciente, com o objetivo de tornar as línguas mais fáceis de entender, mas na minha opinião, foi uma má decisão. Então, de novo, eu geralmente prefiro a filosofia C ++ dar-os-programadores-a-chance-para-rolar-seus, então eu sou um pouco tendencioso.

    
por 07.11.2011 / 17:25
fonte
-1

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

Se qualquer coisa C ++ se torna mais uma dor embora porque um objeto tem que ser fisicamente destruído. Em linguagens de alto nível 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 garantia de que o objeto DBConnection em C ++ não tenha referências ou ponteiros falsos para ele.

Sim, o código C ++ pode ser mais intuitivo de ler, mas pode ser um pesadelo para depurar, pois os limites e limitações que linguagens como Java implementam eliminam alguns dos bugs mais agravantes e difíceis, além de proteger outros desenvolvedores de problemas comuns. erros de novato.

Talvez se trate de preferências, algumas como o baixo nível de poder, controle e pureza do C ++, onde outros como eu preferem uma linguagem mais restrita que é muito mais explícita.

    
por 07.11.2011 / 16:06
fonte