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 DbConnection
em um using
bloco é 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 IDisposable
como tal:
class Foo : IDisposable {
DbConnection dbConn;
public void Dispose()
{
dbConn.Dispose();
}
}
e o que é pior, todos os usuários de Foo
precisam se lembrar de incluir Foo
um using
bloco, 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?)
fonte
Respostas:
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
fonte
manual
gerenciamento 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.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
using
palavra - chave, que surgiu bastante tarde no design do que se tornou o C # 1.0. AIDisposable
coisa toda é bastante miserável, e se pergunta se seria mais eleganteusing
trabalhar com a sintaxe do destruidor C ++~
marcando aquelas classes às quais oIDisposable
padrão da placa da caldeira poderia ser aplicado automaticamente?fonte
~
sintaxe para ser um açúcar sintático paraIDisposable.Dispose()
~
como açúcar sintático paraIDisposable.Dispose()
, e é muito mais conveniente do que a sintaxe C #.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.
Foo
eFoo*
ou entrefoo.bar
efoo->bar
.clone()
. Muitos objetos simplesmente não precisam ser copiados. Por exemplo, imutáveis não.)private
construtores de cópia eoperator=
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.swap
funções. (A menos que você esteja escrevendo uma rotina de classificação.)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 para
Dispose
( 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ônimogcnew FileStream("filename.ext")
e o C ++ / CLI não o descartará automaticamente.fonte
using
declaraçã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 referenciadoIDisposable
daqueles que não possuem; substituir ou abandonar um local de armazenamento que possui um referenciadoIDisposable
deve descartar o destino na ausência de uma diretiva em contrário.new Date(oldDate.getTime())
.O Java7 introduziu algo semelhante ao C #
using
: A instrução try-with-resourcesEntão eu acho que eles não escolheram conscientemente não implementar o RAII ou mudaram de idéia enquanto isso.
fonte
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 implementarAutoCloseable
...using
nã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.using
/ try-with-resources não sendo o mesmo que RAII.using
e é mal não estão perto RAII.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?
myObject
for um objeto baseado em pilha local, o construtor de cópia será chamado (se o resultado for atribuído a algo).myObject
é um objeto baseado em pilha local e estamos retornando uma referência, o resultado é indefinido.myObject
for um membro / objeto global, o construtor de cópia é chamado (se o resultado for atribuído a algo).myObject
for um membro / objeto global e retornarmos uma referência, a referência será retornada.myObject
é um ponteiro para um objeto baseado em pilha local, o resultado é indefinido.myObject
for um ponteiro para um membro / objeto global, esse ponteiro será retornado.myObject
for um ponteiro para um objeto baseado em heap, esse ponteiro será retornado.Agora, o que o mesmo código faz em Java?
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.
fonte
Sua descrição dos orifícios de
using
está incompleta. Considere o seguinte problema:Na minha opinião, não ter RAII e GC foi uma má ideia. Quando se trata de fechamento de arquivos em Java, é
malloc()
efree()
ali.fonte
using
clá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).free()
nofinally
.IEnumerable
não foi herdadoIDisposable
e havia vários iteradores especiais que nunca puderam ser implementados como resultado.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.
fonte
Dispose
todos os campos marcados com umausing
diretiva e especificar seIDisposable.Dispose
deve chamá-lo automaticamente; (3) uma diretiva semelhante ausing
, mas que somente chamariaDispose
em caso de exceção; (4) uma variação doIDisposable
qual levaria umException
parâmetro, e ...using
se apropriado; o parâmetro serianull
se ousing
bloco 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.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.
fonte
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.
fonte
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.
fonte
Você já chamou o equivalente aproximado disso em C # com o
Dispose
método Java também temfinalize
. OBSERVAÇÃO: Eu percebo que o finalize do Java é não determinístico e diferenteDispose
, 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.
fonte