Recommend Me


Mercredi 22 février 2006

Introduction à C#

(Linux Magazine France n°67 - Décembre 2004 - sources)

Personne n’a pu rater l’annonce le 30 juin de cette belle année : “Mono est sortie en version 1.0″. Je ne souhaite pas rentrer dans la polémique et la controverse qui tourne autour de ce projet. Quoi que pourraient en penser les Gnous bronzés au soleil de Java (la vache, c’est fort ça ;)) Mono[1] (et plus particulièrement C#) mérite que l’on s’y attarde ; Au moins par curiosité au mieux pour se prouver que chez Microsoft on est capable du pire comme du meilleur.

0. De dotNet à Mono

l’idée de Mono (telle qu’elle est présenté par Miguel de Icaza) est on ne peut plus simple. Pour cela examinons les possibilités offertes par Java, .NET et Mono en terme d’inter-opérabilité (entre langages) et de portabilité.

Avec Java[2], Sun a offert aux développeurs la possibilité de créer des applications pouvant fonctionner sur n’importe quel plate-forme en utilisant un seul langage. Reposant sur une “machine virtuelle” les applications peuvent être déployées sur n’importe quel système (ayant une VM) sans recompilation.

.Net[3] de son côté prend la philosophie inverse. En fait, Microsoft souhaitait donner la possibilité à ses utilisateurs de travailler avec plusieurs langages (Basic, C++, C#, J#, Eiffel…) en utilisant un Framework commun. De fait un module créé avec un de ces langages est utilisable avec n’importe quel autre, et ce de manière totalement transparente. Malheureusement ceci est réservé à Windows.
En plaçant la CLI[4] (Common Language Infrastructure) et le langage C#[5] sous une norme ECMA Microsoft à ouvert la porte de .NET aux autres plates-formes. Mono reprend le meilleur des deux mondes (Java et .NET) en mettant à disposition des développeurs un Framework et une CLR (Common Language Runtime) disponible sur plusieurs plate-formes (UN*X, Windows, Marc) ; Bien entendu Mono est compatible avec .NET et sait utiliser des classes Java.

Bien entendu, il reste alors le combat Java vs C#. Je vous le laisse[6].

Dans la suite je vais vous présenter Mono au travers du langage C#. Plus tard nous verrons qu’un “assemblage” créé avec C# peut parfaitement être utilisé avec le MonoBasic et ce sur n’importe quelle plate-forme (Windows ou MacOSX). Nous verrons aussi qu’un assemblage Mono et parfaitement compatible avec dotNet (et inversement).

Pour la suite, il est bien entendu nécessaire d’installer Mono afin de pouvoir faire quelques tests. Pour cela je vous laisse apprécier la meilleur solution en fonction de la distribution que vous utilisez. Sinon il reste la possibilité de compiler à partir des sources disponibles sur le site de Mono[1]. Il est aussi bon de rappeler qu’installer la documentation n’est jamais une bétise, d’autant plus qu’elle est livré avec le monodoc-browser permettant une lecture via une interface GTK. Vous pouvez aussi installer MonoDevelop qui est un environnement de développement (embryonnaire). Il présente l’avantage de pouvoir embarquer la documentation. Malheureusement sont étant d’avancement ne le rend pas (encore) très conviviale et les amoureux préférerons garder leur VI ou Emacs…

1. Architecture de Mono

Avant de commencer à explorer C#, voyons comment s’articule les applications Mono. La CLI décrite par Microsoft normalise les couches entre une application et le matériel :

Dans Mono, la CLR est portée sur plusieurs plate-formes ce qui la rend comparable à la Machine Virtuelle de Java.

Le Mono Framework est compatible avec son cousin dotNet. Ceci fait qu’un développeur utilisant Visual Studio peut parfaitement récupérer les “assemblages” créés par un camarade pingouin. L’inverse n’est pas toujours vrai, et ce pour différentes raisons. Un développeur Windows utilisera naturellement les WinForms quand il voudra créer un composant graphique ou une IHM complète. Or même si Mono sait utiliser les WinForms (via Wine sous Linux) ses “retards” d’implémentation bloquent dans certains cas. De plus l’utilisation de Wine rend les choses dépendantes de ce projet qui n’est (malheureusement) pas encore totalement stable dans certain cas.

Une application Mono se comprend en terme d’assemblage. Un assemblage est en fait un ensemble de meta-données et de bytecode permettant la compilation et l’exécution par la CLR. Une DLL (et oui, faudra s’y faire) ou une exécutable est donc un assemblage.

2. Créer des classes en C#

C# est un langage objet qui, en tant que tel, nous oblige à une implémentation rigoureuse. Les développeurs Java seront les plus avantagés tant il est vrai que la syntaxe du langage est proche de celle de Sun. Les amoureux du développement objet “moins rigoureux” (
C++ ou PHP) devront oublier que finalement rien interdit de faire du procédural avec de l’objet (et inversement). En fait il suffira simplement à ces dernier de retenir qu’en C# il est impossible de créer des fonctions en dehors d’une définition de classe.

La création d’une classe se fait en utilisant le mot clé class suivi du nom et du contenu de la classe entre accolades {} :

class MaClasseCSharp {   ... }

Dans le corps de la classe nous trouverons (entre autre) des attributs et des méthodes.

Un attribut est un champ, dans la classe, de type donné. Nous verrons un peu plus loin les types de bases en C#. Cependant un attribut n’est (heureusement) pas forcement d’un type de base, il peut être d’un type quelconque lui même définit par une classe :

class MonType {   ... } class MaClasse {   ...   // Un attribut de type MonType   MonType oMonAttribut;   // Un attribut de type string (base)   string strAutreAttribut;   ... }

Les méthodes quand à elles sont définies en précisant d’abord le type de la valeur de retour suivi du nom de la méthode puis des paramètres d’entrée, entre parenthèses. Le corps de la méthode est placé ensuite entre accolades.

class MonType {   ... } class MaClasse {   ...   // Un attribut de type MonType   MonType oMonAttribut;   // Un attribut de type string (base)   string strAutreAttribut;   ...   // Une méthode prenant en entrée une valeur de type   // string et retournant une valeur de type MonType   MonType MaMethode( string strArg ) {     ...   }   ... }

Nous pouvons définir la visibilité d’un membre (attribut ou méthode) d’une classe, vis à vis du “reste du monde”, en plaçant devant une des options d’accessibilité suivante :

  • public : dans ce cas le membre est totalement ouvert.
  • protected : rend le membre accessible uniquement pour la classe (et ses sous-classes) dans laquelle il est définit.
  • internal : l’accessibilité est limité à l’assemblage courant.
  • protected internal : autorise l’accès aux classes de l’assemblage courant et aux sous-classes de la classe courante.
  • private : les accès aux membres sont limités à la classe courante.

Nous aurons largement l’occasion de revenir sur la notion d’accessibilité des membres d’une classe par la suite.

3. Types de bases en C#

Afin de pouvoir illustrer plus facilement par des exemples la suite de cette découverte de C#, je vous propose de faire un petit détour en vous présentant les types de base.

Nous l’avons vu plus haut, un type est définit dans une classe. Les types “de bases” ne dérogent pas à cette régle. C# utilisant pleinement le Framework Mono. Il est donc facilement compréhensible qu’à chaque type de données intégrés correspondra une classe du Framework. Plus clairement, un type quelconque nommé type et un simple alias vers classe (en Général System.Type) du Framework.

Le tableau suivant donne, pour chaque type de base, son alias C# :

Type Alias C# Description Valeur
System.SByte sbyte Entier 8 bit signé [-128, 128]
System.Byte byte Entier 8 bit non signé. [0, 255]
System.Int16 short Entier 16 bit signé. [-32768, 32767]
System.UInt16 ushort Entier 16 bit non signé. [0, 65535]
System.Int32 int Entier 32 bit signé. [-2147483648, 2147483647]
System.UInt32 uint Entier 32 bit non signé. [0, 4294967295]
System.Int64 long Entier 64 bit signé. [-9223372036854775808, 9223372036854775807]
System.UInt64 ulong Entier 64 bit non signé. [0, 18446744073709551615]
System.Char char Entier 16 bit non signé sous forme de caractère Unicode. [0, 65535]
System.Single float Type à virgule flottante en simple précision (32 bit) [1.5215 10-45, 3.4215 1038] 7 digits
System.Double double Type à virgule flottante en double précision (64 bit) [5.0215 10-324, 1.7215 10308] 15-16 digits
System.Boolean bool Valeur logique. true ou false
System.Decimal decimal Décimal sans arrondi (128 bit) [1.0215 10-28, 7.9215 1028] 28-29 digits
System.String string Chaine de caractères. Objet représentant une chaîne
System.Object object Instance d’un classe quelconque. Référence à une instance de classe quelconque.

Vous pouvez utiliser les deux écritures pour préciser un type, mais dans tous les cas on préférera utiliser l’alias.

class MaClass {   ...   // Deux définition pour un même type   System.String strMaPremiereChaine;   string strMaSecondeChaine;   ... }

Pour connaître toutes les manipulations possibles d’un type de donné, il suffit simplement de connaître l’interface la classe correspondante du Framework. Bien que nous verrons au cours de notre découverte, de façon plus ou moins approfondie, le contenu de certaines de ces classes, je vous conseil vivement de jeter un oeil à la documentation du Framework Mono.

4. Constructeurs et destructeurs

Nous avons déjà vu qu’une classe peut contenir des attributs et des méthodes. Mais il existe beaucoup d’autre membres possibles. Avant de les parcourir tous, examinons certaines méthodes particulières, en commençant par les constructeurs.

Le constructeur (est il besoin de le rappeler) est la méthode appelée lors de l’initialisation d’un objet. Il porte le même nom que la classe et ne retournent jamais de valeur. Nous le définirons donc toujours de la manière suivante :

class MaClass {   // ...   public MaClass( ... ) {     // ...   } }

Vous remarquerez que nous avons fixé le niveau d’accessibilité du constructeur à “public”. Ceci peut sembler logique, mais il ne coûte rien de le rappeler.

J’ai parlé, pour le moment, de constructeur au singulier, mais bien entendu il est possible de surcharger un constructeur et donc d’en définir autant que vous voulez afin d’offrir différentes méthodes (au sens de solutions) d’instanciations pour votre classe :

class MaClass {   // ...   public MaClass( ) {     // un constructeur ne prenant aucun paramètre     // ...   }     public MaClass( string strData ) {     // un constructeur prenant un chaine de caractères en paramètre     // ...   }   // ... }

Une telle classe peut donc être instanciée de deux façons :

MaClass maClass = new MaClass( );

ou

MaClass maClass = new MaClass( "avec un chaine en paramètre" );

Vous pouvez en plus définir un constructeur particulier de type static. Dans ce cas il sera systématiquement appelé lors de l’initialisation avant tout appel à un constructeur d’instance. Ce type de constructeur ne prend aucun paramètre d’entrée et ne retourne rien :

class MaClass {   // ...   static MaClass( ) {     // constructeur static qui sera appelé avant tout autre !     // ...     }           public MaClass( ) {     // un constructeur ne prenant aucun parametre     // ...   }   public MaClass( string strData ) {     // un constructeur prenant un chaine de caractères en paramètre     // ...   }   // ... }

Sachez enfin que rien interdit de créer des classes sans aucun constructeur. Dans ce cas, le compilateur C# créé automatiquement un constructeur par défaut ne prenant par de paramètre et ne faisant rien.

A l’opposé (si je puis me permettre la symétrie) nous avons le destructeur de classe. Il porte le même nom que la classe à la quel il appartient, précédé d’un tilde (~). Lui aussi ne prend aucun paramètre d’entrée et ne retourne rien :

class MaClass {   // ...   ~MaClass( ) {     // ...   }   // ... }

Aucune option d’accessibilité n’est donnée pour le destructeur dans notre exemple. Par défaut il est “protected” et vous pouvez le préciser si vous le souhaitez.

Le destructeur n’est pas appelé en C# quand une référence sort de la porté courante. En fait il est invoqué seulement quand le “travail de destruction” est du ressort de la classe courante donc lorsque l’objet est réclamé par le Garbage Collecteur. Il arrive qu’un destructeur de classe ne soit alors jamais appelé et, contrairement à ce qui peut se faire en C++, nous ne pouvons pas l’utiliser comme fonction de terminaison.

Prenons le petit programme de test suivant :

sample1.cs

     1 using System;       2       3 class MaClass {       4     static MaClass( ) {       5         Console.WriteLine( "Dans le constructeur static" );       6     }       7       8     public MaClass( ) {       9         Console.WriteLine( "Dans le constructeur" );      10     }      11      12     protected ~MaClass( ) {      13         Console.WriteLine( "Dans le destructeur" );      14     }      15 }      16      17 class MainClass {      18     public static void Main( ) {      19         MaClass maClass = new MaClass( );      20     }      21 }

Compilons le :

~$ mcs sample.cs Compilation succeeded ~$

L’exécution peut produire deux types de résultats :

~$ ./sample1.exe Dans le constructeur static Dans le constructeur ~$ ./sample1.exe Dans le constructeur static Dans le constructeur Dans le destructeur ~$

Nous reviendrons dans un prochain article sur les principes de destruction dans les classe.

5. Constantes, propriétés, opérateurs, événements et indexeurs

Nous sommes maintenant à même de créer des classes simples. Avant de vous présenter les membres de classe qu’il nous reste à aborder, faisons un petit compléments concernant les attributs. Par défaut un attribut est propre à une instance. Or il est possible de faire en sorte qu’il soit partagé entre toutes les instances d’une classe. Pour cela il suffit de le déclaré static :

static public int iValeur;

Faite bien attention à la syntaxe : ici le mot clé static est placé avant le niveau d’accessibilité. Si vous inversé le comportant est tout autre. Dans ce dernier cas cela signifie que l’attribut est déclaré “en lecture seule” et dans ce cas il ne peut plus être modifié après sa déclaration. Afin d’éviter tout ambiguïté, préférez utiliser le mot clé “readonly” :

public static int iSessionCount; public readonly int iSessionCount;

Les constantes peuvent être comparées à des attributs dont la valeur est affectée à la compilation et qui ne peut pas être changée. Nous les déclarons en plaçant le mot clé “const” entre son niveau d’accessibilité et son type :

public const int iAge = 30; private const long lTaille = 1.78;

Vous pouvez accéder à un attribut (ou une constante) public depuis n’importe où. Cela peut semblé avantageux, mais risqué dans certains cas. En effet aucun contrôle ne sera fait lors d’une éventuelle (re)affectation. Pour parer à cela, C# met à notre disposition les propriétés. Une propriété est en fait une fonction qui s’utilise comme un attribut donc par affectation/lecture. Pour se faire vous devez placé le code d’affectation entre accolades précédé du mot clé set, et celui de lecture précédé de get :

public int MaPropriete {   get {     int iRetour;     // ...     // Ici, le code a executer quand on fait un appel :     //    iValeur = instanceDeMaClasse.MaPropriete;     // ...     return( iRetour );   }   set {     // ...     // Ici, le code à executer quand on fait un appel :     //    instanceDeMaClasse.MaPropriete = iValeur;     // ...   } }

Dans la clause set, la valeur envoyé est contenu dans un paramètre prédéfinie nommée “value”. En supprimant respectivement la clause set ou get, vous pouvez aisément définir des propriétés simulant des attributs en lecture ou écriture seule.

Une classe peut aussi avoir des op&rateurs parmi ses membres. Il s’agit en fait de la possibilité offerte de surcharger des opérateurs unaires et binaires. Ceci se fait en créant des méthodes dont le nom correspond à l’opérateur à redéfinir, précédé du mot clé “operator” :

class MaClass {   // ...   public static bool operator ==(MaClass oGauche, MaClass oDroit) {     bool bResultat;     // ...     return( bResultat );   }   // ... }

Bien entendu rien ne vous oblige à avoir, en paramètre, des objets du même type que la classe bien que dans bien des cas cela logique.

Les opérateurs pouvant être redéfinis sont de deux types :

  • Unaire : + - ! ~ ++ — true false
  • Binaire : + - * / % & | ^ << >> == != > < >= <=

Il reste deux types de membre que nous n’avons pas abordé : les évènements et les indexeurs. Nous aborderons plus en détails les premiers lors d’un prochain article. Les indexeurs nous permettre de travailler sur des objets comme s’il s’agissait de tableaux donc en travaillant par indice.

La mise en place d’un indexeur se fait en créant une méthode “this” suivi d’une déclaration d’indice entre crochets. Dans le corps nous retrouverons une écriture semblable à celle que nous avons vu pour les propriétés :

class MaClass {   // ...   public MonType this[AutreTyoe indexe] {     get { ... }     set { ... }   }   // ... }

Il est tout à fait possible de définir des indexeurs à plusieurs dimentions, pour cela il suffit simplement d’ajouter des déclarations d’indexes :

class MaClass {   // ...   public MonType this[AutreTyoe indexeOne, EncoreUnType indexeTwo] {     get { ... }      set { ... }   }   // ... }

6. Héritage

L’héritage est une composante de base en programmation objet qui permet de définir une nouvelle classe par combinaison ou spécialisation d’une classe déjà existante.

En C# on fait hériter une classe d’une autre en utilisant la syntaxe suivante :

class MaClassDeBase {   // ... } class MaClass: MaClassDeBase {   // ... }

Dans cet exemple, MaClass hérite de MaClassDeBase. Il apparait alors qu’un objet de type MaClass est aussi de type MaClassDeBase. L’inverse n’est cependant pas vrai. Nous pouvons alors créer un objet MaClass de façon classique :

MaClass c = new MaClass( ...);

Un handle MaClassDeBase pourra aussi pointer sur un objet de type MaClass :

MaClassDeBase b = new MaClass( ... );

Par contre l’inverse est impossible :

LaClass e = new MaClassDeBase( ... );

Ceci provoquera un erreur lors de la compilation :
error CS0029: Cannot convert implicitly from `MaClassDeBase’ to `MaClass’

Sachez aussi que lors de l’appel du constructeur d’une classe fille, le constructeur de la classe mère est appelé juste avant. Et ainsi de suite en remontant tout l’arbre d’héritage.

Il arrive parfois qu’une classes de base n’ait pas besoin d’être (ou ne doive pas être) instanciée directement. En C# il suffit simplement pour cela de la déclarer abstraite :

abstract class MaClassDeBase {   // ... }

Par défaut, dans une classe (de base ou non) toutes les méthodes doivent être implémentées. Il est possible d’éviter cette contrainte en déclarant une méthode comme étant elle même abstraite :

abstract class MaClassDeBase {   // cette méthode n'est pas définie ici mais doit l'être dans la classe fille   abstract public void MyAbstractMethod( );   // ... }

Ceci est particulièrement utile quand on souhaite forcer la classe fille à implémenter une méthode pour laquelle la classe de base ne peut pas donner de définition significative. A l’inverse on peut vouloir définir une méthode dans la classe de base pour laquelle on estime avoir une implémentation significative et donc ne pas obliger de la recoder dans la classe qui hérite. Il suffit de déclarer la méthode vitual :

abstract class MaClassDeBase {   public virtual void MyVirtualMethod( ) {     // ...   }   //... }

Que la méthode de base soit abstraite ou virtuelle, dans tous les cas dans la sous-classe nous devons indiquer notre volonté de surcharger la méthode en utilisant le mot clé override :

abstract class MaClassDeBase {   public abstract void MyAbstractMethod( );   public virtual void MyVirtualMethod( ) {     // ...   }   // ... } class MaClass: MaClassDeBase {   public override void MyAbstractMethod( ) {     // ...   }   public override void MyVirtualMethod( ) {     // ...   }   // ... }

L’accès à l’objet courant se fait en utilisant le mot clé this. Pour accéder à la classe de base, dans un cadre d’héritage, nous utiliserons le mot clé base :

class MaClass: MaClassDeBase {   // ...   public void MaMethode( ) {     base.MaMethodeDeBase( );     // ...   }   // ... }

A l’inverse des classes abstraites, nous pouvons vouloir interdire l’utilisation d’une classe en tant que classe de base pour un héritage. Pour cela il suffit de la déclarer sealed :

sealed class MaClassAMoah {   // ... }

7. Les interfaces

Une interface peut être vue comme une classe purement abstraite qui ne comprend que des méthodes abstraites. La création se fait en utilisant le mot clé interface :

interface IPlayer {   void CreatePlayer( string strNom );   int getScore( ); }

Il est impossible d’instancier directement une interface et elles ne peuvent donc être qu’héritées. Une classe doit obligatoirement implémenter toutes les classes définies dans l’interface dont elle dérive :

class User: IPlayer {   public void CreatePlayer( string strNom ) {     // ...   }   public int getScore( ) {     // ...   }   // ... }

Il est important de savoir que les membres d’une interface sont toujours public, donc une classe dérivée doit forcément implémenter les méthodes décrites dans l’interface comme étant de même niveau d’accessibilité.

8. Les structures

Les structures sont comparables à des classes et nous allons voir que la frontière entre les deux est plus mince qu’il n’y parait. En effet, en C/C++, nous avons l’habitude de manipuler des structures qui ne comptent parmi leurs membres que des champs de type variables. Ceci permet ainsi de former un nouveau type. Nous pouvons faire exactement la même chose en C# :

struct User {   public int iAge;   public string strName;   // ... }

L’utilisation d’une structure peut se faire de deux façons, soit sur le même modèle que pour une classe en utilisant un new :

class MaClass {   // ...   public void UneMethode( ) {     User sMe = new User( );     // ...     sMe.iAge = 30;     sMy.strName = "greg";     // ...   }   // ... }

Soit directement :

class MaClass {   // ...   public void UneMethode( ) {     User sMe;     // ...     sMe.iAge = 30;     sMy.strName = "greg";     // ...   }   // ... }

Nous verrons plus bas dans quel cas utiliser le new… Faite cependant attention car toute tentative de lecture d’un membre d’une structure non alloué se soldera par de sérieuses remontrances de la part du compilateur.

Vous remarquerez que nous avons donné une option d’accessibilité aux champs de la structure. En effet, par défaut, les champs d’une structure sont privés. C# autorise en plus d’ajouter des fonctions comme membre d’une structure. Ceci explique l’intéret qu’il peut y avoir à définir des champs non publiques.

struct User {   public int iAge;   public string strName;   public string DayOrBirth( DateTime tToday ) {     // ..   } }

De fait il est aussi possible d’ajouter un (des) constructeur(s), mais dans ce cas il devra obligatoirement prendre au moins un paramètre ce qui élimine toute possibilité de placer un constructeur par défaut.

struct User {   public int iAge;   public string strName;   public User( Human notAnimal ) {     // ...   }   public string DayOrBirth( DateTime tToday ) {     // ...   } }

Dans ce cas, vous l’aurez compris, utiliser un “new” est obligatoire si vous souhaiter allouer une structure en passant par un constructeur.

La ressemblance entre structure et classe s’arrête là. En effet il est impossible de jouer avec les principes d’héritage entre structures ou entre une structure et une classe.

Nous sommes alors en droit de nous poser la question suivante : mais quel est donc l’intéret d’utiliser des structures alors ? Bonne question mon cher Yves ;) Nous sommes d’accord pour dire qu’en fait une structure est comparable à une classe soumise à des contraintes. Oui, mais les structures se comportent (et donc s’utilisent) comme des types pré définis. De fait les structures sont allouées sur la pile (stack) alors que les classes sont allouées sur le tas (heap) !

Les objets alloués sur le tas sont plus longs à allouer et utilisent plus de place en mémoire car ils sont toujours manipulés par référence. Ce défaut se transforme en avantage car grace à cela ils peuvent facilement être partagés avec d’autres classes. De plus ils vivent au delà de la méthode courante. Il faut cependant être prudant car, mal gérés, les objets peuvent engendrer un gaspillage en mémoire en cas d’erreur voir un plantage pur et simple de l’application.

A l’opposé, les objets alloués sur la pile sont plus performant et moins consommateur car gérés par valeur. Cependant étant donné qu’ils ont une durée de vie limitée à celle de la méthode dans la quel ils sont déclarés, ils sont automatiquement détruit avec cette dernière. Ceci les rend difficilement “partageables” avec d’autres méthodes.

Nous pouvons voir cela sur un petit exemple. Imaginons que nous souhaitions créer un User pour lequel nous voulons renseigner deux champs : son nom et son age. Nous pouvons alors opter pour une solution sous forme de structure (TUser) ou d’objet (OUser) :

struct TUser {     public int Age;     public string Name; } class OUser {     public int Age;     public string Name; }

Voyons ce qui se passe si dans une fonction nous allouons une classe et une structure et que nous demandons à une fonction tierce de les peupler :

private static void SetStructure( TUser user ) {     user.Age = 30;     user.Name = "greg"; } private static void SetObject( OUser user ) {     user.Age = 6;     user.Name = "arthur"; } public static void Reference( ) {     TUser tUser = new TUser( );     OUser oUser = new OUser( );     SetStructure( tUser );     SetObject( oUser );     Console.WriteLine( "tUser.Age = {0}, tUser.Name = {1}", tUser.Age, tUser.Name );     Console.WriteLine( "oUser.Age = {0}, oUser.Name = {1}", oUser.Age, oUser.Name ); }

Alors si vous appelez la fonction Reference( ), vous verrez le résultat suivant :

tUser.Age = 0, tUser.Name = oUser.Age = 6, oUser.Name = arthur

En effet, nous l’avons dis, les structures sont gérées comme des types pré définis, donc par valeur. Si vous voulez obtenir le résultat attendu, il faudra modifier SetStructure de la façon suivante :

private static void SetStructure( ref TUser user ) {     user.Age = 30;     user.Name = "greg"; }

Et faire l’appel comme ceci :

SetStructure( ref tUser );

En fait tout ceci prend un réel sens quand on souhaite créer des tableaux. Si nous choisissons de mettre comme éléments de tableau des TUser, alors, si nous connaissons beaucoup de monde, il y a fort à parier que l’on gagnera en occupation mémoire face à une utilisation de OUser. Voici un petit programme d’exemple mettant en opposition les deux solutions :

strvsobj.cs :

     1 using System;       2 using System.Collections;       3       4 struct TUser {       5     public int Age;       6     public string Name;       7 }       8       9 class OUser {      10     public int Age;      11     public string Name;      12 }      13      14 class Test {      15     private static ArrayList lTUser;      16     private static ArrayList lOUser;      17      18     public static void PopulaiteTUser( ) {      19         TUser tUser;      20         tUser.Age = 30;      21         tUser.Name = "greg";      22      23         lTUser.Add( tUser );      24     }      25      26     public static void PopulaiteOUser( ) {      27         OUser oUser = new OUser( );      28         oUser.Age = 6;      29         oUser.Name = "arthur";      30      31         lOUser.Add( oUser );      32     }      33      34     public static void PrintTUser( ) {      35         IEnumerator myEnumerator = lTUser.GetEnumerator();      36         while ( myEnumerator.MoveNext() ) {      37           Console.WriteLine( "{0} a {1} ans", ((TUser)(myEnumerator.Current)).Name, ((TUser)myEnumerator.Current).Age );      38         }      39     }      40      41     public static void PrintOUser( ) {      42         IEnumerator myEnumerator = lOUser.GetEnumerator();      43         while ( myEnumerator.MoveNext() ) {      44           Console.WriteLine( "{0} a {1} ans", ((OUser)(myEnumerator.Current)).Name, ((OUser)myEnumerator.Current).Age );      45         }      46     }      47      48     public static void Main( ) {      49         lTUser = new ArrayList( );      50         lOUser = new ArrayList( );      51      52         PopulaiteTUser( );      53         PopulaiteOUser( );      54      55         PrintTUser( );      56         PrintOUser( );      57     }      58 } ~$ mcs strvsobj.cs Compilation succeeded ~$ ./strvsobj.exe greg a 30 ans arthur a 6 ans ~$

9. Enumérations

Il existe une dernière méthode permettant de créer un type de donnée : les énumérations. Nous les créons en utilisant le mot clé enum :

enum Mois {   Janvier, Fevrier, Mars, Avril, Mai, Juin,   Juillet, Aout, Septembre, Octobre, Novembre, Decembre }

Par défaut, chaque énumérateur se voit affecter une valeur scalaire (par défaut de type integer) en commençant à zéro. Il est possible de spécifier la valeur initiale :

enum Mois {   Janvier = 1, Fevrier, Mars, Avril, Mai, Juin,   Juillet, Aout, Septembre, Octobre, Novembre, Decembre }

Il est aussi envisageable de donner une valeur spécifique à chaque énumérateur. Il faut cependant faire attention de respecter la régle qui dit que chacun énumérateur doit être unique :

enum Mois {   Janvier = 1, Fevrier = 2, Mars = 4, Avril = 8, Mai = 16, Juin = 32,   Juillet = 64, Aout = 128, Septembre = 256, Octobre = 1024, Novembre = 2048, Decembre = 4096 }

Le type scalaire d’une énumération peut être replacé par n’importe quel autre :

enum Mois : byte {   Janvier = 1, Fevrier = 2, Mars = 4, Avril = 8, Mai = 16, Juin = 32,   Juillet = 64, Aout = 128, Septembre = 256, Octobre = 1024, Novembre = 2048, Decembre = 4096 }

10. Espace de nom

Lors de la création d’un projet, l’un des problèmes classiques pour les développeurs consiste à éviter les collisions entre les types ; Qu’ils les définissent ou qu’ils utilisent ceux provenant de sources externes. C# facilite ce travail en proposant la création d’espace de nom. La syntaxe utilisé est la suivante :

namespace MonEspace {   class MaClass {     // ...   } }

En général les espaces de nom servent à simuler une hiérarchie. Pour cela on peut naturellement déclarer une telle hiérarchie de la façon suivante :

namespace NomEspace {   namespace AutreEspace {     class MaClass {       // ...     }   } }

Afin de nous faciliter le travail et aussi de faciliter la lisibilité du code il est possible de simplifier l’écriture avec la syntaxe suivante :

namespace MonEspace.AutreEspace {   class MaClass {     // ...   } }

Lors d’un développement vous pouvez être amener à vouloir créer un type dans un espace de nom déjà existant (provenant d’une source externe par exemple) ou scinder le contenu d’un espace dans plusieurs fichiers. A ce niveau vous pouvez faire ce que vous voulez, charge au compilateur de rassembler le tout lors de la génération de l’assemblage.

namespace MonEspace {   class MaClass {     // ...   } } // ... namespace MonEspace {   class MonAutreClass {     // ...   } }

Si vous souhaitez utiliser un type appartenant à un espace de nom, vous avez deux possibilités :

  • soit utiliser le nom complet :
    MonEspace.MaClass o = new MonEspace.MaClass( ... );
  • soit ajouter une référence à l’espace de nom auquel appartient le type dans la liste des espaces de noms courants :
    using MonEspace; // ... MaClass o = new MaClass( ... );

Si vous êtes amenés à utiliser des classes ayant le même nom (mais appartenant à des espaces de noms différents), alors seul la première solution est envisageable, y compris si vous avez référencé les différents espaces de noms :

namespace EspaceOne {   class MaClass {     // ...   } } namespace EspaceTwo {   class MaClass {     // ...   } } // ... using EspaceOne; using EspaceTwo; // ... EspaceOne.MaClass one = new EspaceOne.MaClass( ); EspaceTwo.MaClass one = new EspaceTwo.MaClass( ); // ...

11. Conversion de types et casting

Il n’est pas rare de devoir ou vouloir convertir un objet en un autre. Nous l’avons brièvement évoqué lorsque nous avons parlé d’héritage. Nous allons y revenir. Pour le moment voyons comment réaliser une conversion.

Il y a deux solutions pour ce genre de problèmes. Soit la conversion se fait sans aucune intervention, elle est alors dite implicite, soit elle doit être forcée et est alors dite explicite.

Une conversion implicite s’envisage facilement. En fait cela arrive quand nous souhaitons transformer un objet en un autre (de type différent) sans qu’il y ait de perte de données. C’est le cas par example quand nous passons d’un type entier à un type long.

int i = 23; long l = i;

En effet, un entier est codé sur 32 bits et un long sur 64. Sur le même principe nous pouvons “transformer” un long en double :

double d = l;

Nous pouvons ainsi écrire un arbre de conversion :

Ce type de conversion est dite asymétrique. En effet, si nous souhaitons “remonter” l’arbre, nous comprenons facilement qu’il y aurra de la perte. Dans certains cas, ceci peut avoir de grave conséquences sur le résultat du déroulement d’un programme. Ainsi si nous essayons de faire un conversion de double vers int, le compilateur n’hésitera pas à nous rappeler à l’ordre :

// ... double d = 3.999; int i = d; // ... error CS0029: Cannot convert implicitly from `double' to `int'

Une telle transformation est donc rendu possible uniquement si elle est explicitement demandé. Pour cela nous devons utiliser un opérateur de cast :

// ... double d = 3.999; int i = (int)d; // ...

Vous remarquerez que dans l’arbre de conversion donné plus haut, nous n’avons placé que des valeurs scalaires de type numériques ou caractère. Pour les conversion de type chaîne, il n’est pas possible d’utiliser le cast. Pour cela nous devrons utiliser la classe Convert :

string s = Convert.ToString( 3.999 ); int i = Convert.ToInt32( "17" );

Pour plus de détails, je vous renvoi à la documentation du Framework.

Revenons au cas de l’héritage, nous avons vu qu’il est possible de faire une conversion implicite d’un classe fille vers la classe parente :

MaClass c = new MaClass( ... ); // Si MaClass hérite de MaClassDeBase, nous pouvons écrire : MaClassDeBase b = c;

Or il faut savoir qu’en C#, toute classe hérite obligatoirement de la classe System.Object. Nous pouvons donc parfaitement écrire :

object o = c;

(Remarquez que j’utilise ici l’alias object de System.Object pour la déclaration.) Ceux qui suivent seraient alors en droit de dire que dans un tel cas il y a une perte de données. Ceci n’est pas tout à fait exacte. En fait les données propres à la classe fille ne sont pas perdu, elles sont simplement inaccessibles. Prenons le cas d’une conversion double vers int vers double :

double d1 = 3.999; int i = (int)d1; double d2 = i;

A l’issus de cette exécution d2 aurra comme valeur 3. Il y a donc véritablement une perte. Par contre prenons les classes suivantes :

class MaClassDeBase {   public int Champ1; } class MaClass: MaClassDeBase {   public int Champ2; }

Si nous créons une instance de la classe MaClass, que, par conversion explicite l’affectons a un objet de type MaClassDeBase et que nous faisons une conversion explicite vers un nouvel objet de type MaClass :

MaClass c1 = new MaClass( ); c1.Champ1 = 1; c1.Champ2 = 2; MaClassDeBase c2 = c1;

A ce niveau si nous demandons à accéser à c2.Champ2, dés la compilation nous nous ferons remettre en place :

error CS0117: `MaClassDeBase’ does not contain a definition for `Champ2′

MaClass c3 = (MaClass)c2;

Ici, dans c3, nous retrouvons les valeurs de Champ1 et Champ2 telle qu’elles ont été définies dans c1.

Lorsque nous créons des classes, nous pouvons définir nos propres conversions. De cette manière nous pourrons définir de quelle manière un objet de type donné doit être converti en un autre type. Il convient de faire attention lorque nous souhaitons faire ce travail. En aucun cas nous ne pouvons définir de conversion d’un type en ce même type, en une sous-classe ou une superclasse de ce type.

L’écriture d’une conversion se fait sur le modèle suivant :

public static mode operator typecible(typesource arg) {   // code de la conversion }

ou “mode” précise si la conversion est explicit ou implicit, “typecible” est le type de la cible et “typesource” le type de la source. Faite attention, un fonction de conversion ne peut pas prendre plus d’un seul paramètre.

Voyons cela sur un example :

class MaClass {   public double Champ;   public static explicite operator double( MaClass c ) {     return( c.Champ );   } }

Nous pouvons alors faire de la conversion explicite de MaClass vers le type double :

MaClass c = new MaClass( ); c.Champ = 3.1415; double d = (double)MaClass; // ici d vaut alors 3.1415

12. Un exemple…

Histoire de ne pas vous laisser démuni face à tant de théorie, je vous propose un petit exemple pratique que nous réutiliserons tout au long des articles suivants. L’idée est de créer un calculatrice utilisant la notation Polonais Inversé dite RPL[7].

Dans ce premier exemple nous allons nous contenter d’être capable de résoudre les quatres opérations de base (addition, soustraction, multiplication et division). Au fur et à mesure, nous ferons évoluer notre création en lui ajoutant de nouvelles possibilités et une interface utilisateur (GTK, WinForm ou Web).

rpl.cs:

    1  using System;      2  using System.Collections;      3      4  namespace RPL {      5    class Stack {      6      private ArrayList aStack;      7      private int iLength;      8      9      public Stack( ) {     10  #if DEBUG     11        Console.WriteLine( "Initialize stack." );     12  #endif     13        this.aStack = new ArrayList( );     14        this.iLength = 0;     15      }     16     17      public void Push( object oData ) {     18        this.iLength = aStack.Add( oData );     19  #if DEBUG     20        Console.WriteLine( "Stack size = " + this.iLength );     21  #endif     22      }     23     24      public object Pop( ) {     25        object oOut = null;     26        if( this.iLength >= 0 ) {     27          oOut = this.aStack[iLength];     28          this.aStack.RemoveAt( iLength );     29          this.iLength--;     30        }     31        return( oOut );     32      }     33     34      public void Print( ) {     35        int iCpt = 0;     36        IEnumerator myEnumerator = this.aStack.GetEnumerator();     37        Console.WriteLine( "Stack:{0,14}", this.iLength + 1 );     38        Console.WriteLine( "--------------------" );     39        while ( myEnumerator.MoveNext() )     40          Console.WriteLine( "{0,2}: {1,16}", iCpt++, myEnumerator.Current );     41          Console.WriteLine();     42        }     43    }     44     45    class Calculator {     46      private Stack objStack;     47      private object objOLeft;     48      private object objORight;     49     50      public Calculator( ) {     51  #if DEBUG     52        Console.WriteLine( "Initialize calculator." );     53  #endif     54        objStack = new Stack( );     55      }     56     57      public bool Add( string strData ) {     58        bool RCod = false;     59     60        try {     61          double d = Convert.ToDouble( strData );     62  #if DEBUG     63          Console.WriteLine( "Add : " + d );     64  #endif     65          RCod = AddNumber( d );     66        } catch(FormatException e) {     67  #if DEBUG     68          Console.WriteLine( "Add : " + strData );     69  #endif     70          RCod = AddOperator( strData );     71        }     72     73        return( RCod );     74      }     75     76      public bool AddNumber( double dblNumber ) {     77        objStack.Push( dblNumber );     78        return( true );     79      }     80     81      public bool AddOperator( string strOperator ) {     82        bool RCod = GetFromStack( );     83     84        if( RCod = true ) {     85          switch( strOperator ) {     86            case "+":     87              objStack.Push( (double)objOLeft + (double)objORight );     88              break;     89     90            case "-":     91              objStack.Push( (double)objOLeft - (double)objORight );     92              break;     93     94            case "*":     95              objStack.Push( (double)objOLeft * (double)objORight );     96              break;     97     98            case "/":     99              objStack.Push( (double)objOLeft / (double)objORight );    100              break;    101    102            default:    103              RCod = false;    104              break;    105          }    106        }    107    108        return( RCod );    109      }    110    111      public void PrintStack( ) {    112        objStack.Print( );    113      }    114    115      private bool GetFromStack( ) {    116        bool RCodR, RCodL;    117    118        objOLeft = objStack.Pop( );    119        RCodL = (objOLeft is System.Double)?true:false;    120    121        objORight = objStack.Pop( );    122        RCodR = (objORight is System.Double)?true:false;    123    124        return( RCodR && RCodL );    125      }    126    }    127    128    class TestCalculator {    129      public static void Main( string[] args ) {    130        Calculator objCalc = new Calculator( );    131    132        foreach( string strData in args ) {    133          if( objCalc.Add( strData ) == false ) {    134            Console.WriteLine( "Can't perform " + strData + " !" );    135            return;    136          }    137        }    138    139        objCalc.PrintStack( );    140      }    141    }    142  } uranium:~$ mcs rpl.cs Compilation succeeded uranium:~$ ./rpl.exe 3 2 + 4 / 2 '*' Stack:             1 --------------------  0:              1.6 uranium:~$ mcs -d:DEBUG rpl.cs Compilation succeeded uranium:~$ ./rpl.exe 3 2 + 4 / 2 '*' Initialize calculator. Initialize stack. Add : 3 Stack size = 0 Add : 2 Stack size = 1 Add : + Stack size = 0 Add : 4 Stack size = 1 Add : / Stack size = 0 Add : 2 Stack size = 1 Add : * Stack size = 0 Stack:             1 --------------------  0:              1.6

Conclusion

Après ce tour d’horizon, vous pouvez déjà utiliser C# pour vos propres créations. La prochaine fois nous aborderons les principes des délégués et nous reviendrons sur la destruction de classes en détaillant les méthodes Finalize, Dispose et Close. Au mois prochain donc !


[1] http://www.mono-project.com
[2] http://java.sun.com
[3] http://www.microsoft.com/net/
[4] http://www.ecma-international.org/publications/standards/Ecma-335.htm
[5] http://www.ecma-international.org/publications/standards/Ecma-334.htm
[6] http://www.dotnetguru.org/articles/CSharpVsJava.htm
[7] http://encyclopedia.thefreedictionary.com/reverse%20Polish%20notation

• • •

Pas de commentaire »

Pas encore de commentaire.

RSS des commentairesTrackBack URI

Laisser un commentaire

You must be logged in to post a comment.

Powered by: WordPress • Template adapted from the Simple Green' Wench theme - RSS