Question: Pourquoi Java / C # ne peut-il pas implémenter RAII?
Clarification: je sais que le ramasse-miettes n'est pas déterministe. Ainsi, avec les fonctionnalités actuelles du langage, il n'est pas possible d'appeler automatiquement la méthode Dispose () d'un objet à la sortie de la portée. Mais pourrait-on ajouter une telle caractéristique déterministe?
Ma compréhension:
Je pense qu'une implémentation de RAII doit satisfaire deux exigences:
1. La durée de vie d'une ressource doit être liée à une étendue.
2. Implicite. La libération de la ressource doit avoir lieu sans déclaration explicite du programmeur. Analogue à un garbage collector libérant de la mémoire sans déclaration explicite. L '«implicitness» ne doit se produire qu'au point d'utilisation de la classe. Le créateur de la bibliothèque de classes doit bien sûr implémenter explicitement un destructeur ou une méthode Dispose ().
Java / C # satisfait le point 1. En C #, une ressource implémentant IDisposable peut être liée à une étendue "using":
void test()
{
using(Resource r = new Resource())
{
r.foo();
}//resource released on scope exit
}
Cela ne satisfait pas le point 2. Le programmeur doit lier explicitement l'objet à une portée spéciale "using". Les programmeurs peuvent (et oublient) de lier explicitement la ressource à une étendue, créant une fuite.
En fait, les blocs "using" sont convertis en code try-finally-dispose () par le compilateur. Il a la même nature explicite que le modèle try-finally-dispose (). Sans libération implicite, l'hameçon à une portée est le sucre syntaxique.
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!!!
Je pense qu'il vaut la peine de créer une fonctionnalité de langage en Java / C # permettant des objets spéciaux qui sont accrochés à la pile via un pointeur intelligent. La fonctionnalité vous permettrait de marquer une classe comme liée à la portée, afin qu'elle soit toujours créée avec un crochet à la pile. Il pourrait y avoir des options pour différents types de pointeurs intelligents.
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.
Je pense que l'implicitation en vaut la peine. Tout comme l'implication de la collecte des ordures en "vaut la peine". L'utilisation de blocs explicites est rafraîchissante pour les yeux, mais n'offre aucun avantage sémantique par rapport à try-finally-dispose ().
Est-il impossible d'implémenter une telle fonctionnalité dans les langages Java / C #? Pourrait-il être introduit sans casser l'ancien code?
Dispose
s sont jamais exécutées, la façon dont ils sont déclenchés. L'ajout d'une destruction implicite à la fin de la portée n'aidera pas cela.using
l'exécution deDispose
est garanti (enfin, actualiser le processus en train de mourir soudainement sans exception, à ce moment-là, tout nettoyage devient vraisemblablement théorique).struct
), mais ils sont généralement évités , sauf dans des cas très particuliers. Voir aussi .Réponses:
Une telle extension linguistique serait beaucoup plus compliquée et invasive que vous ne le pensez. Vous ne pouvez pas simplement ajouter
à la section pertinente de la spécification de langue et être fait. J'ignorerai le problème des valeurs temporaires (
new Resource().doSomething()
) qui peut être résolu par une formulation légèrement plus générale, ce n'est pas le problème le plus grave. Par exemple, ce code serait cassé (et ce genre de chose devient probablement impossible à faire en général):Maintenant, vous avez besoin de constructeurs de copie définis par l'utilisateur (ou de déplacer des constructeurs) et commencez à les appeler partout. Non seulement cela a des implications en termes de performances, mais cela rend également ces choses des types de valeur efficaces, alors que presque tous les autres objets sont des types de référence. Dans le cas de Java, il s'agit d'une déviation radicale par rapport au fonctionnement des objets. En C # moins (a déjà
struct
s, mais pas de constructeurs de copie définis par l'utilisateur pour eux AFAIK), mais cela rend toujours ces objets RAII plus spéciaux. Alternativement, une version limitée des types linéaires (cf. Rust) peut également résoudre le problème, au prix d'interdire l'alias, y compris le passage de paramètres (sauf si vous voulez introduire encore plus de complexité en adoptant des références empruntées de type Rust et un vérificateur d'emprunt).Cela peut être fait techniquement, mais vous vous retrouvez avec une catégorie de choses très différentes de tout le reste dans la langue. C'est presque toujours une mauvaise idée, avec des conséquences pour les implémenteurs (plus de cas marginaux, plus de temps / coût dans chaque département) et les utilisateurs (plus de concepts à apprendre, plus de possibilité de bugs). Cela ne vaut pas la commodité supplémentaire.
la source
File
cette façon, rien ne change etDispose
n'est jamais appelé. Si vous appelez toujoursDispose
, vous ne pouvez rien faire avec des objets jetables. Ou proposez-vous un plan pour parfois éliminer et parfois non? Si oui, veuillez le décrire en détail et je vous dirai les situations dans lesquelles il échoue.Dispose
si une référence s'échappe? L'analyse d'échappement est un problème ancien et difficile, cela ne fonctionnera pas toujours sans modifications supplémentaires de la langue. Lorsqu'une référence est passée à une autre méthode (virtuelle) (something.EatFile(f);
), doitf.Dispose
être appelée à la fin de la portée? Si oui, vous interrompez les appelants qui stockentf
pour une utilisation ultérieure. Sinon, vous perdez la ressource si l'appelant ne stocke pasf
. Le seul moyen quelque peu simple de supprimer cela est un système de type linéaire qui (comme je l'ai déjà expliqué plus tard dans ma réponse) introduit à la place de nombreuses autres complications.La plus grande difficulté à implémenter quelque chose comme ça pour Java ou C # serait de définir le fonctionnement du transfert de ressources. Vous auriez besoin d'un moyen de prolonger la durée de vie de la ressource au-delà de la portée. Considérer:
Le pire, c'est que cela peut ne pas être évident pour le réalisateur de
IWrapAResource
:Quelque chose comme la
using
déclaration de C # est probablement aussi proche que possible de la sémantique RAII sans recourir à des ressources de comptage de référence ou forcer la sémantique des valeurs partout comme C ou C ++. Étant donné que Java et C # ont un partage implicite des ressources gérées par un garbage collector, le minimum qu'un programmeur devrait être en mesure de faire est de choisir la portée à laquelle une ressource est liée, ce qui est exactement ce qui existeusing
déjà.la source
using
déclaration.IWrapSomething
vous débarrasserT
. Celui qui a crééT
doit s'en préoccuper, qu'il utiliseusing
, qu'il soitIDisposable
lui-même ou qu'il ait un schéma de cycle de vie des ressources ad hoc.La raison pour laquelle RAII ne peut pas fonctionner dans un langage comme C #, mais il fonctionne en C ++, c'est parce qu'en C ++, vous pouvez décider si un objet est vraiment temporaire (en l'allouant sur la pile) ou s'il dure longtemps (en l'allouer sur le tas en utilisant
new
et en utilisant des pointeurs).Donc, en C ++, vous pouvez faire quelque chose comme ceci:
En C #, vous ne pouvez pas différencier les deux cas, donc le compilateur n'aurait aucune idée de finaliser l'objet ou non.
Ce que vous pourriez faire, c'est introduire une sorte de variable locale spéciale, que vous ne pouvez pas mettre dans les champs, etc. * et qui serait automatiquement supprimée lorsqu'elle sortira du champ d'application. C'est exactement ce que fait C ++ / CLI. En C ++ / CLI, vous écrivez du code comme ceci:
Cela compile essentiellement le même IL que le C # suivant:
Pour conclure, si je devais deviner pourquoi les concepteurs de C # n'ont pas ajouté RAII, c'est parce qu'ils pensaient qu'avoir deux types différents de variables locales ne valait pas la peine, principalement parce que dans un langage avec GC, la finalisation déterministe n'est pas utile que souvent.
* Pas sans l'équivalent de l'
&
opérateur, qui est en C ++ / CLI%
. Bien que cela soit «dangereux» dans le sens où une fois la méthode terminée, le champ référencera un objet supprimé.la source
struct
types comme D le fait.Si ce qui vous dérange avec les
using
blocs est leur explicitation, nous pouvons peut-être faire un petit pas vers moins d'explicitness, plutôt que de changer la spécification C # elle-même. Considérez ce code:Voir le
local
mot - clé que j'ai ajouté? Tout ce qu'il fait, c'est ajouter un peu plus de sucre syntaxique, tout commeusing
, en disant au compilateur d'appelerDispose
unfinally
bloc à la fin de la portée de la variable. C'est tout. C'est totalement équivalent à:mais avec une portée implicite, plutôt qu'une explicite. C'est plus simple que les autres suggestions car je n'ai pas besoin de définir la classe comme liée à la portée. Sucre syntaxique juste plus propre et plus implicite.
Il peut y avoir des problèmes ici avec des étendues difficiles à résoudre, bien que je ne puisse pas le voir pour le moment, et j'apprécierais tous ceux qui peuvent le trouver.
la source
using
mot - clé, nous pouvons conserver le comportement existant et l'utiliser également, dans les cas où nous n'avons pas besoin de la portée spécifique. Avoirusing
par défaut sans accolade la portée actuelle.Pour un exemple du fonctionnement de RAII dans un langage récupéré, vérifiez le
with
mot clé en Python . Au lieu de s'appuyer sur des objets détruits de manière déterministe, il vous permet d'associer des méthodes__enter__()
et__exit__()
une portée lexicale donnée. Un exemple courant est:Comme avec le style RAII de C ++, le fichier serait fermé à la sortie de ce bloc, peu importe qu'il s'agisse d'une sortie «normale», a
break
, immédiatereturn
ou exceptionnelle.Notez que l'
open()
appel est la fonction d'ouverture de fichier habituelle. pour que cela fonctionne, l'objet fichier renvoyé comprend deux méthodes:Il s'agit d'un idiome courant en Python: les objets associés à une ressource incluent généralement ces deux méthodes.
Notez que l'objet fichier peut toujours rester alloué après l'
__exit__()
appel, l'important est qu'il soit fermé.la source
with
en Python est presque exactement commeusing
en C #, et en tant que tel pas RAII en ce qui concerne cette question.defer
en langue Go).