10
votes

Raii est-il sûr à utiliser dans C #? Et autres ordures collectant des langues?

Je faisais une classe Raii qui prend dans un système.Windows.Form Control et définit son curseur. Et dans le destructeur, il remet le curseur à ce qu'il était.

Mais est-ce une mauvaise idée? Puis-je compter en toute sécurité que le destructeur sera appelé lorsque des objets de cette classe se déroulent hors de portée?


1 commentaires

FYI, C # n'a pas de destructeurs dans le même sens que C ++. Il a des finaliseurs, mais ils sont appelés au temps de la collecte des ordures, pas lorsque l'objet sort de la portée.


4 Réponses :


21
votes

C'est une idée très très mauvaise.

Les finaliseurs sont pas appelés lorsque les variables sont hors de portée. Ils sont appelés à un moment donné avant que l'objet ne soit recueilli, ce qui pourrait être longtemps plus tard.

à la place, vous souhaitez mettre en œuvre Idisposable , puis les appelants peuvent utiliser: xxx

qui appellera jetera automatiquement automatiquement.

Les derniers sont très rarement nécessaires dans C # - ils ne sont nécessaires que lorsque vous directement possède une ressource non gérée (par exemple, une poignée de Windows). Sinon, vous avez généralement une classe de wrapper géant (par exemple, filestream ) qui aura un finaliseur s'il en a besoin.

Notez que vous n'avez besoin que tout de cela lorsque vous avez des ressources pour être nettoyées - la plupart des classes de .net ne sont pas Implément Idisposable .

EDIT: Juste pour répondre à la Commentaire sur la nidification, je suis d'accord que cela peut être un peu moche, mais vous ne devriez pas avoir besoin de en utilisant les déclarations très souvent dans mon expérience. Vous pouvez également vous nier de manière verticale comme celle-ci, dans le cas où vous avez deux directement adjacents: xxx


8 commentaires

Utilise suffisamment pour garantir que le destructeur sera appelé @jarededpar suggéré ci-dessous? Si cela le garantit, pourquoi devrais-je implémenter Idisose à la place?


par Idispose, je voulais dire Idisposable


@Net citoyen - non, cela n'appellera pas le destructeur / finisseur. Il va simplement appeler Idisposable. Cela n'affecte pas la finalisation (sauf si vous appelez GC.SuppressFinalize à éliminer le cours!)


Merci, je souhaite qu'il y ait une syntaxe plus propre pour "utiliser". Il portait mon code avec une nidification supplémentaire :(


+1 pour le conseil adjacent à l'aide de la déclaration. Ne savait pas à ce sujet.


RE: Le finaliseur: Habituellement, le motif est d'avoir le jargon et le finaliseur faire la même chose, avec le gabarit supprimant le finaliseur. Vous pouvez trouver une implémentation standard de ce modèle à copier dans n'importe quel nombre de places.


Eric's est absolument juste (bien sûr) mais je stresserais que c'est très rare d'exiger votre propre finaliseur de nos jours. Regardez la classe SafeHandle pour contourner la plupart des besoins. Si vous n'avez pas besoin d'un finaliseur et que votre classe est scellée, le motif devient beaucoup plus simple :)


Il est préférable de lire le "R" dans "Raii" comme "responsabilité", alors chaque fois que vous acquérez une responsabilité, vous remédez à un objet qui le gérera. Je penserais que c'est réellement relativement commun.



1
votes

Utilisez le motif iDisposable si vous voulez faire des choses similaires à Raii en C #


0 commentaires

13
votes

