Bonnes pratiques de programmation avec C# (seconde édition) par Patrick Smacchia

Épisode 2
Impression papier : environ 9 pages
Version 1.0

Métrique du Framework .NET. 1

Un gros avantage du mot-clé C# lock sur la classe Monitor 3

Partager un événement entre processus. 4

Exécuter les méthodes abonnées à un événement d’une manière asynchrone. 5

Se protéger des exceptions lancées par les méthodes abonnées à un événement 7

Métriques du Framework .NET

Lors de la discussion autour d’un précédent article concernant FxCop, je pensais qu’il serait intéressant que cet outil signale des dépassements de seuils de métrique de code. De là m’est venu l’idée de mesurer quelques valeurs pertinentes concernant le Framework .NET 1.1. Voici les résultats.

 

Description de la métrique

Moyenne

Ecart Type

Echantillon

Maximum

Nombre de méthodes dans les interfaces publiques

(Les surcharges d’une méthode comptent toutes pour 1)

2.88

3.63

271 interfaces

23 méthodes pour l’interface System._AppDomain

Nombre de méthodes dans les interfaces publiques

(Chaque surcharge d’une méthode compte pour 1)

3.15

4.52

271 interfaces

43 méthodes pour l’interface System._AppDomain

Nombre de méthodes dans les classes publiques

(Les surcharges d’une méthode comptent toutes pour 1) (*)

23.72

44.04

2291 classes

446 méthodes pour la classe System.Windows.Forms.DataGrid

Nombre de méthodes dans les classes publiques

(Chaque surcharge d’une méthode compte pour 1) (*)

26,48

48.19

2291 classes

473 méthodes pour la classe System.Windows.Forms.DataGrid

Nombre de méthodes publiques dans les classes publiques

(Les surcharges d’une méthode comptent toutes pour 1) (*)

10.26

9.22

2291 classes

86 méthodes pour la classe System.Runtime.InteropServices.Marshal

Nombre de méthodes publiques dans les classes publiques

(Chaque surcharge d’une méthode compte pour 1) (*)

12.44

15.19

2291 classes

331 méthodes pour la classe System.Convert

Nombre de propriétés dans les interfaces publiques

1.00

1.94

271 interfaces

141 propriétés pour l’interface Accessibility.IAccessible

Nombre de propriétés dans les classes publiques

7.12

14.03

2291 classes

14 propriétés pour les classes System.Drawing.Brushes et System.Drawing.Pens

Nombre de paramètres des méthodes des interfaces publiques

1.62

1.67

855 méthodes

13 paramètres pour la méthode AddServerTlb() de l’interface System.EnterpriseServices.Internal.ISoapServerTlb

Nombre de paramètres des méthodes des classes publiques

0.98

1.19

60665 méthodes

15 paramètres pour la méthode PushScanner() de la classes System.Xml.XmlTextReader

Nombre de paramètres des méthodes publiques des classes publiques

0.90

1,15

28508 méthodes

14 paramètres pour la méthode DrawBorder() de la classes System.Windows.Forms.ControlPaint

Profondeur des classes dans l’arbre d’héritage d’implémentation (**)

2.00

1.24

5467 classes

Profondeur de 10 pour la classe System.Xml.Schema.Datatype_unsignedByte

Profondeur des classes publiques dans l’arbre d’héritage d’implémentation (**)

2.24

1.14

2291 classes

Profondeur de 7 pour les classes System.Web.UI.Design.WebControls.CalendarAutoFormatDialog 

System.Web.UI.Design.WebControls.RegexEditorDialog 

System.Messaging.Design.QueuePathDialog 

System.ServiceProcess.Design.ServiceInstallerDialog 

System.Windows.Forms.DomainUpDown 

System.Windows.Forms.NumericUpDown 

System.Windows.Forms.ThreadExceptionDialog 

System.Windows.Forms.Design.ComponentEditorForm 

System.Windows.Forms.PrintPreviewDialog

Nombre d’interfaces implémentées ou héritées par une classe

1.14

2.69

5467 classes

19 interfaces implémentées ou héritées par les classes

System.Windows.Forms.Design.ComponentTray 

System.Windows.Forms.Design.ComponentDocumentDesigner

System.Windows.Forms.PropertyGrid

Nombre d’interfaces implémentées ou héritées par une classe publique

1.50

2.76

2291 classes

19 interfaces implémentées ou héritées par les classes

System.Windows.Forms.Design.ComponentTray  System.Windows.Forms.PropertyGrid

Nombre de classes par espace de noms

43.06

