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.

 

Pour pallier ces problèmes, il serait bien pratique que les méthodes abonnées soient exécutées d’une manière asynchrone. En outre, il serait aussi intéressant que différents threads exécutent les différentes méthodes abonnées. Ainsi, une méthode abonnée qui s’exécute en un temps inacceptable ou qui lance une exception ne gène ni l’exécution du déclencheur de l’événement, ni les exécutions des autres méthodes abonnées. En fait, le pool de threads .NET est particulièrement adapté à ces deux contraintes.

 

Ceux qui connaissent la syntaxe d’un appel asynchrone de méthode risquent d’être tentés d’utiliser directement la fonction BeginInvoke() sur l’événement lui-même. Le problème de cette solution est que seule une méthode abonnée sera exécutée. La bonne solution, illustrée par l’exemple suivant (tiré de l’exemple 13-9 de Pratique de .NET et C#) nécessite le parcours explicite de la liste des méthodes abonnées: (Notez qu’il n’y a pas lieu d’appeler la méthode EndInvoke() puisque les méthodes abonnées ne sont pas sensées retourner de résultat à la méthode qui a déclenché l’événement).

 

using System;

  

class Abonné

{

   private string m_Nom;

   public Abonné( string Nom ) { m_Nom = Nom; }

   // Méthode à appeler lorsqu'un événement est déclenché

   // (i.e lorsqu'un bulletin d'information est publié).

   public void ReceptionInfo(object sender, EventArgs e)

   {

      InfoEventArgs Info = e as InfoEventArgs;

      if( Info != null )

      {

         Console.WriteLine(m_Nom + " reçoit l'info:" + ((InfoEventArgs)e).GetBulletinInfo() );

      }

   }

}

 

//La classe d’argument de l’événement bulletin d'information

class InfoEventArgs: EventArgs

{

   private string m_Description;

   public string GetBulletinInfo() { return m_Description; }

   public InfoEventArgs (string Description){ m_Description = Description; }

}

 

// Définition du type délégué 'handler d'un bulletin d'information'

public delegate void InfoEventHandler(object sender, EventArgs e);

 

// Classe contenant les événements bulletin d'information

class AgenceDePresse

{

   // Définition des événements bulletins d'information.

   public event InfoEventHandler InfoFrance;

   public event InfoEventHandler InfoMonde;

 

   // Méthodes de déclenchement des événements

   // (i.e de publication de bulletin d'information).

   public void OnInfoFrance(InfoEventArgs BulletinInfo)

   {

      if( InfoFrance != null )  

      {

         // Ce code permet l’exécution asynchrone des méthodes abonnées à l’évènement

         Delegate[] DTab = InfoFrance.GetInvocationList();

         foreach(Delegate tmp in DTab )

         {

            InfoEventHandler Dtmp = (InfoEventHandler) tmp;

            Dtmp.BeginInvoke(this,BulletinInfo,null,null);

         }

         // au lieu de :

         // if( InfoFrance != null )    InfoFrance(this,BulletinInfo)

 

         // attend un peu pour permettre les exécutions asynchrones

         // celles ci se faisant sur des threads du pool,

         // qui sont de threads background

         System.Threading.Thread.Sleep(100);

      }

   }

   public void OnInfoMonde(InfoEventArgs BulletinInfo)

   {

      if( InfoMonde != null )     InfoMonde(this,BulletinInfo);

   }

}

 

class Prog

{

   public static void Main(string[] args)

   {

      // Création de l'agence de presse.

      AgenceDePresse AgencePresse = new AgenceDePresse();

 

      // Création des abonnés.

      Abonné Raymond = new Abonné("Raymond");

      Abonné Olivier = new Abonné("Olivier");

      Abonné Mathieu = new Abonné("Mathieu");

 

      // Création des abonnements aux événements bulletins d'information.

      AgencePresse.InfoFrance += new InfoEventHandler(Raymond.ReceptionInfo );

      AgencePresse.InfoFrance += new InfoEventHandler(Olivier.ReceptionInfo );

      AgencePresse.InfoMonde  += new InfoEventHandler(Olivier.ReceptionInfo );

      AgencePresse.InfoMonde  += new InfoEventHandler(Mathieu.ReceptionInfo );

         

      // Publication de bulletins d'information

      //(déclenchement des événement).

      AgencePresse.OnInfoFrance(new InfoEventArgs("Hausse du prix du tabac."));

      AgencePresse.OnInfoMonde(new InfoEventArgs("Nouvelle élection au Etats-Unis."));

         

      // Résilation d'abonnement.

      AgencePresse.InfoFrance -= new InfoEventHandler(Olivier.ReceptionInfo );

         

      // Publication de bulletins d'information.

      AgencePresse.OnInfoFrance(new InfoEventArgs ("Baisse des impôts."));

   }

}

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

Invoquer les méthodes abonnées à un événement d’une manière asynchrone est une technique efficace pour protéger le code qui déclenche un évènement des exceptions remontées par les méthodes abonnées. Il est cependant possible d’avoir le même niveau de protection en invoquant les méthodes d’une manière synchrone. Pour cela, il suffit d'invoquer les différentes méthodes abonnées une à une, comme le montre l’exemple suivant:

 

...

   // Méthodes de déclenchement des événements

   // (i.e de publication de bulletin d'information).

   public void OnInfoFrance(InfoEventArgs BulletinInfo)

   {

      if( InfoFrance != null )  

      {

         Delegate[] DTab = InfoFrance.GetInvocationList();

         foreach(Delegate tmp in DTab )

         {

            InfoEventHandler Dtmp = (InfoEventHandler) tmp;

            try

            {

               Dtmp(this,BulletinInfo);

            }

            catch(Exception){/*traitement d'exception*/}

         }

 

      }

   }

...

 

Auteurs : Patrick Smacchia

Copyright © Décembre 2003

Patrick Smacchia, email [patrick@smacchia.com]

Patrick Smacchia assure de nombreuses formations sur .NET, à la fois dans l’industrie et dans le milieu universitaire (Université de Nice). Passionné par l’architecture logicielle, il aide les entreprises à concevoir et à développer leurs applications. Ingénieur diplômé de l’ENSEEIHT, il a notamment collaboré avec Amadeus et avec les divisions espace et téléphonie mobile d’Alcatel. Son site expose plus en détail ses activités. Ses compétences ont été reconnues par Microsoft France, ce qui lui a valu la distinction MVP .NET (Most Valuable Professional sur les technologies .NET).

L’ouvrage Pratique de .NET et C# (O’Reilly 2003)

L’ ouvrage Pratique de .NET et C# (O’Reilly 2003) couvre la plupart des aspects du développement sous .NET avec le langage C# (architecture .NET sous jacente; langage C#, bibliothèques ADO.NET, XML, WinForm, GDI+, architectures distribuées avec COM+, .NET Remoting et ASP.NET etc.). Cet ouvrage contient de nombreux rappels pour le rendre accessible aux étudiants et aux débutants. Les développeurs confirmés pourront quant à eux rapidement exploiter les subtiles possibilités proposées par .NET, que sont par exemple la réflexion, la programmation orientée aspect ou le mécanisme d’attribut.