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 de langage actuelles, 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 répondre à 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 au 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?
Une telle extension linguistique serait beaucoup plus compliquée et invasive que vous ne le pensez. Vous ne pouvez pas simplement ajouter
si la durée de vie d'une variable d'un type lié à la pile se termine, appelez
Dispose
sur l'objet auquel elle se réfère
à 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):
File openSavegame(string id) {
string path = ... id ...;
File f = new File(path);
// do something, perhaps logging
return f;
} // f goes out of scope, caller receives a closed file
Maintenant, vous avez besoin de constructeurs de copie définis par l'utilisateur (ou de déplacer des constructeurs) et commencez à les invoquer 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 qui sont 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 Edge, 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 plus grande difficulté à implémenter quelque chose comme ça pour Java ou C # serait de définir comment fonctionne le transfert de ressources. Vous auriez besoin d'un moyen pour prolonger la durée de vie de la ressource au-delà de la portée. Considérez:
class IWrapAResource
{
private readonly Resource resource;
public IWrapAResource()
{
// Where Resource is scope bound
Resource builder = new Resource(args, args, args);
this.resource = builder;
} // Uh oh, resource is destroyed
} // Crap, there's no scope for IWrapAResource we can bind to!
Le pire est que cela peut ne pas être évident pour l'implémenteur de IWrapAResource
:
class IWrapSomething<T>
{
private readonly T resource; // What happens if T is Resource?
public IWrapSomething(T input)
{
this.resource = input;
}
}
Quelque chose comme l'instruction using
de C # est probablement aussi proche que vous allez arriver à avoir la sémantique RAII sans recourir à des ressources de comptage de référence ou forcer la sémantique de valeur partout comme C ou C++. Parce que Java et C # ont un partage implicite des ressources gérées par un garbage collector, le minimum qu'un programmeur devrait être capable de faire est de choisir l'étendue à laquelle une ressource est liée, ce qui est exactement ce que using
le fait déjà.
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:
void f()
{
Foo f1;
Foo* f2 = new Foo();
Foo::someStaticField = f2;
// f1 is destroyed here, the object pointed to by f2 isn't
}
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 deviendrait hors de portée. C'est exactement ce que fait C++/CLI. En C++/CLI, vous écrivez du code comme ceci:
void f()
{
Foo f1;
Foo^ f2 = gcnew Foo();
Foo::someStaticField = f2;
// f1 is disposed here, the object pointed to by f2 isn't
}
Cela compile essentiellement le même IL que le C # suivant:
void f()
{
using (Foo f1 = new Foo())
{
Foo f2 = new Foo();
Foo.someStaticField = f2;
}
// f1 is disposed here, the object pointed to by f2 isn't
}
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 du &
opérateur, qui en C++/CLI est %
. Bien que cela soit "dangereux" dans le sens où une fois la méthode terminée, le champ référencera un objet supprimé.
Si ce qui vous dérange avec les blocs using
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:
public void ReadFile ()
{
string filename = "myFile.dat";
local Stream file = File.Open(filename);
file.Read(blah blah blah);
}
Voir le mot clé local
que j'ai ajouté? Tout ce qu'il fait, c'est ajouter un peu plus de sucre syntaxique, tout comme using
, en disant au compilateur d'appeler Dispose
dans un bloc finally
à la fin de la portée de la variable. C'est tout. C'est totalement équivalent à:
public void ReadFile ()
{
string filename = "myFile.dat";
using (Stream file = File.Open(filename))
{
file.Read(blah blah blah);
}
}
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.
Pour un exemple du fonctionnement de RAII dans un langage récupéré, consultez le mot clé with
en Python . Au lieu de s'appuyer sur des objets détruits de façon déterministe, il vous permet d'associer les méthodes __enter__()
et __exit__()
à une portée lexicale donnée. Un exemple courant est:
with open('output.txt', 'w') as f:
f.write('Hi there!')
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", d'un break
, d'un return
immédiat ou d'une exception.
Notez que l'appel open()
est la fonction d'ouverture de fichier habituelle. pour que cela fonctionne, l'objet fichier renvoyé comprend deux méthodes:
def __enter__(self):
return self
def __exit__(self):
self.close()
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'appel __exit__()
, l'important est qu'il soit fermé.