61.62

126 espaces de

noms

564 classes contenues dans l’espace de noms

System.Windows.Forms

Nombre de classes publiques par espace de noms

18.17

27.07

126 espaces de

noms

206 classes publiques contenues dans l’espace de noms

System.Windows.Forms

Nombre de types valeur par espace de noms

9.02

16.93

126 espaces de

noms

145 types valeur contenus dans l’espace de noms

System.Windows.Forms

Nombre de types valeur publics par espace de noms

4.60

10.92

126 espaces de

noms

96 types valeur publics contenus dans l’espace de noms

System.Windows.Forms

 

(*)  Les six méthodes suivantes de la classe System.Object (implémentées par toutes les classes du Framework) sont prises en compte dans le calcul de ces statistiques: Finalize() GetHashCode() Equals() ToString() GetType() MemberwiseClone()

 

(**) Le fait que toutes les classes dérivent de la classe System.Object et ont donc au moins une profondeur de 1 dans l’arbre d’héritage d’implémentation, est pris en compte dans le calcul de ces statistiques.

 

Vous pouvez télécharger le petit programme qui m’a servi à obtenir ces valeurs ici. Ce programme peut aussi vous servir à obtenir ces valeurs sur vos propres assemblages. Il suffit de remplacer la fonction LoadSystemAssemblies() par une fonction qui charge vos assemblage dans le domaine d’application courant. Ne tenez pas compte de l’assemblage nommé mscorlib.

Le mot-clé C# lock et la classe Monitor

Nous avions vu dans le précédent article concernant les bonnes pratiques de programmation avec C#, que l’utilisation du mot clé lock de C# provoquait l’utilisation de la classe de synchronisation System.Threading.Monitor. La signature des méthodes statiques Enter() et Exit() de cette classe est la suivante :

 

public static void Enter(object o)

public static void Exit(object o)

 

Ainsi, lorsque vous souhaitez synchroniser une portion de code avec la classe Monitor, il vous faut fournir un objet par référence. Un problème se pose cependant lorsque vous fournissez par référence un objet de type valeur (i.e une structure ou une instance d’un type primitif, int, double… excepté string). En effet, un tel objet est alors boxé, c’est à dire qu’une copie de cet objet est automatiquement réalisée dans le tas du process courant, et une référence vers cette copie est passée. Le problème est qu’une telle copie est réalisée à chaque appel de la méthode Enter() et à chaque appel de la méthode Exit(). En conséquence, tout se passe comme si l’on synchronisait notre portion de code avec un objet différent à chaque passage. Il n’y a donc aucune synchronisation réalisée ! Pour vous en convaincre, analysez le programme suivant qui s’exécute en 5 secondes lorsque SyncObj est de type int (type valeur) et en 10 secondes lorsque SyncObj est de type string (type référence).

 

using System;

using System.Threading;

 

class Prog

{

   static void Main(string[] args)

   {

      Thread t1 = new Thread(new ThreadStart( f ));

      Thread t2 = new Thread(new ThreadStart( f ));

      t1.Start();t2.Start();t1.Join(); t2.Join();

   }

   static int SyncObj = 0;

   //static string SyncObj = "Foo";

   static void f()

   {

      for(int i =0;i<5;i++)

      {

         try

         {

            Monitor.Enter(SyncObj);

            Console.WriteLine("{0}",DateTime.Now.Second);

            Thread.Sleep(1000);

         }

         finally

         {

            Monitor.Exit(SyncObj);

         }

      }

   }

}

 

Ce problème est d’autant plus vicieux que le compilateur n’a aucun moyen de vous prévenir puisqu’il est autorisé à générer une opération de boxing lorsqu’une instance d’un type valeur doit être utilisée par l’intermédiaire d’une référence. Une bonne nouvelle est que l’utilisation du mot-clé lock modifie ce comportement du compilateur C#, et ce dernier produit une erreur si l’objet avec lequel on se synchronise est de type valeur. Voici donc une raison autre que le bienfait syntaxique d’utiliser le mot clé lock à la place de la classe Monitor.

 

Remarquez que la fonctionnalité apportée par les méthodes statiques Wait(), Pulse() et PulseAll() de la classe Monitor, n’est pas couverte par l’utilisation du mot-clé lock. Ainsi, lors de l’utilisation de cette fonctionnalité la classe Monitor est utilisée explicitement. On pourrait arguer que dans ce cas il est toujours possible de se synchroniser par inadvertance sur une instance d’un type valeur sans être averti par le compilateur. Cependant, une telle faute entraîne des durées d’attentes anormales (voire infinies), et produit donc un comportement aberrant facilement détectable.

