Pergunta: Por que o Java / C # não pode implementar o RAII?
Esclarecimento: Estou ciente de que o coletor de lixo não é determinístico. Portanto, com os recursos de idioma atuais, não é possível que o método Dispose () de um objeto seja chamado automaticamente na saída do escopo. Mas um recurso tão determinístico poderia ser adicionado?
Meu entendimento:
Eu sinto que uma implementação do RAII deve atender a dois requisitos:
1. A vida útil de um recurso deve estar vinculada a um escopo.
2. Implícito. A liberação do recurso deve ocorrer sem uma declaração explícita do programador. Análogo a um coletor de lixo, liberando memória sem uma declaração explícita. A "implicitude" só precisa ocorrer no ponto de uso da classe. É claro que o criador da biblioteca de classes deve implementar explicitamente um destruidor ou um método Dispose ().
Java / C # satisfaz o ponto 1. No C #, um recurso implementando IDisposable pode ser vinculado a um escopo "using":
void test()
{
using(Resource r = new Resource())
{
r.foo();
}//resource released on scope exit
}
Isso não atende ao ponto 2. O programador deve vincular explicitamente o objeto a um escopo especial "usando". Os programadores podem (e fazem) esquecer de vincular explicitamente o recurso a um escopo, criando um vazamento.
De fato, os blocos "using" são convertidos para o código try-finally-dispose () pelo compilador. Ele tem a mesma natureza explícita do padrão try-finalmente-dispose (). Sem uma liberação implícita, o gancho para um escopo é o açúcar sintático.
void test()
{
//Programmer forgot (or was not aware of the need) to explicitly
//bind Resource to a scope.
Resource r = new Resource();
r.foo();
}//resource leaked!!!
Eu acho que vale a pena criar um recurso de linguagem em Java / C # que permita objetos especiais conectados à pilha por meio de um ponteiro inteligente. O recurso permitiria sinalizar uma classe como vinculada ao escopo, para que ela sempre seja criada com um gancho na pilha. Pode haver opções para diferentes tipos de ponteiros inteligentes.
class Resource - ScopeBound
{
/* class details */
void Dispose()
{
//free resource
}
}
void test()
{
//class Resource was flagged as ScopeBound so the tie to the stack is implicit.
Resource r = new Resource(); //r is a smart-pointer
r.foo();
}//resource released on scope exit.
Eu acho que implícita "vale a pena". Assim como a implicitude da coleta de lixo "vale a pena". Blocos usando explícitos são refrescantes para os olhos, mas não oferecem vantagem semântica sobre o try-finalmente-dispose ().
É impraticável implementar esse recurso nas linguagens Java / C #? Poderia ser introduzido sem quebrar o código antigo?
Dispose
s são sempre executado, independentemente de como eles são acionados. Adicionar destruição implícita no final do escopo não ajudará nisso.using
a execução deDispose
é garantida (bem, descontar o processo de repente morrer sem que uma exceção seja lançada; nesse ponto, toda a limpeza presumivelmente se torna discutível).struct
), mas eles são tipicamente evitado exceto em casos muito especiais. Veja também .Respostas:
Essa extensão de linguagem seria significativamente mais complicada e invasiva do que você imagina. Você não pode simplesmente adicionar
para a seção relevante da especificação do idioma e pronto. Ignorarei o problema dos valores temporários (
new Resource().doSomething()
) que podem ser resolvidos com palavras um pouco mais gerais, esse não é o problema mais sério. Por exemplo, esse código seria quebrado (e esse tipo de coisa provavelmente se torna impossível de fazer em geral):Agora você precisa de construtores de cópia definidos pelo usuário (ou mova construtores) e comece a invocá-los em qualquer lugar. Isso não apenas traz implicações de desempenho, mas também faz com que essas coisas valorizem efetivamente os tipos, enquanto quase todos os outros objetos são tipos de referência. No caso de Java, esse é um desvio radical de como os objetos funcionam. Em C # menos (já possui
struct
s, mas nenhum construtor de cópias definido pelo usuário para eles AFAIK), mas ainda torna esses objetos RAII mais especiais. Como alternativa, uma versão limitada de tipos lineares (cf. Rust) também pode resolver o problema, ao custo de proibir o alias, incluindo a passagem de parâmetros (a menos que você queira introduzir ainda mais complexidade adotando referências emprestadas do tipo Rust e um verificador de empréstimo).Isso pode ser feito tecnicamente, mas você acaba com uma categoria de coisas muito diferentes de tudo o mais no idioma. Isso quase sempre é uma péssima idéia, com consequências para implementadores (mais casos extremos, mais tempo / custo em todos os departamentos) e usuários (mais conceitos a aprender, mais possibilidade de erros). Não vale a conveniência adicional.
fonte
File
esse caminho, nada muda eDispose
nunca é chamado. Se você sempre ligaDispose
, não pode fazer nada com objetos descartáveis. Ou você está propondo algum esquema para algumas vezes descartar e outras não? Em caso afirmativo, descreva-o em detalhes e eu lhe mostrarei situações em que ele falhará.Dispose
se uma referência escapar? A análise de escape é um problema antigo e difícil, que nem sempre funciona sem novas alterações no idioma. Quando uma referência é passada para outro método (virtual) (something.EatFile(f);
), devef.Dispose
ser chamada no final do escopo? Se sim, você interrompe os chamadores que armazenamf
para uso posterior. Caso contrário, você vazará o recurso se o chamador não for armazenadof
. A única maneira um tanto simples de remover isso é um sistema do tipo linear, que (como eu já discuti mais adiante na minha resposta) apresenta muitas outras complicações.A maior dificuldade na implementação de algo assim para Java ou C # seria definir como a transferência de recursos funciona. Você precisaria de alguma maneira para estender a vida útil do recurso além do escopo. Considerar:
O pior é que isso pode não ser óbvio para o implementador de
IWrapAResource
:Algo como a
using
declaração do C # provavelmente é o mais próximo que você terá da semântica de RAII sem recorrer à referência de recursos de contagem ou forçar a semântica de valor em qualquer lugar, como C ou C ++. Como Java e C # têm compartilhamento implícito de recursos gerenciados por um coletor de lixo, o mínimo que um programador precisa fazer é escolher o escopo ao qual um recurso está vinculado, exatamente o queusing
já faz.fonte
using
declaração.IWrapSomething
descartarT
. Quem criouT
precisa se preocupar com isso, seja usandousing
, sendoIDisposable
ele mesmo ou tendo algum esquema de ciclo de vida de recursos ad-hoc.A razão pela qual o RAII não pode funcionar em uma linguagem como C #, mas funciona em C ++, é porque em C ++ você pode decidir se um objeto é realmente temporário (alocando-o na pilha) ou se é de longa duração (por alocá-lo no heap usando
new
e usando ponteiros).Portanto, em C ++, você pode fazer algo assim:
Em C #, você não pode diferenciar os dois casos, portanto, o compilador não tem idéia se finaliza o objeto ou não.
O que você pode fazer é introduzir algum tipo de variável local especial, que você não pode colocar em campos etc. * e que seria descartado automaticamente quando sair do escopo. Qual é exatamente o que C ++ / CLI faz. Em C ++ / CLI, você escreve um código como este:
Isso é compilado basicamente para o mesmo IL que o seguinte c #:
Para concluir, se eu adivinhasse por que os designers de C # não adicionaram RAII, é porque eles pensaram que não vale a pena ter dois tipos diferentes de variáveis locais, principalmente porque em uma linguagem com GC, a finalização determinística não é útil que frequentemente.
* Não sem o equivalente do
&
operador, que é em C ++ / CLI%
. Embora fazer isso seja "inseguro" no sentido de que, após o término do método, o campo fará referência a um objeto descartado.fonte
struct
tipos como o D.Se o que incomoda você com os
using
blocos é a sua explicitação, talvez possamos dar um pequeno passo em direção a menos explicitação, em vez de alterar as próprias especificações do C #. Considere este código:Veja a
local
palavra - chave que adicionei? Tudo o que faz é adicionar um pouco mais de açúcar sintático, assim comousing
dizer ao compilador para chamarDispose
umfinally
bloco no final do escopo da variável. Isso é tudo. É totalmente equivalente a:mas com um escopo implícito, e não explícito. É mais simples que as outras sugestões, já que não preciso ter a classe definida como vinculada ao escopo. Apenas açúcar sintático mais limpo e implícito.
Pode haver problemas aqui com escopos difíceis de resolver, embora eu não possa vê-lo agora e aprecio qualquer um que possa encontrá-lo.
fonte
using
palavra - chave, podemos manter o comportamento existente e usá-lo também, nos casos em que não precisarmos do escopo específico. Tenha umusing
padrão sem chaves no escopo atual.Para um exemplo de como o RAII funciona em uma linguagem de coleta de lixo, verifique a
with
palavra - chave em Python . Em vez de confiar em objetos destruídos deterministicamente, vamos associar__enter__()
e__exit__()
métodos a um determinado escopo lexical. Um exemplo comum é:Como no estilo RAII do C ++, o arquivo seria fechado ao sair desse bloco, independentemente de ser uma saída 'normal', a
break
, uma imediatareturn
ou uma exceção.Observe que a
open()
chamada é a função usual de abertura de arquivo. Para fazer isso funcionar, o objeto de arquivo retornado inclui dois métodos:Esse é um idioma comum no Python: os objetos associados a um recurso geralmente incluem esses dois métodos.
Observe que o objeto de arquivo ainda pode permanecer alocado após a
__exit__()
chamada, o importante é que ele seja fechado.fonte
with
em Python é quase exatamente comousing
em C # e, como tal, não RAII no que diz respeito a essa questão.defer
no idioma Go).