Vous savez, beaucoup de gens intelligents disent "Utilisez Idisposable si vous souhaitez implémenter Raii en C #", et je ne l'achète tout simplement pas. Je sais que je suis dans la minorité ici, mais quand je vois "en utilisant (bla) {foo (bla);}" Je pense automatiquement "Blah contient une ressource non gérée qui doit être nettoyée agressivement dès que FOO ait fini ( ou lancers) afin que quelqu'un d'autre puisse utiliser cette ressource ». Je ne pense pas que "bla ne contient rien d'intéressant, mais il y a des sémantiquement une mutation importante qui doit se produire, et nous allons représenter cette opération sémantiquement importante par le personnage"} "- une mutation comme Une certaine pile doit être apparue ou qu'un drapeau doit être réinitialisé ou autre chose.

Je dis que si vous avez une opération sémantiquement importante qui doit être effectuée lorsque quelque chose complète, nous avons un mot pour cela et le mot est "enfin". Si l'opération est importante , elle doit alors être représentée comme une déclaration que vous pouvez voir ici et mettre un point d'arrêt sur, et non un effet secondaire invisible d'une attelle à droite.

Alors pensons à votre opération particulière. Vous voulez représenter: xxx

ce code est parfaitement clair . Tous les effets secondaires sont là, visibles au programmeur de maintenance qui souhaite comprendre ce que votre code fait.

Vous avez maintenant un point de décision. Et si Blah jette? Si bla jette alors quelque chose d'inattendu est arrivé . Vous n'avez maintenant aucune idée de quel état "forme" est dans; Cela aurait pu être du code dans "formulaire" qui a jeté. Cela aurait pu être à mi-chemin de la mutation et est maintenant dans un état complètement fou.

donné cette situation, que voulez-vous faire?

1) PUNT Le problème de quelqu'un d'autre . Ne fais rien. J'espère que quelqu'un d'autre dans la pile d'appels sait comment faire face à cette situation. Raison que la forme est déjà dans un mauvais état; Le fait que son curseur ne soit pas au bon endroit est le moindre de ses inquiétudes. Ne moquez pas quelque chose qui est déjà si fragile, il a été signalé une exception une fois.

2) Réinitialisez le curseur dans un blocage enfin, puis signalez le problème à quelqu'un d'autre. Espérons - sans preuve que votre espoir sera réalisé - que la réinitialisation du curseur sur une forme que vous connaissez est dans un état fragile ne résoudra pas une exception. Parce que ce qui se passe dans ce cas? L'exception originale, qui, peut-être que quelqu'un savait comment gérer, est perdu. Vous avez des informations détruites sur la cause initiale du problème, des informations qui auraient pu être utiles. Et vous l'avez remplacé avec d'autres informations sur une défaillance du curseur qui est utilisée pour personne.

3) écrire une logique complexe qui gère les problèmes de (2) - attraper l'exception, puis essayez de réinitialiser la position du curseur dans un bloc de capture try-track distinct qui supprime la nouvelle exception et re -La l'exception originale. Cela peut être difficile à avoir raison, mais vous pouvez le faire.

Franchement, ma conviction est que la bonne réponse est presque toujours (1). Si quelque chose d'horrible s'est trompé, vous ne pouvez pas raisonner en toute sécurité quelles mutations supplémentaires à l'état fragile sont légales. Si vous ne savez pas comment gérer l'exception, abandonnez-vous.

(2) est la solution que Raii avec un bloc utilisant un bloc vous donne. Encore une fois, la raison pour laquelle nous avons l'utilisation des blocs en premier lieu est de nettoyer agressivement des ressources vitales lorsqu'elles ne peuvent plus être utilisées. Si une exception se produit ou non, ces ressources doivent être nettoyées rapidement , c'est pourquoi "utiliser" des blocs ont "enfin" sémantique. Mais la sémantique "enfin" ne convient pas nécessairement aux opérations qui ne sont pas des opérations de nettoyage des ressources; Comme nous l'avons vu, le programme-Sémantics-Laden bloque finalement implicitement l'hypothèse qu'il est toujours en sécurité de faire le nettoyage; Mais le fait que nous soyons dans une situation de traitement des exceptions indique qu'il pourrait ne pas être sûr.

