Pourquoi Java / C # ne peut-il pas implémenter RAII?


29

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?


3
Ce n'est pas impossible, c'est impossible . La norme C # ne garantit pas Destructeurs / Disposes 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.
Telastyn

20
@Telastyn Huh? Ce que dit la norme C # maintenant n'a aucune pertinence, car nous discutons de la modification de ce document. La seule question est de savoir si cela est pratique à faire, et pour cela le seul élément intéressant au sujet de l’absence actuelle de garantie est les raisons de cette absence de garantie. Notez que pour usingl'exécution de Dispose est garanti (enfin, actualiser le processus en train de mourir soudainement sans exception, à ce moment-là, tout nettoyage devient vraisemblablement théorique).

4
duplicate of Les développeurs de Java ont-ils délibérément abandonné RAII? , bien que la réponse acceptée soit complètement incorrecte. La réponse courte est que Java utilise la sémantique de référence (tas) plutôt que la sémantique de valeur (pile) , donc la finalisation déterministe n'est pas très utile / possible. C # ne jouissent d'une valeur sémantique ( struct), mais ils sont généralement évités , sauf dans des cas très particuliers. Voir aussi .
BlueRaja - Danny Pflughoeft

2
C'est similaire, pas un double exact.
Maniero

3
blogs.msdn.com/b/oldnewthing/archive/2010/08/10/10048150.aspx est une page pertinente pour cette question.
Maniero

Réponses:


17

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 Disposel'objet auquel il 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 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.


Pourquoi avez-vous besoin de copier / déplacer le constructeur? Le fichier conserve un type de référence. Dans cette situation, f qui est un pointeur est copié vers l'appelant et il est responsable de disposer de la ressource (le compilateur mettrait implicitement un modèle try-finally-dispose dans l'appelant)
Maniero

1
@bigown Si vous traitez chaque référence de Filecette façon, rien ne change et Disposen'est jamais appelé. Si vous appelez toujours Dispose, 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.

Je ne vois pas ce que vous avez dit maintenant (je ne dis pas que vous vous trompez). L'objet a une ressource, pas la référence.
Maniero

Ma compréhension, en changeant votre exemple en un simple retour, est que le compilateur insérera un essai juste avant l'acquisition des ressources (ligne 3 dans votre exemple) et le bloc de suppression finale juste avant la fin de la portée (ligne 6). Pas de problème ici, d'accord? Revenons à votre exemple. Le compilateur voit un transfert, il n'a pas pu insérer try-finally ici mais l'appelant recevra un (pointeur vers) un objet File et en supposant que l'appelant ne transfère plus cet objet, le compilateur insérera le modèle try-finally là-bas. En d'autres termes, chaque objet IDisposable non transféré doit appliquer un modèle try-finally.
Maniero

1
@bigown En d'autres termes, n'appelez pas Disposesi 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);), doit f.Disposeêtre appelée à la fin de la portée? Si oui, vous interrompez les appelants qui stockent fpour 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.

26

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:

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, c'est que cela peut ne pas être évident pour le réalisateur 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 la usingdé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 existe usingdéjà.


En supposant que vous n'ayez pas besoin de vous référer à une variable après qu'elle soit hors de portée (et il ne devrait vraiment pas y avoir un tel besoin), je prétends que vous pouvez toujours rendre un objet autonome en écrivant un finaliseur pour cela . Le finaliseur est appelé juste avant que l'objet ne soit récupéré. Voir msdn.microsoft.com/en-us/library/0s71x931.aspx
Robert Harvey

8
@Robert: Un programme correctement écrit ne peut pas supposer que les finaliseurs ont été exécutés. blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Billy ONeal

1
Hm. Eh bien, c'est probablement pourquoi ils ont fait cette usingdéclaration.
Robert Harvey

2
Exactement. C'est une énorme source de bugs pour les débutants en C ++, et serait également en Java / C #. Java / C # n'élimine pas la possibilité de divulguer la référence à une ressource sur le point d'être détruite, mais en la rendant explicite et facultative, ils rappellent au programmeur et lui donnent un choix conscient de ce qu'il doit faire.
Aleksandr Dubinsky

1
@svick Ce n'est pas à vous de IWrapSomethingvous débarrasser T. Celui qui a créé Tdoit s'en préoccuper, qu'il utilise using, qu'il soit IDisposablelui-même ou qu'il ait un schéma de cycle de vie des ressources ad hoc.
Aleksandr Dubinsky

13

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 newet 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 sortira du champ d'application. 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 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é.


1
C # pourrait trivialement faire RAII s'il permettait des destructeurs pour des structtypes comme D le fait.
Jan Hudec

6

Si ce qui vous dérange avec les usingblocs 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 localmot - clé 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 Disposeun finallybloc à 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.


1
@ mike30 mais le déplacer vers la définition de type vous amène exactement aux problèmes énumérés par les autres - que se passe-t-il si vous passez le pointeur à une autre méthode ou le retournez de la fonction? De cette façon, la portée est déclarée dans la portée, pas ailleurs. Un type peut être jetable, mais ce n'est pas à lui d'appeler Dispose.
Avner Shahar-Kashtan du

3
@ mike30: Meh. Cette syntaxe ne fait que supprimer les accolades et, par extension, le contrôle d'étendue qu'elles fournissent.
Robert Harvey

1
@RobertHarvey Exactement. Il sacrifie une certaine flexibilité pour un code plus propre et moins imbriqué. Si nous prenons la suggestion de @ delnan et réutilisons le usingmot - 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. Avoir usingpar défaut sans accolade la portée actuelle.
Avner Shahar-Kashtan

1
Je n'ai aucun problème avec les exercices semi-pratiques de conception de langage.
Avner Shahar-Kashtan

1
@RobertHarvey. Vous semblez avoir un parti pris contre tout ce qui n'est pas actuellement implémenté en C #. Nous n'aurions pas de génériques, linq, using-blocks, types ipmlicit, etc. si nous étions satisfaits de C # 1.0. Cette syntaxe ne résout pas le problème de l'implicite, mais c'est un bon sucre à lier à la portée actuelle.
mike30

1

Pour un exemple du fonctionnement de RAII dans un langage récupéré, vérifiez le withmot 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:

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», a break, immédiate returnou 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:

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' __exit__()appel, l'important est qu'il soit fermé.


7
withen Python est presque exactement comme usingen C #, et en tant que tel pas RAII en ce qui concerne cette question.

1
Le «avec» de Python est une gestion des ressources liée à la portée, mais il manque l'implicite d'un pointeur intelligent. Le fait de déclarer un pointeur comme intelligent pourrait être considéré comme "explicite", mais si le compilateur appliquait l'intelligence comme faisant partie du type d'objets, il pencherait vers "implicite".
mike30

AFAICT, le but du RAII est d'établir une portée stricte des ressources. si vous souhaitez uniquement effectuer la désallocation d'objets, alors non, les langages récupérés ne peuvent pas le faire. si vous êtes intéressé à libérer régulièrement des ressources, alors c'est une façon de le faire (une autre est deferen langue Go).
Javier

1
En fait, je pense qu'il est juste de dire que Java et C # favorisent fortement les constructions explicites. Sinon, pourquoi s'embêter avec toute la cérémonie inhérente à l'utilisation des interfaces et de l'héritage?
Robert Harvey

1
@delnan, Go a des interfaces «implicites».
Javier
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.