Partager un événement entre processus

Nous allons montrer ici une façon de contourner une petite incohérence du Framework .NET. En effet, à l’instar de l’API win32, le Framework .NET nous permet de nommer les mutex (classe System.Threading.Mutex). Rappelons que l’intérêt principal d’un mutex nommé est qu’il peut être utilisé pour synchroniser des threads qui ne sont pas dans le même processus. En outre l’API win32 permet également de nommer les événement et donc, de les partager entre processus. Bizarrement, les événements du Framework .NET (classe System.Threading.ManualResetEvent et System.Threading.AutoResetEvent) ne peuvent pas être nommés, et par conséquent un événement .NET ne peut pas être utilisé par plusieurs processus. Ce problème est assez gênant puisque de nombreuses architectures multi-processus requièrent de partager un événement.

 

La solution proposée par la classe suivante est très simple : elle encapsule (wrappe) les fonctions de l’API win32 relatives aux évènements nommés. Elle constitue en outre un bon exemple de l’utilisation du mécanisme Platform Invoke qui permet d’appeler des fonctions de DLLs natives à partir de vos programmes .NET:

 

using System;

using System.Runtime.InteropServices ;

 

public class NamedEvent

{

   [DllImport("kernel32", EntryPoint="CreateEvent", SetLastError = true)]

   private static extern IntPtr CreateEvent(

      IntPtr   eventAttributes,

      bool   bManualReset,

      bool   bInitialState,

      String   Name);

 

   [DllImport("kernel32", EntryPoint="WaitForSingleObject", SetLastError = true)]

   private static extern int WaitForSingleObject(IntPtr hEvent, int dwMilliseconds);

 

   [DllImport("kernel32", EntryPoint="SetEvent", SetLastError = true)]

   private static extern bool SetEvent(IntPtr hEvent);

 

   [DllImport("kernel32", EntryPoint="ResetEvent", SetLastError = true)]

   private static extern bool ResetEvent(IntPtr hEvent);

 

   [DllImport("kernel32", EntryPoint="CloseHandle", SetLastError = true)]

   private static extern IntPtr CloseHandle(IntPtr hEvent);

 

   private const int WAIT_0 = 0x00000000;

   private const int WAIT_TIMEOUT = 0x00000102;

   private const int WAIT_ABANDONED = 0x00000080;  

   private IntPtr m_Handle = IntPtr.Zero;

 

   public enum WaitStatus{WAIT_0,WAIT_TIMEOUT,WAIT_ABANDONED,WAIT_UNKNOWN}

           

   public NamedEvent(bool   bManualReset,bool   bInitialState,String   Name)

   {

      m_Handle = CreateEvent(IntPtr.Zero,bManualReset,bInitialState,Name);

   }

   public WaitStatus Wait(int dwMilliseconds)

   {

      int Ret = WaitForSingleObject(m_Handle,dwMilliseconds);

      switch(Ret)

      {

         case WAIT_0:

            return WaitStatus.WAIT_0;

         case WAIT_TIMEOUT:

            return WaitStatus.WAIT_TIMEOUT;

         case WAIT_ABANDONED:

            return WaitStatus.WAIT_ABANDONED;

      }

      return WaitStatus.WAIT_UNKNOWN;

   }

 

   public bool SetEvent()

   {

      return SetEvent(m_Handle);

   }

 

   public bool ResetEvent()

   {

      return ResetEvent(m_Handle);

   }

 

   public void CloseHandle()

   {

      if( m_Handle != IntPtr.Zero )

         CloseHandle(m_Handle);

      m_Handle = IntPtr.Zero;

   }

}

Exécuter les méthodes abonnées à un événement d’une manière asynchrone

Attention, ici nous parlons d’événements .NET (mot-clé C# event) et non pas d’événements de synchronisation comme dans la section précédente.

 

Lorsqu’un évènement est déclenché, il faut bien être conscient que c’est le même thread qui exécute l’instruction de déclenchement et les méthodes abonnées à l’évènement. Un problème potentiel est que certaines méthodes abonnées s’exécutent en un temps inacceptable. Un autre problème potentiel est qu’une méthode abonnée peut ne pas rattraper une exception. Cette dernière empêchera alors les exécutions des autres méthodes abonnées. De plus une exception lancée dans une méthode abonnée ne devrait pas être propagée dans la méthode qui déclenche un événement. En effet, cette dernière doit être totalement découplée des méthodes abonnées.