(3) ressemble à beaucoup de travail.

Donc en bref, je dis cesse d'essayer d'être si intelligent. Vous voulez muter le curseur, faire du travail et skuter le curseur. Donc, écrivez trois lignes de code qui font cela, fait. Aucun pantalon de fantaisie Raii n'est nécessaire; Il ajoute simplement une indirection inutile, cela rend plus difficile la lecture du programme et le rend potentiellement plus fragile dans des situations exceptionnelles, pas moins fragiles.


13 commentaires

enfin est encombré. Ce que les gens veulent, c'est une manière concise d'exprimer Raii. La conception de c # /. Net n'a tout simplement pas été facturée dans l'énorme utilité de Raii, essayant plutôt de fournir des constructions spéciales pour des cas spéciaux ( verrouillage , en utilisant ). Pour cette raison, les gens essaient de ne pas dépendre de Raii dans .NET, mais parfois c'est la manière la plus naturelle d'exprimer une opération jumelée.


Mais Raii est fréquemment faux quand il est utilisé pour gérer des opérations sémantiques plutôt que pour le nettoyage des ressources. L'hypothèse que la mutation sémantique dans le destructeur est toujours la bonne chose à faire, même dans une situation exceptionnelle n'est pas justifiée; Dans une situation exceptionnelle Si vous ne savez pas quelle bonne chose à faire est, n'essayez pas . Vous allez juste le faire pire.


Je suis d'accord avec @konrad. En outre, je trouve enfin assez non naturel. Si vous avez une déclaration de retour à l'intérieur de votre try, vous quittez votre fonction, la plupart des gens s'attendent à ce que le code revienne tout de suite. Mais cela saura à la place à la fin de la fin, puis de revenir. De même si vous regardez enfin un blocage, vous vous attendez à ce que la ligne suivante s'exécute à l'extérieur de l'enfin, mais dans le scénario décrit ci-dessus, cela peut parfois le retourner aussi. C'est comme goto mais avec du code caché.


Et bien sûr, nous avons ce problème avec "LOCK", comme j'ai écrit dans le passé. "LOCK" libère automatiquement la serrure à une exception. Vous négociez une élimination des blocages pour permettre l'accès à un état incohérent non déverrouillé! L'exception qui a provoqué le déverrouillage n'a été lancée, car tout est génial dans l'objet que vous muniez sous la serrure, croyez-moi. Mais apparemment, la plupart des gens préfèrent avoir des programmes fous d'écrasement logiquement incohérents imprévisibles que des programmes qui impasse lorsqu'une mutation sous une serrure échoue de manière catastrophique.


@Konrad - Vérifiez mon message. Le type cursorapplier est long, mais est donc de type RIAA. L'utilisation de l'utilisation d'autre part est très propre plus propre que RIAA.


@ERIC: Vous dites que Raii est faux pour tout, mais un nettoyage de ressources (avec une définition très étroite de «ressource») parce que vous adhérez à l'étiquette de «Raii» trop littéralement. Raii est une horrible abusive et fournit simplement une manière merveilleusement fiable et terrible d'opérations d'exécution dans un ordre spécifique. Vous êtes bien sûr juste dans la mesure où en utilisant et iDisposable a reçu un objectif spécifique dans .NET, mais je pense que cette contrainte restrictive est totalement inutile.


"Donc, écrivez trois lignes de code qui font cela, fait. Aucun pantalon de fantaisie est nécessaire;" .... Mais avec Raii, vous garantissez que vous ne pouvez pas oublier d'écrire ce code de nettoyage. Et il a moins de duplication de code. Si vous voulez faire un nettoyage supplémentaire également, vous pouvez l'ajouter à la fois et que cela affecte partout à l'aide de votre ressource Raii. Vous n'avez pas besoin de modifier 600 endroits dans le code comme votre façon de suggérer


