
Multithreading
Parlons maintenant de la glace mince. Dans les sections précédentes sur IDisposable, nous avons abordé un concept très important qui sous-tend non seulement les principes de conception des types jetables, mais tout type en général. Il s'agit du concept d'intégrité de l'objet. Cela signifie qu'à un moment donné, un objet est dans un état strictement déterminé et que toute action avec cet objet transforme son état en l'une des options prédéterminées lors de la conception d'un type de cet objet. En d'autres termes, aucune action avec l'objet ne doit le transformer en un état indéfini. Cela entraîne un problème avec les types conçus dans les exemples ci-dessus. Ils ne sont pas thread-safe. Il est possible que les méthodes publiques de ces types soient appelées lorsque la destruction d'un objet est en cours. Résolvons ce problème et décidons si nous devons le résoudre.
Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.
Aussi, si vous voulez nous remercier, la meilleure façon de le faire est de nous donner une étoile sur github ou sur fork repository
github / sidristij / dotnetbook .
public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; object _disposingSync = new object(); public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Seek(int position) { lock(_disposingSync) { CheckDisposed(); // Seek API call } } public void Dispose() { lock(_disposingSync) { if(_disposed) return; _disposed = true; } InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { lock(_disposingSync) { if(_disposed) { throw new ObjectDisposedException(); } } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
Le code de validation _disposed
dans Dispose () doit être initialisé en tant que section critique. En fait, tout le code des méthodes publiques devrait être initialisé en tant que section critique. Cela résoudra le problème de l'accès simultané à une méthode publique d'un type d'instance et à une méthode de destruction. Cependant, cela apporte d'autres problèmes qui deviennent une bombe à retardement:
- L'utilisation intensive des méthodes d'instance de type ainsi que la création et la destruction d'objets réduiront considérablement les performances. En effet, la prise d'un verrou prend du temps. Ce temps est nécessaire pour allouer les tables SyncBlockIndex, vérifier le thread actuel et bien d'autres choses (nous les traiterons dans le chapitre sur le multithreading). Cela signifie que nous devrons sacrifier les performances de l'objet tout au long de sa vie pour le «dernier kilomètre» de sa vie.
- Trafic mémoire supplémentaire pour les objets de synchronisation.
- Étapes supplémentaires que GC doit suivre pour parcourir un graphe d'objets.
Maintenant, nommons le deuxième et, à mon avis, la chose la plus importante. Nous permettons la destruction d'un objet et en même temps nous attendons à travailler avec lui à nouveau. Qu'espérons-nous dans cette situation? qu'il échouera? Parce que si Dispose s'exécute en premier, l'utilisation suivante des méthodes objet entraînera certainement ObjectDisposedException
. Vous devez donc déléguer la synchronisation entre les appels Dispose () et d'autres méthodes publiques d'un type du côté service, c'est-à-dire au code qui a créé l'instance de la classe FileWrapper
. C'est parce que seul le côté créateur sait ce qu'il fera avec une instance d'une classe et quand la détruire. D'un autre côté, un appel Dispose ne devrait produire que des erreurs critiques, telles que OutOfMemoryException
, mais pas IOException par exemple. Cela est dû aux exigences pour l'architecture des classes qui implémentent IDisposable. Cela signifie que si Dispose est appelé à partir de plusieurs threads à la fois, la destruction d'une entité peut se produire à partir de deux threads simultanément (nous sautons la vérification if(_disposed) return;
). Cela dépend de la situation: si une ressource peut être libérée plusieurs fois, il n'est pas nécessaire d'effectuer des vérifications supplémentaires. Sinon, une protection est nécessaire:
// I don't show the whole pattern on purpose as the example will be too long // and will not show the essence class Disposable : IDisposable { private volatile int _disposed; public void Dispose() { if(Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { // dispose } } }
Deux niveaux de principe de conception jetable
Quel est le modèle le plus populaire pour implémenter IDisposable
que vous pouvez rencontrer dans les livres .NET et Internet? Quel modèle attendez-vous de vous pendant les entretiens pour un nouvel emploi potentiel? Très probablement celui-ci:
public class Disposable : IDisposable { bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if(disposing) { // here we release managed resources } // here we release unmanaged resources } protected void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } ~Disposable() { Dispose(false); } }
Quel est le problème avec cet exemple et pourquoi nous n'avons pas écrit comme ça auparavant? En fait, c'est un bon modèle adapté à toutes les situations. Cependant, son utilisation omniprésente n'est pas un bon style à mon avis, car nous ne traitons presque pas les ressources non gérées dans la pratique, ce qui fait que la moitié du modèle ne sert à rien. De plus, puisqu'il gère simultanément des ressources gérées et non gérées, il viole le principe de partage des responsabilités. Je pense que c'est faux. Regardons une approche légèrement différente. Principe de conception jetable . En bref, cela fonctionne comme suit:
L'élimination est divisée en deux niveaux de classes:
- Les types de niveau 0 encapsulent directement les ressources non gérées
- Ils sont soit abstraits soit emballés.
- Toutes les méthodes doivent être marquées:
- PrePrepareMethod, afin qu'une méthode puisse être compilée lors du chargement d'un type
- SecuritySafeCritical pour se protéger contre un appel du code, fonctionnant sous des restrictions
- ReliabilityContract (Consistency.WillNotCorruptState, Cer.Success / MayFail)] pour mettre CER pour une méthode et tous ses appels enfants
- Ils peuvent référencer des types de niveau 0, mais doivent incrémenter le compteur d'objets référencés pour garantir le bon ordre d'entrée dans le «dernier kilomètre»
- Les types de niveau 1 encapsulent uniquement les ressources gérées
- Ils sont hérités uniquement des types de niveau 1 ou implémentent directement IDisposable
- Ils ne peuvent pas hériter des types de niveau 0 ou CriticalFinalizerObject
- Ils peuvent encapsuler les types gérés de niveau 1 et de niveau 0
- Ils implémentent IDisposable. Éliminer en détruisant les objets encapsulés à partir des types de niveau 0 et en allant au niveau 1
- Ils n'implémentent pas de finaliseur car ils ne traitent pas des ressources non gérées
- Ils doivent contenir une propriété protégée qui donne accès aux types de niveau 0.
C'est pourquoi j'ai utilisé la division en deux types depuis le début: celle qui contient une ressource gérée et celle avec une ressource non gérée. Ils devraient fonctionner différemment.
Autres façons d'utiliser Dispose
L'idée derrière la création d'IDisposable était de libérer des ressources non gérées. Mais comme pour de nombreux autres modèles, il est très utile pour d'autres tâches, par exemple pour publier des références à des ressources gérées. Bien que la libération des ressources gérées ne semble pas très utile. Je veux dire qu'ils sont appelés gérés exprès afin que nous nous détendions avec un sourire concernant les développeurs C / C ++, non? Mais ce n'est pas le cas. Il peut toujours y avoir une situation où nous perdons une référence à un objet mais en même temps pensons que tout va bien: GC va collecter les ordures, y compris notre objet. Cependant, il s'avère que la mémoire augmente. Nous entrons dans le programme d'analyse de la mémoire et voyons que quelque chose d'autre contient cet objet. Le fait est qu'il peut y avoir une logique pour la capture implicite d'une référence à votre entité dans la plate-forme .NET et l'architecture des classes externes. Comme la capture est implicite, un programmeur peut manquer la nécessité de sa libération et obtenir une fuite de mémoire.
Délégués, événements
Regardons cet exemple synthétique:
class Secondary { Action _action; void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action(); } } class Primary { Secondary _foo = new Secondary(); public void PlanSayHello() { _foo.SaveForUseInFuture(Strategy); } public void SayHello() { _foo.CallAction(); } void Strategy() { Console.WriteLine("Hello!"); } }
Quel problème ce code montre-t-il? La classe secondaire stocke Action
délégué de type Action
dans le champ _action
qui est accepté dans la méthode SaveForUseInFuture
. Ensuite, la méthode PlanSayHello
à Primary
intérieur de la classe Primary
transmet le pointeur de la méthode Strategy
à la classe Secondary
. C'est curieux mais si, dans cet exemple, vous passez quelque part une méthode statique ou une méthode d'instance, la SaveForUseInFuture
passée ne sera pas modifiée, mais une instance de classe Primary
sera référencée implicitement ou ne sera pas référencée du tout. Extérieurement, il semble que vous ayez demandé la méthode à appeler. Mais en fait, un délégué est construit non seulement à l'aide d'un pointeur de méthode, mais également à l'aide du pointeur vers une instance d'une classe. Un appelant doit comprendre pour quelle instance d'une classe il doit appeler la méthode Strategy
! C'est l'instance de la classe Secondary
qui a implicitement accepté et détient le pointeur vers l'instance de la classe Primary
, bien qu'il ne soit pas indiqué explicitement. Pour nous, cela signifie seulement que si nous passons le pointeur _foo
ailleurs et perdons la référence à Primary
, GC ne collectera pas d' objet Primary
, car Secondary
le conservera. Comment éviter de telles situations? Nous avons besoin d'une approche déterminée pour nous communiquer une référence. Un mécanisme qui correspond parfaitement à cet objectif est IDisposable
// This is a simplified implementation class Secondary : IDisposable { Action _action; public event Action<Secondary> OnDisposed; public void SaveForUseInFuture(Action action) { _action = action; } public void CallAction() { _action?.Invoke(); } void Dispose() { _action = null; OnDisposed?.Invoke(this); } }
Maintenant, l'exemple semble acceptable. Si une instance d'une classe est transmise à un tiers et que la référence à _action
delegate sera perdue au cours de ce processus, nous la mettrons à zéro et le tiers sera informé de la destruction de l'instance et supprimera la référence à celle-ci .
Le deuxième danger du code qui s'exécute sur les délégués est le principe de fonctionnement de l' event
. Voyons ce qu'ils entraînent:
// a private field of a handler private Action<Secondary> _event; // add/remove methods are marked as [MethodImpl(MethodImplOptions.Synchronized)] // that is similar to lock(this) public event Action<Secondary> OnDisposed { add { lock(this) { _event += value; } } remove { lock(this) { _event -= value; } } }
La messagerie C # masque les éléments internes des événements et contient tous les objets qui se sont abonnés pour se mettre à jour via l' event
. Si quelque chose se passe mal, une référence à un objet signé reste dans OnDisposed
et contiendra l'objet. C'est une situation étrange car en termes d'architecture, nous obtenons un concept de «source d'événements» qui ne devrait rien contenir logiquement. Mais en fait, les objets abonnés à la mise à jour sont conservés implicitement. De plus, nous ne pouvons pas changer quelque chose à l'intérieur de ce tableau de délégués bien que l'entité nous appartienne. La seule chose que nous pouvons faire est de supprimer cette liste en affectant null à une source d'événements.
La deuxième façon consiste à implémenter explicitement les méthodes d' add
/ remove
, afin que nous puissions contrôler une collection de délégués.
Une autre situation implicite peut apparaître ici. Il peut sembler que si vous attribuez null à une source d'événements, l'abonnement suivant aux événements provoquera NullReferenceException
. Je pense que ce serait plus logique.
Mais ce n'est pas vrai. Si le code externe s'abonne aux événements après la OnDisposed
une source d'événements, FCL créera une nouvelle instance de la classe Action et la stockera dans OnDisposed
. Cette implicitation en C # peut induire en erreur un programmeur: traiter des champs nuls devrait produire une sorte de vigilance plutôt que de calme. Ici, nous démontrons également une approche lorsque l'insouciance d'un programmeur peut entraîner des fuites de mémoire.
Fermetures Lambdas
L'utilisation de sucre syntaxique comme les lambdas est particulièrement dangereuse.
Je voudrais aborder le sucre syntaxique dans son ensemble. Je pense que vous devriez l'utiliser plutôt soigneusement et seulement si vous connaissez exactement le résultat. Les exemples avec des expressions lambda sont les fermetures, les fermetures dans les expressions et de nombreuses autres misères que vous pouvez vous infliger.
Bien sûr, vous pouvez dire que vous savez qu'une expression lambda crée une fermeture et peut entraîner un risque de fuite de ressources. Mais il est si soigné, si agréable qu'il est difficile d'éviter d'utiliser lambda au lieu d'allouer la méthode entière, qui sera décrite à un endroit différent de celui où elle sera utilisée. En fait, vous ne devriez pas adhérer à cette provocation, bien que tout le monde ne puisse pas résister. Regardons l'exemple:
button.Clicked += () => service.SendMessageAsync(MessageType.Deploy);
D'accord, cette ligne semble très sûre. Mais cela cache un gros problème: maintenant, la variable button
implicitement référence au service
et le maintient. Même si nous décidons que nous n'avons plus besoin de service
, le button
conservera la référence tant que cette variable sera active. L'une des façons de résoudre ce problème consiste à utiliser un modèle pour créer IDisposable
partir de n'importe quelle Action
( System.Reactive.Disposables
):
// Here we create a delegate from a lambda Action action = () => service.SendMessageAsync(MessageType.Deploy); // Here we subscribe button.Clicked += action; // We unsubscribe var subscription = Disposable.Create(() => button.Clicked -= action); // where it is necessary subscription.Dispose();
Admettez, cela semble un peu long et nous perdons tout le but d'utiliser des expressions lambda. Il est beaucoup plus sûr et plus simple d'utiliser des méthodes privées communes pour capturer implicitement des variables.
Protection Threadabort
Lorsque vous créez une bibliothèque pour un développeur tiers, vous ne pouvez pas prédire son comportement dans une application tierce. Parfois, vous ne pouvez que deviner ce qu'un programmeur a fait à votre bibliothèque qui a provoqué un résultat particulier. Un exemple est le fonctionnement dans un environnement multithread lorsque la cohérence du nettoyage des ressources peut devenir un problème critique. Notez que lorsque nous écrivons la méthode Dispose()
, nous pouvons garantir l'absence d'exceptions. Cependant, nous ne pouvons pas garantir que lors de l'exécution de la méthode Dispose()
aucune ThreadAbortException
ne se produira qui désactivera notre thread d'exécution. Ici, nous devons nous rappeler que lorsque ThreadAbortException
se produit, tous les blocs catch / finally sont exécutés de toute façon (à la fin d'un bloc catch / finally, ThreadAbort se produit plus loin). Donc, pour assurer l'exécution d'un certain code en utilisant Thread.Abort, vous devez encapsuler une section critique dans try { ... } finally { ... }
, voir l'exemple ci-dessous:
void Dispose() { if(_disposed) return; _someInstance.Unsubscribe(this); _disposed = true; }
On peut abandonner cela à tout moment en utilisant Thread.Abort
. Il détruit partiellement un objet, bien que vous puissiez toujours travailler avec lui à l'avenir. Dans le même temps, le code suivant:
void Dispose() { if(_disposed) return; // ThreadAbortException protection try {} finally { _someInstance.Unsubscribe(this); _disposed = true; } }
est protégé contre un tel abandon et fonctionnera sans problème et à coup sûr, même si Thread.Abort
apparaît entre l'appel de la méthode Thread.Abort
et l'exécution de ses instructions.
Résultats
Les avantages
Eh bien, nous avons beaucoup appris sur ce modèle le plus simple. Déterminons ses avantages:
- Le principal avantage du modèle est la capacité de libérer des ressources de façon déterminante, c'est-à-dire lorsque vous en avez besoin.
- Le deuxième avantage est l'introduction d'un moyen éprouvé de vérifier si une instance spécifique nécessite de détruire ses instances après utilisation.
- Si vous implémentez le modèle correctement, un type conçu fonctionnera en toute sécurité en termes d'utilisation par des composants tiers ainsi qu'en termes de déchargement et de destruction de ressources lorsqu'un processus se bloque (par exemple en raison d'un manque de mémoire). C'est le dernier avantage.
Inconvénients
À mon avis, ce modèle présente plus d'inconvénients que d'avantages.
- D'une part, tout type qui implémente ce modèle indique aux autres parties que s'il l'utilise, il prend une sorte d'offre publique. Ceci est tellement implicite que, comme dans le cas d'offres publiques, un utilisateur d'un type ne sait pas toujours que le type a cette interface. Vous devez donc suivre les invites IDE (tapez un point, Dis ... et vérifiez s'il existe une méthode dans la liste des membres filtrés d'une classe). Si vous voyez un modèle Dispose, vous devez l'implémenter dans votre code. Parfois, cela ne se produit pas immédiatement et dans ce cas, vous devez implémenter un modèle via un système de types qui ajoute des fonctionnalités. Un bon exemple est que
IEnumerator<T>
implique IDisposable
. - Habituellement, lorsque vous concevez une interface, il est nécessaire d'insérer IDisposable dans le système des interfaces d'un type lorsqu'une des interfaces doit hériter d'IDisposable. À mon avis, cela endommage les interfaces que nous avons conçues. Je veux dire que lorsque vous concevez une interface, vous créez d'abord un protocole d'interaction. Il s'agit d'un ensemble d'actions que vous pouvez effectuer avec quelque chose caché derrière l'interface.
Dispose()
est une méthode pour détruire une instance d'une classe. Cela contredit l'essence d'un protocole d'interaction . En fait, ce sont les détails de l'implémentation qui se sont infiltrés dans l'interface. - Bien qu'il soit déterminé, Dispose () ne signifie pas la destruction directe d'un objet. L'objet existera toujours après sa destruction mais dans un autre état. Pour que cela soit vrai, CheckDisposed () doit être la première commande de chaque méthode publique. Cela ressemble à une solution temporaire que quelqu'un nous a donnée en disant: «Allez-y et multipliez-vous»;
- Il existe également une petite chance d'obtenir un type qui implémente
IDisposable
via une implémentation explicite . Ou vous pouvez obtenir un type qui implémente ID isposable sans avoir la possibilité de déterminer qui doit le détruire: vous ou la partie qui vous l'a donné. Cela a abouti à un contre-modèle d'appels multiples de Dispose () qui permet de détruire un objet détruit; - L'implémentation complète est difficile et différente pour les ressources gérées et non gérées. Ici, la tentative de faciliter le travail des développeurs via GC semble maladroite. Vous pouvez remplacer
virtual void Dispose()
méthode virtual void Dispose()
et introduire un type DisposableObject qui implémente l'ensemble du modèle, mais cela ne résout pas les autres problèmes liés au modèle; - En règle générale, la méthode Dispose () est implémentée à la fin d'un fichier tandis que '.ctor' est déclaré au début. Si vous modifiez une classe ou introduisez de nouvelles ressources, il est facile d'oublier d'ajouter une élimination pour elles.
- Enfin, il est difficile de déterminer l'ordre de destruction dans un environnement multithread lorsque vous utilisez un modèle pour les graphiques d'objets où les objets implémentent entièrement ou partiellement ce modèle. Je veux dire des situations où Dispose () peut commencer à différentes extrémités d'un graphique. Ici, il est préférable d'utiliser d'autres modèles, par exemple le modèle Lifetime.
- Le souhait des développeurs de plates-formes d'automatiser le contrôle de la mémoire combiné aux réalités: les applications interagissent très souvent avec du code non managé + vous devez contrôler la publication des références aux objets afin que Garbage Collector puisse les collecter. Cela ajoute une grande confusion dans la compréhension de questions telles que: «Comment devrions-nous implémenter correctement un modèle»? «Existe-t-il un modèle fiable du tout»? Peut-être en appelant
delete obj; delete[] arr;
delete obj; delete[] arr;
est plus simple?
Déchargement de domaine et sortie d'une application
Si vous arrivez à cette partie, vous êtes devenu plus confiant dans le succès des futurs entretiens d'embauche. Cependant, nous n'avons pas discuté de toutes les questions liées à ce modèle aussi simple que cela puisse paraître. La dernière question est de savoir si le comportement d'une application diffère en cas de garbage collection simple et lorsque les déchets sont collectés lors du déchargement de domaine et lors de la fermeture de l'application. Cette question touche simplement Dispose()
... Cependant Dispose()
et la finalisation vont de pair et nous rencontrons rarement une implémentation d'une classe qui a la finalisation mais n'a pas la méthode Dispose()
. Décrivons donc la finalisation dans une section distincte. Ici, nous ajoutons juste quelques détails importants.
Lors du déchargement du domaine d'application, vous déchargez les assemblys chargés dans le domaine d'application et tous les objets créés dans le cadre du domaine à décharger. En fait, cela signifie le nettoyage (collecte par GC) de ces objets et l'appel de finaliseurs pour eux. Si la logique d'un finaliseur attend que la finalisation d'autres objets soit détruite dans le bon ordre, vous pouvez prêter attention à Environment.HasShutdownStarted
propriété Environment.HasShutdownStarted
indiquant qu'une application est déchargée de la mémoire et à la méthode AppDomain.CurrentDomain.IsFinalizingForUnload()
indiquant que cela le domaine est déchargé, ce qui est la raison de la finalisation. Si ces événements se produisent, l'ordre de finalisation des ressources devient généralement sans importance. Nous ne pouvons pas retarder le déchargement de domaine ou d'une application car nous devons tout faire le plus rapidement possible.
C'est ainsi que cette tâche est résolue dans le cadre d'une classe LoaderAllocatorScout
// Assemblies and LoaderAllocators will be cleaned up during AppDomain shutdown in // an unmanaged code // So it is ok to skip reregistration and cleanup for finalization during appdomain shutdown. // We also avoid early finalization of LoaderAllocatorScout due to AD unload when the object was inside DelayedFinalizationList. if (!Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload()) { // Destroy returns false if the managed LoaderAllocator is still alive. if (!Destroy(m_nativeLoaderAllocator)) { // Somebody might have been holding a reference on us via weak handle. // We will keep trying. It will be hopefully released eventually. GC.ReRegisterForFinalize(this); } }
Erreurs d'implémentation typiques
Comme je vous l'ai montré, il n'y a pas de modèle universel pour implémenter IDisposable. De plus, une certaine dépendance au contrôle automatique de la mémoire induit les gens en erreur et prend des décisions confuses lors de la mise en œuvre d'un modèle. L'ensemble du .NET Framework est truffé d'erreurs dans sa mise en œuvre. Pour prouver mon point, examinons ces erreurs en utilisant exactement l'exemple de .NET Framework. Toutes les implémentations sont disponibles via: IDisposable Usages
Classe FileEntry cmsinterop.cs
Ce code est écrit à la hâte juste pour fermer le problème. De toute évidence, l'auteur voulait faire quelque chose mais a changé d'avis et a gardé une solution défectueuse
internal class FileEntry : IDisposable { // Other fields // ... [MarshalAs(UnmanagedType.SysInt)] public IntPtr HashValue; // ... ~FileEntry() { Dispose(false); } // The implementation is hidden and complicates calling the *right* version of a method. void IDisposable.Dispose() { this.Dispose(true); } // Choosing a public method is a serious mistake that allows for incorrect destruction of // an instance of a class. Moreover, you CANNOT call this method from the outside public void Dispose(bool fDisposing) { if (HashValue != IntPtr.Zero) { Marshal.FreeCoTaskMem(HashValue); HashValue = IntPtr.Zero; } if (fDisposing) { if( MuiMapping != null) { MuiMapping.Dispose(true); MuiMapping = null; } System.GC.SuppressFinalize(this); } } }
Système de classe SemaphoreSlim / Filetage / SemaphoreSlim.cs
Cette erreur se trouve en haut des erreurs de .NET Framework concernant IDisposable: SuppressFinalize pour les classes où il n'y a pas de finaliseur. C'est très courant.
public void Dispose() { Dispose(true); // As the class doesn't have a finalizer, there is no need in GC.SuppressFinalize GC.SuppressFinalize(this); } // The implementation of this pattern assumes the finalizer exists. But it doesn't. // It was possible to do with just public virtual void Dispose() protected virtual void Dispose(bool disposing) { if (disposing) { if (m_waitHandle != null) { m_waitHandle.Close(); m_waitHandle = null; } m_lockObj = null; m_asyncHead = null; m_asyncTail = null; } }
Appel de Close + Dispose Some Code de projet NativeWatcher
Parfois, les gens appellent à la fois Close et Dispose. C'est faux mais cela ne produira pas d'erreur car le deuxième Dispose ne génère pas d'exception.
En fait, Close est un autre modèle pour rendre les choses plus claires pour les gens. Cependant, cela rendait tout plus flou.
public void Dispose() { if (MainForm != null) { MainForm.Close(); MainForm.Dispose(); } MainForm = null; }
Résultats généraux
- IDposable est un standard de la plateforme et la qualité de sa mise en œuvre influence la qualité de l'ensemble de l'application. De plus, dans certaines situations, cela influence la sécurité de votre application qui peut être attaquée via des ressources non gérées.
- La mise en œuvre d'IDisposable doit être au maximum productive. Cela est particulièrement vrai pour la section de finalisation, qui fonctionne en parallèle avec le reste du code, en chargeant Garbage Collector.
- Lorsque vous implémentez IDisposable, vous ne devez pas utiliser Dispose () simultanément avec les méthodes publiques d'une classe. La destruction ne peut pas aller de pair avec l'usage. Cela doit être pris en compte lors de la conception d'un type qui utilisera un objet IDisposable.
- Cependant, il devrait y avoir une protection contre l'appel simultané de «Dispose ()» à partir de deux threads. Cela résulte de l'instruction que Dispose () ne doit pas produire d'erreurs.
- Les types qui contiennent des ressources non gérées doivent être séparés des autres types. Je veux dire que si vous encapsulez une ressource non gérée, vous devez lui allouer un type distinct. Ce type doit contenir la finalisation et doit être hérité de
SafeHandle / CriticalHandle / CriticalFinalizerObject
. Cette séparation des responsabilités se traduira par une meilleure prise en charge du système de types et simplifiera l'implémentation pour détruire les instances de types via Dispose (): les types avec cette implémentation n'auront pas besoin d'implémenter un finaliseur. - En général, ce modèle n'est pas confortable à utiliser ainsi que dans la maintenance du code. Probablement, nous devrions utiliser l'approche d'inversion de contrôle lorsque nous détruisons l'état des objets via le modèle
Lifetime
. Cependant, nous en parlerons dans la section suivante.
Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.
Aussi, si vous voulez nous remercier, la meilleure façon de le faire est de nous donner une étoile sur github ou sur fork repository
github / sidristij / dotnetbook .