En tant que programmeur C # de longue date, je suis récemment venu en savoir plus sur les avantages de l’ initialisation d’acquisition de ressources (RAII). En particulier, j'ai découvert que l'idiome C #:
using (var dbConn = new DbConnection(connStr)) {
// do stuff with dbConn
}
a l'équivalent C ++:
{
DbConnection dbConn(connStr);
// do stuff with dbConn
}
ce qui signifie que se rappeler d’inclure l’utilisation de ressources comme DbConnection
dans un using
bloc n’est pas nécessaire en C ++! Cela semble être un avantage majeur du C ++. Ceci est encore plus convaincant quand on considère une classe qui a un membre d'instance de type DbConnection
, par exemple
class Foo {
DbConnection dbConn;
// ...
}
En C #, il faudrait que Foo implémente en IDisposable
tant que tel:
class Foo : IDisposable {
DbConnection dbConn;
public void Dispose()
{
dbConn.Dispose();
}
}
Et ce qui est pire, chaque utilisateur de Foo
devrait avoir à se rappeler de mettre Foo
dans un using
bloc, comme:
using (var foo = new Foo()) {
// do stuff with "foo"
}
Maintenant, en regardant C # et ses racines Java, je me demande ... les développeurs de Java ont-ils pleinement compris ce qu’ils abandonnaient lorsqu’ils ont abandonné la pile au profit du tas, abandonnant ainsi RAII?
(De même, Stroustrup a-t-il pleinement compris la signification de RAII?)
la source
Réponses:
Je suis à peu près sûr que Gosling n’a pas compris l’importance de RAII au moment où il a conçu Java. Dans ses entretiens, il a souvent évoqué les raisons pour lesquelles on omettait les génériques et la surcharge d'opérateurs, sans jamais mentionner les destructeurs déterministes et la RAII.
Assez drôle, même Stroustrup n'était pas conscient de l'importance des destructeurs déterministes au moment où il les a conçus. Je ne trouve pas la citation, mais si vous y tenez vraiment, vous pouvez la trouver parmi ses interviews ici: http://www.stroustrup.com/interviews.html
la source
manual
gestion de la mémoire des pointeurs intelligents C ++ . Ils ressemblent davantage à un éboueur déterministe à grain fin contrôlable. S'ils sont utilisés correctement, les pointeurs intelligents sont les genoux des abeilles.Oui, les concepteurs de C # (et, j'en suis sûr, de Java) se sont spécifiquement prononcés contre la finalisation déterministe. J'ai interrogé Anders Hejlsberg à ce sujet plusieurs fois vers 1999-2002.
Premièrement, l'idée d'une sémantique différente pour un objet, selon qu'il repose ou non sur un tas, va certainement à l'encontre de l'objectif de conception unificateur des deux langages, qui était de soulager les programmeurs de tels problèmes.
Deuxièmement, même si vous reconnaissez qu'il y a des avantages, la comptabilité comporte des complexités et des inefficacités importantes en matière de mise en œuvre. Vous ne pouvez pas vraiment mettre des objets de type pile sur la pile dans un langage géré. Il vous reste à dire «sémantique semblable à une pile» et à vous engager dans un travail important (les types de valeur sont déjà assez difficiles, pensez à un objet qui est une instance d'une classe complexe, avec des références entrant et sortant dans la mémoire gérée).
Pour cette raison, vous ne voulez pas de finalisation déterministe sur chaque objet d'un système de programmation où "(presque) tout est un objet". Donc , vous ne devez introduire une sorte de syntaxe contrôlée programmeur pour séparer un objet normalement suivi d'un qui a la finalisation déterministe.
En C #, vous avez le
using
mot - clé, qui est entré assez tard dans la conception de ce qui est devenu C # 1.0. ToutIDisposable
cela est plutôt misérable, et on se demande s'il serait plus élégant deusing
travailler avec la syntaxe de destructeur C ++~
marquant les classes auxquelles leIDisposable
motif de plaque de chaudière pourrait être automatiquement appliqué?la source
~
syntaxe comme sucre syntaxique pourIDisposable.Dispose()
~
IDisposable.Dispose()
N'oubliez pas que Java a été développé en 1991-1995, alors que C ++ était un langage très différent. Les exceptions (qui rendaient RAII nécessaire ) et les modèles (facilitant la mise en œuvre de pointeurs intelligents) constituaient des fonctionnalités "nouvelles". La plupart des programmeurs C ++ étaient issus du C et étaient habitués à la gestion manuelle de la mémoire.
Je doute donc que les développeurs de Java aient délibérément décidé d'abandonner RAII. Toutefois, Java a pris la décision délibérée de préférer la sémantique de référence à la sémantique de valeur. La destruction déterministe est difficile à implémenter dans un langage de sémantique de référence.
Alors, pourquoi utiliser la sémantique de référence au lieu de la sémantique de valeur?
Parce que cela rend la langue beaucoup plus simple.
Foo
etFoo*
ou entrefoo.bar
etfoo->bar
.clone()
. De nombreux objets n'ont simplement pas besoin d'être copiés. Par exemple, les immuables ne le sont pas.)private
constructeurs de copie etoperator=
de rendre une classe non copiable. Si vous ne voulez pas que les objets d'une classe soient copiés, vous n'écrivez pas une fonction pour la copier.swap
fonctions. (Sauf si vous écrivez une routine de tri.)Le principal inconvénient de la sémantique de référence est que, lorsque chaque objet a potentiellement plusieurs références, il devient difficile de savoir quand le supprimer. Vous devez très bien avoir une gestion automatique de la mémoire.
Java a choisi d'utiliser un ramasse-miettes non déterministe.
GC ne peut-il pas être déterministe?
Oui il peut. Par exemple, l'implémentation C de Python utilise le comptage de références. Et plus tard, nous avons ajouté le suivi GC pour traiter les déchets cycliques où refcount échoue.
Mais recomptage est horriblement inefficace. Beaucoup de cycles du processeur ont été consacrés à la mise à jour des comptes. Pire encore, dans un environnement multithread (comme celui pour lequel Java a été conçu) où ces mises à jour doivent être synchronisées. Il est préférable d’utiliser le collecteur de déchets null jusqu'à ce que vous deviez passer à un autre.
On pourrait dire que Java a choisi d’optimiser le cas commun (mémoire) aux dépens de ressources non fongibles comme des fichiers et des sockets. Aujourd'hui, à la lumière de l'adoption de RAII en C ++, cela peut sembler être un mauvais choix. Mais rappelez-vous qu'une grande partie du public cible de Java était les programmeurs C (ou "C avec classes") habitués à fermer ces choses explicitement.
Mais qu'en est-il des "objets de pile" C ++ / CLI?
Ils ne sont que du sucre syntaxique pour
Dispose
( lien d'origine ), un peu comme le C #using
. Cependant, cela ne résout pas le problème général de la destruction déterministe, car vous pouvez créer un fichier anonymegcnew FileStream("filename.ext")
et que C ++ / CLI ne le supprime pas automatiquement.la source
using
déclaration traite bien de nombreux problèmes liés au nettoyage, mais il en reste beaucoup. Je suggérerais que la bonne approche pour un langage et un cadre serait de faire une distinction déclarative entre les emplacements de stockage qui "possèdent" une référence etIDisposable
ceux qui ne le sont pas; écraser ou abandonner un emplacement de stockage qui possède un référencéIDisposable
devrait éliminer la cible en l'absence de directive contraire.new Date(oldDate.getTime())
.Java7 a introduit quelque chose de similaire au C #
using
: La déclaration try-with-resourcesJe suppose donc qu’ils n’ont pas consciemment choisi de ne pas mettre en œuvre la norme RAII ou qu’ils ont changé d’avis en attendant.
la source
java.lang.AutoCloseable
. Probablement pas un gros problème, mais je n'aime pas la sensation de contrainte. Peut-être ai-je un autre objet qui devrait être supprimé automatiquement, mais il est très sémantique de le faire mettre en œuvreAutoCloseable
...using
n’est pas la même chose que RAII - dans un cas, l’appelant s’inquiète de la possibilité de disposer des ressources, dans l’autre cas, l’appelant s’en occupe.using
/ try-with-resources n'étant pas identique à RAII.using
et il est loin d'être RAII.Java n'a intentionnellement pas d'objets basés sur des piles (objets de valeur). Celles-ci sont nécessaires pour que l'objet soit automatiquement détruit à la fin de la méthode.
À cause de cela et du fait que Java est une ordure ménagée, la finalisation déterministe est plus ou moins impossible (ex. Et si mon objet "local" était référencé ailleurs? Alors quand la méthode se termine, nous ne voulons pas qu'elle soit détruite ) .
Cependant, cela convient à la plupart d'entre nous, car la finalisation déterministe n'est presque jamais nécessaire , sauf en cas d'interaction avec des ressources natives (C ++)!
Pourquoi Java n'a-t-il pas d'objets en pile?
(Autres que les primitives ..)
Parce que les objets basés sur des piles ont une sémantique différente de celle des références basées sur des tas. Imaginez le code suivant en C ++; Qu'est ce que ça fait?
myObject
est un objet local basé sur une pile, le constructeur de copie est appelé (si le résultat est assigné à quelque chose).myObject
est un objet local basé sur une pile et que nous renvoyons une référence, le résultat est indéfini.myObject
est un membre / objet global, le constructeur de copie est appelé (si le résultat est assigné à quelque chose).myObject
est un membre / objet global et que nous renvoyons une référence, la référence est renvoyée.myObject
est un pointeur sur un objet local basé sur une pile, le résultat est indéfini.myObject
est un pointeur sur un membre / objet global, ce pointeur est renvoyé.myObject
est un pointeur sur un objet basé sur un tas, ce pointeur est renvoyé.Maintenant, que fait le même code en Java?
myObject
est renvoyée. Peu importe que la variable soit locale, membre ou globale; et il n'y a pas d'objet basé sur la pile ni de cas de pointeur à craindre.Ce qui précède montre pourquoi les objets basés sur des piles sont une cause très fréquente d’erreurs de programmation en C ++. À cause de cela, les concepteurs de Java les ont sortis; et sans eux, il est inutile d'utiliser RAII en Java.
la source
Votre description des trous de
using
est incomplète. Considérez le problème suivant:À mon avis, ne pas avoir à la fois RAII et GC était une mauvaise idée. Quand il s'agit de fermer des fichiers en Java, c'est
malloc()
etfree()
là-bas.la source
using
clause est un grand pas en avant pour C # par rapport à Java. Cela permet une destruction déterministe et donc une gestion correcte des ressources (ce n’est pas aussi bon que RAII, il faut bien le garder à l’esprit, mais c’est une bonne idée).free()
lefinally
.IEnumerable
n'a pas hérité deIDisposable
, et il y avait un tas d'itérateurs spéciaux qui ne pourraient jamais être implémentés.Je suis assez vieux J'y suis allé et l'ai vu et me suis cogné la tête à plusieurs reprises.
J'étais à une conférence à Hursley Park où les garçons d'IBM nous disaient à quel point ce nouveau langage Java était merveilleux. Seule une personne a demandé ... pourquoi n'y a-t-il pas de destructeur pour ces objets? Il ne voulait pas dire ce que nous savons en tant que destructeur en C ++, mais il n'y avait pas non plus de finaliseur (ou il avait des finaliseurs mais ils ne fonctionnaient pas fondamentalement). Cela fait longtemps, et nous avons décidé que Java était un langage de jouet à ce moment-là.
maintenant, ils ont ajouté les finaliseurs aux spécifications de langage et Java a été adopté.
Bien sûr, plus tard, on a dit à tout le monde de ne pas mettre de finaliseurs sur leurs objets car cela ralentissait énormément le GC. (car il fallait non seulement verrouiller le tas, mais aussi déplacer les objets à finaliser dans une zone temporaire, car ces méthodes ne pouvaient pas être appelées car le GC avait suspendu l'exécution de l'application. Au lieu de cela, elles seraient appelées immédiatement avant la prochaine Cycle GC) (et pire encore, le finaliseur n’était jamais appelé du tout lorsque l’application était en train de s’arrêter. Imaginez que votre fichier ne soit pas fermé, jamais)
Ensuite, nous avons eu C #, et je me souviens du forum de discussion sur MSDN où on nous a dit à quel point ce nouveau langage C # était merveilleux. Quelqu'un a demandé pourquoi il n'y avait pas de finalisation déterministe et les gars de la SP nous ont dit que nous n'avions pas besoin de telles choses, puis nous ont dit que nous devions changer notre façon de concevoir les applications, puis nous ont dit à quel point le GC était incroyable et toutes nos anciennes applications. ordures et jamais travaillé à cause de toutes les références circulaires. Ensuite, ils ont cédé à la pression et nous ont dit qu'ils avaient ajouté ce modèle IDispose à la spécification que nous pouvions utiliser. Je pensais que c'était à peu près le retour à la gestion de mémoire manuelle pour nous dans les applications C # à ce stade.
Bien sûr, les gars de la SP ont découvert plus tard que tout ce qu'ils nous avaient dit était… eh bien, ils ont fait qu'Idispose était un peu plus qu'une simple interface standard, et ils ont ensuite ajouté la déclaration using. W00t! Ils ont compris que la finalisation déterministe était quelque chose qui manquait dans la langue après tout. Bien sûr, vous devez toujours vous rappeler de le mettre partout, donc c'est toujours un peu manuel, mais c'est mieux.
Alors, pourquoi l'ont-ils fait alors qu'ils auraient pu dès le départ placer une sémantique de style utilisateur dans chaque bloc de portée? Probablement l'efficacité, mais j'aime penser qu'ils ne se sont pas rendus compte. Tout comme finalement, ils se sont rendu compte que vous aviez encore besoin de pointeurs intelligents dans .NET (google SafeHandle), ils pensaient que le GC résoudrait réellement tous les problèmes. Ils ont oublié qu'un objet est plus qu'une simple mémoire et que le CPG est principalement conçu pour gérer la gestion de la mémoire. ils ont été pris au dépourvu par l'idée que le GC s'en chargerait et ont oublié que vous y mettiez d'autres éléments, un objet n'est pas simplement une goutte de mémoire qui n'a pas d'importance si vous ne le supprimez pas pendant un certain temps.
Mais je pense aussi que l’absence de méthode de finalisation dans le langage Java d’origine avait quelque chose de plus important: les objets que vous avez créés étaient tous liés à la mémoire, et si vous vouliez supprimer autre chose (comme un descripteur de base de données, un socket ou autre). ) alors vous deviez le faire manuellement .
Rappelez-vous que Java a été conçu pour les environnements embarqués où les utilisateurs étaient habitués à écrire du code C avec beaucoup d'allocations manuelles. Par conséquent, le fait de ne pas avoir la gratuité automatique n'était pas un problème - ils ne l'avaient jamais fait auparavant, alors pourquoi en auriez-vous besoin en Java? Le problème n'avait rien à voir avec les threads, ou pile / tas, il était probablement juste là pour faciliter l'allocation de mémoire (et donc le dé-allouer). En tout, l'instruction try / finally est probablement un meilleur endroit pour gérer les ressources non-mémoire.
Donc, IMHO, la façon dont .NET a simplement copié le plus gros défaut de Java est sa plus grande faiblesse. .NET aurait dû être un meilleur C ++, pas un meilleur Java.
la source
Dispose
tous les champs marqués d'uneusing
directive, et spécifier si elleIDisposable.Dispose
doit automatiquement l'appeler; (3) une directive similaire àusing
, mais qui n'appelleraitDispose
qu'en cas d'exception; (4) une variation deIDisposable
ce qui prendrait unException
paramètre, et ...using
cas échéant; le paramètre seraitnull
si leusing
bloc quittait normalement, ou indiquerait quelle exception était en attente s'il sortait via une exception. Si de telles choses existaient, il serait beaucoup plus facile de gérer efficacement les ressources et d’éviter les fuites.Bruce Eckel, auteur de "Thinking in Java" et "Thinking in C ++" et membre du comité de normalisation C ++, est d'avis que, dans de nombreux domaines (pas seulement le RAII), Gosling et l'équipe de Java n'ont pas fait leur possible. devoirs.
la source
La meilleure raison est beaucoup plus simple que la plupart des réponses ici.
Vous ne pouvez pas transmettre des objets alloués à d'autres threads.
Arrêtez-vous et réfléchissez-y. Continuez à penser .... Maintenant, le C ++ n'avait pas de fil lorsque tout le monde était si enthousiaste dans RAII. Même Erlang (des tas distincts par thread) est gênant lorsque vous passez trop d'objets. C ++ n'a qu'un modèle de mémoire en C ++ 2011; maintenant vous pouvez presque raisonner sur la concurrence en C ++ sans avoir à vous référer à la "documentation" de votre compilateur.
Java a été conçu dès le premier jour (ou presque) pour plusieurs threads.
J'ai toujours mon ancien exemplaire du "Langage de programmation C ++" où Stroustrup m'assure que je n'aurai pas besoin de threads.
La deuxième raison douloureuse est d'éviter de trancher.
la source
En C ++, vous utilisez des fonctionnalités de langage de bas niveau plus générales (les destructeurs appelés automatiquement sur des objets basés sur des piles) pour implémenter une version de niveau supérieur (RAII). Cette approche est quelque chose que les gens de C # / Java ne semblent pas être trop friands de. Ils préfèrent concevoir des outils spécifiques de haut niveau pour des besoins spécifiques et les fournir aux programmeurs prêts à l'emploi, intégrés au langage. Le problème avec de tels outils spécifiques est qu’ils sont souvent impossibles à personnaliser (c’est en partie ce qui les rend si faciles à apprendre). Lors de la construction à partir de blocs plus petits, une meilleure solution peut apparaître avec le temps, alors que si vous ne disposez que de constructions de haut niveau et intégrées, cela est moins probable.
Donc oui, je pense (je n'étais pas vraiment là ...) que c'était une décision consciente, dans le but de rendre les langues plus faciles à comprendre, mais à mon avis, c'était une mauvaise décision. Encore une fois, je préfère généralement le C ++ qui donne aux programmeurs une chance de rouler leur propre philosophie, donc je suis un peu partial.
la source
Vous avez déjà appelé l'équivalent approximatif de ceci en C # avec la
Dispose
méthode. Java a égalementfinalize
. NOTE: Je réalise que la finalisation de Java est non déterministe et différente de celleDispose
que je viens de signaler. Elles ont toutes deux une méthode de nettoyage des ressources parallèlement au GC.Si quelque chose C ++ devient plus pénible cependant, car un objet doit être détruit physiquement. Dans les langages de niveau supérieur tels que C # et Java, nous dépendons d'un ramasse-miettes pour le nettoyer lorsqu'il ne fait plus référence à celui-ci. Rien ne garantit que l'objet DBConnection en C ++ ne comporte pas de références ni de pointeurs non autorisés.
Oui, le code C ++ peut être plus intuitif à lire, mais peut être un cauchemar pour déboguer, car les limites et les limitations mises en place par des langages tels que Java excluent certains des bogues les plus difficiles et les plus pénibles, tout en protégeant les autres développeurs contre les erreurs courantes des recrues.
Peut-être s'agit-il de préférences, comme la puissance, le contrôle et la pureté bas niveau du C ++, alors que d’autres, comme moi, préfèrent un langage beaucoup plus explicite.
la source