L'une des objectifs du RAII est de fournir un moyen automatique de changer d'état à des conditions d'achèvement ou d'exception. L'allocation des ressources est, en réalité un état ainsi que d'autres types d'états (état du curseur, dans l'exemple). En n'utilisant pas Raii, vous forcez la méthode pour gérer l'état par lui-même, même si vous avez peut-être créé une bonne façon de gérer l'état automatiquement.


L'allocation des ressources n'est pas un état de sémantique du programme; le fait qu'une ressource doit être traitée dans les meilleurs délais est une considération exogène; La plupart d'entre nous n'écrivent pas de programmes où c'est la seule chose que le système d'exploitation traite. La répartition des ressources est juste la politesse; Dire au système d'exploitation "J'ai fini, quelqu'un d'autre peut l'utiliser maintenant". Si vous oubliez de le faire en temps voulu, le pire qui se produit est que quelqu'un d'autre ne peut pas faire son travail. Raii pour Sémantican Les mutations nécessaires pour maintenir Les invariants de programme internes sont complètement différents.


Je ne suis pas d'accord. Lorsque vous allouez une ressource, vous modifiez l'état du programme. Vous avez entré un "État alloué de ressources". Cependant, même si nous acceptons votre position, ce n'est qu'un argument sémantique. Donc, si je crée un nouveau modèle appelé SCII (changement d'état est l'initialisation) qui change d'état sur l'initialisation et la nettoie sur la destruction, elle est identique à Raii dans tout sauf.


Bien sûr, vous modifiez l'état du programme; Mais ce changement d'état fait partie du mécanisme du programme, qui ne font pas partie de la sémantique . Le problème avec les langues non gc'd est qu'ils vous font mixer le code qui maintient les mécanismes satisfaits du code qui met en œuvre la logique commerciale du programme; Il viole la séparation des préoccupations. Le GC résout ce problème en cachant le mécanisme et en vous permettant de vous concentrer sur l'écriture de votre sémantique.


Le problème avec l'état-changement Raii est qu'il fait le contraire: il faut une préoccupation sémantique et la masque dans les mécanismes d'allocation de ressources / de translocation, exactement où il ne devrait pas être. Ces mécanismes ont un but: de gérer en douceur la gestion des ressources afin que vous puissiez continuer à trouver la logique commerciale de votre programme. Utilisez-les à des fins prévues; Ne mélangez pas la logique du mécanisme avec la logique commerciale et ne cachez pas la logique commerciale dans les mécanismes d'allocation.


Il est préférable de penser à "R" dans "Raii" comme "responsabilité". Donc, chaque fois qu'une responsabilité est acquise, telle que la responsabilité de réinitialiser l'état du curseur, vous remettez cette responsabilité à un objet qui le gérera correctement. Je ne vois pas d'argument convaincant ici que c'est une bonne idée de faire la distinction que vous faites entre la logique du mécanisme et la logique commerciale. Définir et utiliser un objet pour gérer quelque chose comme la réinitialisation de l'état du curseur est beaucoup plus clair et plus maintenu qu'un tas de blocs de copie enfin.



6
votes

edit: clairement Eric et moi sommes à un désaccord sur cette utilisation. : o

Vous pouvez utiliser quelque chose comme ceci: xxx


1 commentaires

Des commentaires sur la réponse d'Eric: "L'utilisation de l'utilisation de l'autre main est considérablement plus propre que RIAA" - je suis en désaccord. Alors que en utilisant est certainement plus explicite , RAII est un idiome aussi établi en C ++ ou (dire) VB6 que son utilisation est devenue une attente au lieu d'une action cachée inattendue. À condition que le destructeur de l'objet ne viole pas le principe du moindre surprise en faisant quelque chose de totalement inapproprié, sans papiers et imprévisibles, son utilisation est tout aussi claire et propre que en utilisant et moins verbose (car Chaque bloc est un implicite en utilisant sur ses variables de pile locales)