Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Academia.eduAcademia.edu

C sharp

Le Framework .NET est un environnement d'exécution (CLR Common Language Runtime) ainsi qu'une bibliothèque de classes (plus de 2000 classes). L'environnement d'exécution (CLR) de .NET est une machine virtuelle comparable à celle de Java.

Introduction

Le langage C# (C Sharp) est un langage objet créé spécialement pour le framework Microsoft .NET. L'équipe qui a créé ce langage a été dirigée par Anders Hejlsberg, un informaticien danois qui avait également été à l'origine de la conception du langage Delphi pour la société Borland (évolution objet du langage Pascal).

Type structuré (struct) avec des fonctions membres

Du fait de l'orientation objet du langage C#, on peut avoir des fonctions membres dans les types structurés, et pas uniquement des données. Il suffit de définir des fonctions au sein des blocs décrivant le type structuré. Dès lors, sur une variable, on peut appeler des fonctions rattachées à cette variable. La notation est homogène avec l'accès aux champs de la structure. On remarquera d'ailleurs que les fonctions utilisées sont nécessairement membres d'un type : soit struct soit class. De plus, on peut avoir plusieurs fonctions membres ayant le même nom, mais des signatures différentes. C'est ce qu'on appelle la surcharge. Console.WriteLine("p2.X={0}", p2.GetX()); Console.WriteLine("p1" + p1.ToString()); Console.WriteLine("p2" + p2.ToString()); } } }

La programmation orientée objet en C#

Le paradigme objet en bref (vocabulaire)

La Programmation Orientée Objet (POO) est une façon de programmer et de décomposer des applications en informatique. Un objet est une variable structurée contenant des champs mais aussi des fonctions membres. Ceci est possible en C# mais ne l'est pas en langage C.

Le mot clé class ( class vs struct)

Le C# (comme d'autres langages objet) utilise le mot clé class pour pouvoir définir d'autres types structurés avec des méthodes (struct ne convient pas à tous les objets). Les types class sont plus riches que les types struct (on peut dériver des classes) et conduisent également à un fonctionnement différent de la gestion de la mémoire. On peut donc aussi définir un type class pour représenter des points du plan.

Types valeurs / Types référence

En C#, selon le type d'une variable / d'un objet (rappelons qu'un objet n'est rien d'autre qu'une variable structurée), l'allocation de mémoire est différent. L'allocation d'une variable n'est donc pas le simple fait du programmeur (comme en C où l'on choisit ce que l'on met dans la pile (par défaut) et ce que l'on met dans le tas (allocation dyamique par des fonctions spécifiques)).

En C#, selon le type d'une variable, celle-ci est réservée soit dans la pile soit dans le tas (mémoire allouée dynamiquement). On distingue deux catégories de types : les types "valeur" et les types "référence".

Les types "valeur" → alloués dans la pile Les types "référence" → alloués dans le tas (allocation dynamique) Quels sont les types "valeur" ? Tous les types numériques primitifs du C# (byte, char, long …), les énumérations et les variables générées à partir d'un type struct. Le type structuré PointS est un type valeur. Quand on crée une variable de type PointS, elle est réservée dans la pile. La mémoire des variables de type valeur est libérée automatiquement à la fin du bloc de leur déclaration (comme les variables locales en C) Quels sont les types "référence" ? Les types créés avec le mot clé class (ainsi que le type string).

Dans ce cas, la mémoire est allouée dynamiquement dans le tas quand on crée un objet (avec le mot clé new). Quand on instancie une classe (ie qu'on crée un objet), on réserve de la mémoire dans le tas. On récupère une référence (on peut voir ca comme un pointeur) sur l'emplacement réservé. Pour les types références, ce sont des références qui permettent de désigner les objets.

Quand la mémoire est-elle désallouée ? : pour les variables de type valeur, à la fin du bloc de déclaration. Pour les objets de type référence, quand il n'y a plus aucune référence sur un objet donné (il n'y a alors plus aucun moyen d'y accéder). Dans ce cas, le Garbage Collector (un programme qui s'exécute périodiquement pour libérer de la mémoire) récupère la mémoire de l'objet. Ca veut dire que la zone mémoire peut de nouveau être allouée à un autre objet nécessaitant de la mémoire dans le tas. L'intérêt du C# par rapport au C/C++, est qu'on ne peut donc pas oublier de désallouer de la mémoire (fuites mémoires). La désallocation n'est pas de notre responsabilité.

Distinctions Types valeur/Types référence concernant l'affectation

Cette distinction entre types "valeur" et types "référence" est essentielle en .NET. C'est un as^pect du système qu'il faut vraiment bien comprendr. De plus, l'affectation n'a pas le même sens pour les variables de type valeur et celles de type référence.

Affectation pour les types "valeur" Une variable de type valeur contient des données. int i=18; // i est une variable allouée dans la pile contenant l'entier signé 18 codé sur 32 bits Remarque : même dans la situation ci-dessous, la variable est dans la pile. ATTENTION! Pour les variables de type référence, l'affectation réalise seulement une copie des références (comparable à une copie de pointeurs en C++). Dans l'exemple ci-dessous, quand on affecte p3 à p4, puisque PointC est un type référence, les variables p3 et p4 sont des références. Par conséquent, après l'affectation p4=p3, p4 fera désormais référence au même objet que p3. Il y aura alors deux références au même objet (celui d'état (2,3)). L'objet dont l'état est (6,1) n'aura donc plus de référence qui le désigne. Il n'est plus référencé et le Garbage Collector peut donc désallouer sa mémoire. Mais cette désallocation n'est pas instantanée.

Distinction type valeur/type référence concernant l'égalité (==)

L'opérateur == pour les types valeur

Par défaut (puisque l'opérateur == peut être redéfini), le test d'égalité avec l'opérateur == sur des types valeur teste l'égalité de valeur des champs. Il y a comparaison de la valeur de tous les champs. L'opérateur renvoie la valeur true seulement si tous les champs ont les mêmes valeurs deux à deux

L'opérateur == pour les types référence

Par défaut, le test d'égalité avec l'opérateur == teste normalement si deux références désignent le même objet. Mais puisque l'opérateur == peut être reprogrammé, il peut très bien réalisé un traitement différent. Par exemple, pour les chaînes de type string, qui est pourtant un type référence, le test avec l'opérateur == indique l'égalité des chaînes (même longueur et mêmes caractères).

L'écriture de classes en C# (types référence)

Namespaces

Le mot clé namespace permet de définir un espace de nommage. Ceci permet de structurer le nommage des types (on peut rattacher un type à un espace de noms) et de lever d'éventuels conflits de noms. On peut avoir un même nom de type dans différents namespaces. Ci-dessous, on définit une classe A dans différents namespaces. Il s'agit donc de classes différentes.

Accessibilité

Les membres définis au sein des types peuvent avoir un niveau d'accessibilité parmi public : membre (donnée ou fonction) accessible par tous private : membre accessible seulement par une méthode d'un objet de la classe protected : membre accessible par une méthode d'un objet de la classe ou par une méthode d'un objet d'une classe dérivée (voir plus tard la dérivation) C'est grâce à ces modificateurs d'accès que l'on assure l'encapsulation des données. Tous les membres (attributs, méthodes ...) doivent être déclarés avec un niveau d'accessibilité. Par défaut, un membre est privé.

Il y a aussi des modificateurs d'accès spécifiques (non expliqué dans ce support) internal : accès autorisé pour l'assemblage courant protected internal : assemblage courant + sous classes Ci-dessous un exemple complet avec la définition de la classe MaClasse et son utilisation dans le programme. Notons qu'il est possible aussi de donner une initialisation des attributs pour les types class. Ce n'est pas autorisé pour les types struct.

Objet courant (référence this)

Dans une méthode, on appelle "Objet Courant", l'objet sur lequel a été appelée la méthode.

Dans l'exemple ci-dessous, quand on invoque obj1.Compare(obj2), dans la méthode Compare, l'objet courant est obj1. Dans une méthode, on peut désigner l'objet courant par la référence this.

C#/.NET

Notons que par défaut dans une méthode, quand on désigne un attribut (sans préfixer par une référence d'objet), il s'agit d'un attribut de l'objet courant. Sauf si une variable locale porte le même nom qu'un attribut, ce qui est déconseillé dans la mesure où c'est source d'erreur.

Remarque : il y a aussi pour les types numériques une méthode ToString(string format, IFormatProvider provider) qui permet de préciser le formattage (cf section sur la classe string)

Constructeurs (initialisation des objets)

Un constructeur est une méthode qui initialise l'état d'un objet (ne retourne rien) juste après son instanciation. Il a pour rôle d'éviter qu'un objet ait un état initial indéfini. Comme pour toutes les méthodes, il peut y avoir surcharge des constructeurs (c'est-à-dire plusieurs constructeurs avec des signatures différentes). De même qu'en C++, un constructeur a comme nom le nom de la classe.

Un constructeur peut recevoir un ou plusieurs arguments pour initialiser l'objet. Ces arguments sont fournis au moment de l'instanciation avec le mot clé new.

Remarque : si l'on ne met aucun constructeur dans une classe, il y en a alors un sans paramètre fourni par le compilateur (ce mécanisme est mis en place par défaut). Ce constructeur fourni par défaut ne fait aucun traitement mais permet d'instancier la classe. Dès lors qu'au moins un constructeur est spécifié, le constructeur sans paramètre par défaut n'est plus mis en place. On peut utiliser un autre constructeur dans un constructeur. Celà permet de bénéficier d'un traitement d'initialisation déja fourni par un autre constructeur. La notation utilise également le mot clé this :

Membres statiques (données ou méthodes)

Une donnée membre statique (appelée aussi variable de classe) n'existe qu'en un seul exemplaire quel que soit le nombre d'objets créés (même si aucun objet n'existe).

Une fonction membre statique (appelée aussi méthode de classe) n'accède pas aux attributs des objets. En revanche, une fonction membre statique peut accéder aux données membres statiques. Une telle fonction peut donc être invoquée même si aucune instance n'existe. Ce mécanisme remplace les fonctions hors classes du C++. Par exemple, les fonctions mathématiques sont des fonctions statiques de la classe System.Math.

Console.WriteLine(System.Math.Sin(3.14/2)); Par défaut, les paramètres sont passés par copie (comme en C/C++). Celà signifie que le paramètre formel est une variable locale à la méthode, initialisée lors de l'appel par le paramètre effectif. Toute manipulation du paramètre formel n'a pas d'incidence sur le paramètre effectif.

Pour pouvoir modifier les paramètres au sein de la méthode, il faut ajouter le mot clé ref à la fois dans la définition de la méthode et lors de l'appel

Propriétés

En C#, on peut définir des propriétés. Il s'agit de membres qui donnent l'impression d'être des champs de données publics, alors que leur accès est contrôlé par des fonctions en lecture et en écriture.

Dans les propriétés, le mot clé value désigne la valeur reçue en écriture (bloc set ).

Indexeur

Correspond à l'opérateur d'indexation du C++. Ce mécanisme permet de traiter un objet comme un tableau. Pour utiliser ce mécanisme, il faut définir une méthode this[ ] dont le paramètre est la variable qui sert d'indice.

Par exemple, l'indexeur ci-dessous utilise un indice entier et retourne un entier. La partie get gère l'accès en lecture et la partie set l'accès en écriture. Opérateur unaire : fonction membre statique avec un seul argument, du type de la classe. Doit retourner un objet, du type de la classe contenant le résultat.

Définition des opérateurs en C#

Opérateur binaire : fonction membre statique avec deux arguments, du type de la classe. Doit retourner un objet, du type de la classe contenant le résultat.

Parsing (conversion string vers numérique)

Les

Les énumérations (types valeur)

Un type créé avec le mot clé enum représente des variables pouvant prendre un nombre fini de valeurs prédéfinies. Une variable d'un type énumération peut prendre pour valeur une des valeurs définies dans la liste décrivant l'énumération :

Les valeurs d'une énumération correspondent à des valeurs entières qu'on peut choisir. On peut choisir aussi le type entier sous-jacent. On peut transtyper une valeur énumération en valeur entière. On peut aussi appliquer

Les structures

Le mot-clé struct permet de définir des types valeur ayant des méthodes et des atttributs. L'allocation d'une variable de type valeur se fait sur la pile alors que les objets de type référence sont alloués dans le tas. L'intérêt des types struct (type valeur) est de pouvoir gérer les objets plus efficacement (accès mémoire plus rapide et ne nécessite pas le garbage collector). Certaints types fournis par .NET sont des types struct. En contrepartie, certaines restrictions s'appliquent aux types créés avec le mot clé struct.

Particularités des types créés avec struct :

Les structures peuvent être converties en type object (comme tout type) Les structures ne peuvent dériver d'aucune classe ou structure. Les structures ne peuvent pas servir de base pour la dérivation. Un type structure peut contenir un/des constructeur(s) mais pas de constructeur sans argument Les champs ne peuvent pas être initialisés en dehors des constructeurs Un type structure peut implémenter une interface Il y a toujours un constructeur par défaut (sans argument) qui fait une initialisation par défaut (0 pour les valeurs numériques). Ce constructeur reste utilisable même si un autre constructeur a été défini. On peut définir des propriétés, des indexeurs dans les structures.

Les structures doivent être utilisées principalement pour les objets transportant peu de données membre. Au delà de quelques octets, le bénéfice obtenu du fait que l'allocation se fait dans la pile est perdu lors des passages de paramètres, puisqu'un objet de type valeur est dupliqué lorsque l'on fait un passage par valeur.

La portée d'une variable de type valeur va de la déclaration jusqu'à la fin du bloc de la déclaration (puisque les variables sont alors stockées sur la pile).

La composition (objet avec des objets membres)

La programmation orientée objet permet de décrire les applications comme des collaborations d'objets. Par conséquent, dans les diagrammes de classe, certaines relations apparaissent entre les classes. Par exemple, certaines méthodes d'une classe utilisent les services d'une autre classe. Il n'est pas question de rentrer dans les détails de la modélisation par UML, mais on peut toutefois s'intéresser à certaines relations importantes comme C#/.NET la composition et la dérivation (cf. Section suivante) La composition décrit la situation où un objet est lui même constitué d'objets d'autres classes. Par exemple un ordinateur est composé d'une unité centrale et de périphériques. Un livre est constitué de différents chapitres.

Cette relation particulière est décrite par une relation où un losange est dessiné côté composite (l'objet qui agrège).

Pour le diagramme ci-contre, la relation entre la classe Réseau et la classe Ordinateur signifie : "un réseau est constitué de 1 ou plusieurs ordinateurs (1..*) "

Dans le langage objet cible, cette relation de composition est simplement décrite par le fait qu'une classe puisse avoir un ou plusieurs membres de type classe (ou struct). Les classes ci-dessus ont les relations suivantes : un objet de la classe B est constitué d'un objet de la classe A un objet de la classe C est constitué de 0, 1 ou plusieurs objets de la classe A puisque un objet de type C stocke éventuellement plusieurs objets de type A.

La dérivation

La relation de composition (un objet est composé d'autres objets) n'est pas la seule relation entre les objets. La dérivation est une seconde relation essentielle de la programmation orientée objet.

La dérivation est un mécanisme fourni par la langage qui permet de spécialiser (la classe décrit des objets plus spécifiques) une classe existante en lui ajoutant éventuellement de nouveaux attributs / de nouvelles méthodes. La classe dérivée possède alors intégralement les attributs et les méthodes de la classe de base. On peut donc considérer un objet de la classe dérivée comme un objet de la classe de base, avec des membres en plus. C'est une façon de ré-utiliser une classe existante.

Ci-dessous, la classe B dérive de la classe A (c'est indiqué dans l'entête de la classe B). La classe B possède donc automatiquement les attributs et les méthodes de la classe A, avec en plus des membres spécifiques. La classe B dérive de la classe A.

B est une sous-classe de la classe A.

A est super-classe ou classe parent de la classe B.

Important : sur le plan de la compatibilité de type, un objet de la classe B peut toujours être considéré comme un objet de la classe A aussi (puisqu'il possède au moins les attributs et méthodes de la classe de base).

Syntaxe

On peut définir une classe comme sous-classe d'une classe existante de la façon suivante : la classe Derivee est sous-classe de la classe MaClasse. Ce qui signifie que tout objet de la classe Derivee dispose des attributs et méthodes de sa (ses) super-classe(s).

Différence de visibilité protected/private

Un membre privé n'est pas accessible par l'utilisateur de la classe, ni par un objet de la classe dérivée.

Un membre protected n'est pas accessible par l'utilisateur de la classe mais est accessible par un objet de la classe dérivée. Voir ci-dessous le membre _valeur.

Initialisateur de constructeur (dans le cas de la dérivation)

Le mot clé base( ) utilisé au niveau du constructeur indique quel constructeur de la classe de base utiliser pour initialiser la partie héritée d'un objet Derivee.

Par défaut, les méthodes ne sont pas virtuelles (ligature statique)

De même qu'en C++, les méthodes sont non virtuelles par défaut. Ce qui signifie qu'à partir d'une référence de type MaClasse, on ne peut appeler que des méthodes de cette classe ou de ses classes de base. Y compris si la référence désigne en réalité un objet d'une classe dérivée de MaClasse, ce qui est possible avec la compatibilité de type. On appelle ça une ligature statique des méthodes. La méthode invoquée est alors désignée à la compilation.

Dans le cas de la surdéfinition d'une méthode (non virtuelle) de la classe de base dans la classe dérivée, il faut tout de même préciser qu'on surdéfinit la méthode (on reconnaît qu'il y a déja une méthode de même nom dans l'ascendance) en utilisant le mot-clé new.

Définition des méthodes virtuelles (méthodes à ligature dynamique)

Le mot clé virtual doit être ajouté pour que l'appel d'une méthode bénéficie d'une recherche dynamique de la "bonne" méthode à l'exécution. En outre, la surdéfinition d'une méthode virtuelle dans la classe dérivée doit être précédée du mot-clé override (on reconnaît ainsi qu'il y a déja une méthode de même nom dans l'ascendance et que celle-ci est virtuelle)

Classe object (System.Object)

En .NET, le type object (System.Object) est classe racine de tout type. Tout peut donc être transformé en type object. Au besoin, pour les types valeur, l'opération de boxing permet de créer un objet compatible avec object. La classe object dispose des méthodes ci-dessous bool Equals(object o ) : (virtuelle) indique l'égalité d'objets. Pour les types valeurs, l'égalité de valeur. Pour les types référence, l'égalité de référence. Attention, ce mécanisme peut être dynamiquement substitué. On doit donc consulter la documentation du type pour connaître le rôle exact de cette méthode.

Boxing/unboxing

.NET fait la distinction type valeur/type référence pour que la gestion des variables avec peu de données soit plus efficace. Pour conserver une uniformité d'utilisation de tous les types, même les variables de type valeur peuvent être considérées comme de type référence. L'opération qui consiste à créer un objet de type référence à partir d'une variable de type valeur est appelée Boxing. L'opération inverse permettant de retrouver une variable à partir d'un type référence est appelée Unboxing.

Boxing : conversion type valeur vers type référence Unboxing : conversion inverse Grâce au boxing, on peut considérer une variable int (qui est pourtant un type primitif) en objet compatible avec les autres classes .NET. Le boxing consiste à créer une variable sur le tas qui stocke la valeur et à obtenir une référence (compatible object) sur cet objet. L'opération de boxing a donc un coût à l'exécution. Mais ce mécanisme garantit que toute variable/objet puisse être compatible avec le type ancêtre object.

Reconnaissance de type (runtime)

Avec .NET (pas seulement C#), les types ont une description stockée dans les modules compilés. Le compilateur n'est pas le seul a connaître les types utilisés. Ces types sont décrits par des méta-données stockées dans les modules compilés. On peut donc connaître à l'exécution (runtime) beaucoup d'informations sur les objets manipulés et leurs types. Tous les types sont représentés par un descripteur de type de la classe System.Type qui décrit exhaustivement les caractéristiques du type : nom, taille, méthodes et signatures … Ce mécanisme est dans l'interface de la classe de base object. Tout objet (et même toute variable) dispose donc d'une méthode GetType qui fournit un descripteur du type de l'objet.

public void A() { Console.WriteLine("Base.A()");} virtual public void B() { Console.WriteLine("Base.B()");} public void C() { Console.WriteLine("Base.C()");} } //

Conversions de type

Notation des casts

Utilise la notation de cast du C/C++. La conversion Dérivé vers Base est toujours possible. On peut par exemple toujours stocker une référence à n'importe quel objet dans une référence de type object. Dans l'autre sens, si la conversion est possible, elle nécessite un cast.

Opérateur is

Typeof/Sizeof

Le mot-clé typeof retourne le descripteur de type pour un type donné.

Type t =typeof(int);

L'opérateur sizeof ne s'applique qu'à un type valeur et retourne la taille d'une variable de ce type Console.WriteLine(sizeof(int));

Interfaces et classes abstraites

Interfaces

Une interface est un ensemble de prototypes de méthodes, de propriétés ou d'indexeurs qui forme un contrat. Une classe qui décide d'implémenter une interface s'engage à fournir une implémentation de toutes les méthodes prévues par l'interface.

En contrepartie, dès lors qu'une classe implémente une interface donnée, elle contient nécessairement un comportement minimal, celui prévu par l'interface.

Une interface décrit uniquement un ensemble de prototypes de méthodes que devront implémenter les classes qui veulent respecter cette interface :

pas de qualificatif public/private/protected pour les membres pas de données membres pas de qualificatif virtual Ci-dessous une interface avec 2 méthodes et deux classes implémentant cette interface.

Forcer à utiliser l'abstraction

On peut forcer l'utilisateur à utiliser une classe via son abstraction définie par une interface. Ci-dessous, la méthode A de l'implémentation ne peut être appelée qu'à partir d'une référence de type IMonBesoin. Interface System.ICloneable : une classe qui implémente cette interface fournit un moyen de créer une copie d'un objet de la classe (objet clone).

Tester qu'un objet implémente une interface (opérateurs is et as)

On peut tester, à l'exécution, si un objet donné implémente ou non une interface donnée. On utilise là encore les opérateurs is et as du langage qui s'appuient sur le CTS pour vérifier la compatibilité de type.

Classes abstraites

Les interfaces ne peuvent décrire qu'une liste de méthodes qu'une classe devra implémenter. On ne peut pas définir de méthode dans une interface ni même déclarer des données membres. Une classe abstraite correspond aux classes abstraites du C++. Les méthodes n'ont pas toutes une implémentation. Celle qui n'ont pas d'implémentation sont marquée abstract. En revanche, certaines méthodes concrètes peuvent être définies au niveau d'une classe abstraite. C'est ce qui les différencie des interfaces.

Puisqu'une classe abstraite ne définit pas toutes ses méthodes, il n'est pas possible d'instancier une classe abstraite. En revanche, une classe abstraite sert de base dans le cadre de la dérivation. Toutes les méthodes et attributs définis dans une classe abstraite sont disponibles dans les classes qui en dérivent.

Exceptions

De nombreuses fonctions C# sont susceptibles de générer des exceptions, c'est à dire des erreurs. Une exception est déclenchée lorsqu'un comportement anormal se produit. Lorsqu'une fonction est susceptible de générer une exception, le programmeur devrait la gérer dans le but d'obtenir des programmes plus résistants aux erreurs : il faut toujours éviter le "plantage" sauvage d'une application. La gestion d'une exception se fait selon le schéma suivant :

try{ code susceptible de générer une exception } catch (Exception e){ traiter l'exception e } instruction suivante

Si la fonction ne génère pas d'exception, on passe alors à instruction suivante, sinon on passe dans le corps de la clause catch puis à instruction suivante. Notons les points suivants :

• e est un objet de type Exception ou dérivé. On peut être plus précis en utilisant des types tels que IndexOutOfRangeException, FormatException, SystemException, etc… : il existe plusieurs types d'exceptions.

En écrivant catch (Exception e), on indique qu'on veut gérer tous les types d'exceptions. Si le code de la clause try est susceptible de générer plusieurs types d'exceptions, on peut vouloir être plus précis en gérant l'exception avec plusieurs clauses catch :

catch (System.OverflowException e) // intercepte les exceptions de type OverflowException {

Console.WriteLine(e.Message); } } } 9. Divers

Types imbriqués

On peut créer un type à l'intérieur d'un autre type. Ci-dessous, la classe B est déclarée à l'intérieur de la classe A. Les méthodes de la classe B ont donc accès à la partie privée d'un objet de la classe A. Puisque le type B est public, on peut aussi instancier un objet de la classe A.B (la classe B définie dans A).

Dérivation et vérification de type

Le fichier ci-dessous décrit trois classes en relation hiérarchique de dérivation. La classe C dérive de B qui ellemême dérive de A.

Comme l'on peut désigner tout objet par une référence de type object, il est important de savoir vérifier le type réel d'un objet à l'exécution. On peut utiliser les transtypages, ou les opérateurs is et as. /*****************************************/ // b=a; // erreur de compilation (utiliser cast ou is/as) /*****************************************/ /*************************************************/ // opérateur is : vérification dynamique de type /*************************************************/

a = new C(); if (a is B) { /* (a is B) booléen qui indique si le type dynamique de a est B ou d'une sous-classe de B */ // ici (a is B)==true puisque a est de type dynamique C } if (a is C) { // (a is C) == true } a= null; if (a is C){ // (a is null)==false }

/*************************************************/ // operateur as : conversion dynamique de type /*************************************************/ a = new B();

b = a as B; /// equivaut à if(a is B) { b = (B)a;} else {b = null;} if(b!=null){ // a désignait un objet compatible avec B, b est sa référence } c = a as C; // (a as C)==null puisque a n'est pas de type dynamique C if(c==null){

Console.WriteLine("a n'est pas de type C"); // l'objet désigné par a n'est pas de type compatible avec C } /*************************************************/ // cast et exception // les cast impossibles déclenchent des exceptions /************************************************

Accès aux membres non virtuels hérités

Après dérivation, un objet peut disposer de plusieurs méthodes non virtuelles de même nom. Dans l'exemple cidessous, un objet de la classe C dispose de 3 méthodes f(). Par défaut, une référence de type A ne permet d'atteindre que la méthode de la classe A (puisque f n'est pas virtuelle). Inversement, avec une référence de type C, on appelle la méthode de la classe C. Pour pouvoir utiliser les autres méthodes héritées, il faut transtyper. Dans l'exemple ci-dessous, la méthode B.g() n'est plus virtuelle. Le mot clé new indique que le mécanisme ne doit pas s'appliquer pour les appels de cette méthode sur un objet B. Alors que la méthode f() est toujours virtuelle. On peut même regrouper les mots clé new et virtual. Voir le comportement de l'appel de la méthode f() selon qu'il s'applique à un objet B, C ou D et selon que la référence est de type A, B ou C.

Délégation

Le mot-clé delegate permet de créer des classes particulières appelées classes de délégation. Dans l'exemple cidessous, la classe de délégation est Deleg. Une instance de cette classe est appelée objet délégué ou délégué.

Une classe de délégation dérive implicitement de la classe System.MulticastDelegate. Un objet délégué doit assurer des appels à des méthodes qu'on lui a indiquées.

Lors de la définition de la classe de délégation, on mentionne également la signature des méthodes prises en charge par la délégation. Une instance de Deleg permettra d'appeler des méthodes acceptant un paramètre double et retournant un int.

Ci-dessous, Round désigne une instance de la classe Deleg. On peut ensuite utiliser l'objet Round comme s'il s'agissait d'une fonction acceptant un paramètre double et retournant un entier. A l'instanciation, on précise quelle méthode utiliser pour la délégation. Ensuite, on peut changer dynamiquement la méthode utilisée par l'objet délégué. Les opérateurs -= et += agissent sur l'objet délégué pour retirer ou ajouter une méthode.

Simplification d'écriture avec C# 2

Avec la deuxième version de C#, le compilateur peut inférer certains types ou signatures. La définirion de la classe Deleg est identique. Seule la création des objets délégués devient un peu plus simple.

Plusieurs appels pour un délégué

Un objet délégué peut prendre en charge l'appel d'une méthode mais aussi d'une liste de méthodes. Dans l'exemple ci-dessous, une instance de la classe CallBack permet d'appeler plusieurs fonctions.

Appels d'une méthode d'une classe par un délégué

Les exemples précédents de délégation utilisaient des méthodes statiques. Un délégué peut aussi invoquer une méthode d'une classe mais il faut alors préciser sur quel objet elle devra être appelée. Ci-dessous, l'objet fonctionCB peut déclencher l'appel de a.CB2().

Evénéments

On peut voir les événements comme une forme particulière de délégation. La similitude repose sur le fait qu'un événement peut déclencher l'appel d'une ou plusieurs méthodes (comme pour une délégation). En revanche, selon que l'on est dans la classe propriétaire de l'événement ou non, on n'a pas les même droits d'utilisation. Seule la classe propriétaire de l'événement peut déclencher des appels. Par contre, n'importe quel utilisateur peut abonner/désabonner une méthode à l'événement, c'est-à-dire ajouter ou retirer une méthode de la liste des métodes invoquées par l'événement.

Cet aspect du langage permet de simplifier la mise en oeuvre du design pattern Observer. En C#, le sujet est une classe contenant un événement. Les observateurs sont toutes les classes abonnant une de leurs méthodes à l'événement du sujet.

Dans l'exemple ci-dessous, la classe Emetteur a un événement appelé EmetteurEvent. Cet événement est déclenchable par tout objet de la classe Emetteur. Lorsque l'événement est déclenché, les abonnés à cet événement sont notifiés. A noter, un événement est toujours déclaré à partir d'un type de délégation. Le type de délégation décrit la signature des méthodes pouvant s'abonner à l'événement.

L'événement ci-dessous ne déclenche que des méthodes sans paramètre ni retour.

Exploitation standard des événements dans .NET

L'utilisation des événements, notamment dans les classes System.Windows.Forms, suit toujours un peu la méme architecture. Les événements sont abondamment utilisés dans les application WinForms, puisque tous les événements du système (souris, clavier, …) sont vus comme des événements dans les classes. Un gestionnaire d'événement consiste donc à abonner une méthode de notre cru à un événement donné.

Dans ce schéma là, les événements ont généralement une signature (défini par une délégation) assez standard. Les événements appellent des méthodes ayant le plus souvent deux paramètres. Le premier est une référence à l'objet qui déclecnhe l'événement (le sujet ci-après). Le second est une référence à un objet censé transporter des données associées à l'événement. Pour un événement click souris, les données transmises sont les coordonnées du click. Le second paramètre est une référence de type System.EventArgs (ou dérivé).

Si l'on reprend ce schéma, pour une application console où l'on souhaite que le sujet puisse notifier un observateur, on obtient l'application ci-après.

Les classes conteneurs

Le framework .NET fournit des classes pour stocker des données. Nous décrivons sommairement quelques classes d'usage fréquent.

Classe string

Les membres de la classe string

Le type string du C# est un alias du type System.String de .NET qui permet de gérer des chaînes de caractère. Il s'agit d'un type référence mais dont l'usage ressemble à celui des types valeurs. Pour le type string, l'affectation fait en réalité une copie des valeurs (et non une copie de références). L'autre caractéristique est que les objets de cette classe sont immutables. Celà signifie que les objets gardent la même valeur du début à la fin de leur vie. Toutes les opérations visant à changer la valeur de l'objet retourneront en réalité un nouvel objet.

Constructeurs string(Char[] tabC)

Initialise une nouvelle instance de la classe String à la valeur indiquée par un tableau de caractères Unicode. string(Char, Int32 repete)

Initialise une nouvelle instance de la classe String à la valeur indiquée par un caractère Unicode et répété un certain nombre de fois.

string(Char[], Int32 index, Int32 count)

Initialise une nouvelle instance de la classe String à la valeur indiquée par un tableau de caractères Unicode, un point de départ pour le caractère dans ce tableau et une longueur.

Length

Obtient le nombre de caractères dans cette instance.

Méthodes publiques

public bool EndsWith(string value) rend vrai si la chaîne se termine par value public bool StartsWith(string value) rend vrai si la chaîne commence par value public virtual bool Equals(object obj) rend vrai si la chaînes est égale à objéquivalent chaîne==obj public int IndexOf(string value, int startIndex) rend la première position dans la chaîne de la chaîne value -la recherche commence à partir du caractère n° startIndex

Les tableaux

En C#, les tableaux sont des objets de type référence, compatibles avec le type de base Array. Le type Array est une classe abstraite que seul le compilateur peut dériver et implémenter. Pour construire des objets tableaux, on utilise les constructions décrites ci-dessous.

Il faut retenir que les tableaux générés ainsi ne sont pas redimensionnables. Dès lors qu'il est nécessaire de changer de dimension, les méthodes utilisées re-créent de nouveaux tableaux.

Tableau unidimensionnel

Déclaration des tableaux (à une dimension):

Parcours d'un tableau

Les conteneurs génériques.

Le C# permet la définition de classes paramétrées en type. Celà signifie que certains types peuvent être choisis au moment de l'instanciation. Du fait que certains types sont paramétrables, on parle de classe générique.

Sans rentrer dans les détails, on peut fournir un exemple simple de classe générique. On représente des paires de valeurs. Le type des valeurs de la paire est un paramètre de type de la classe.

Quelques interfaces de conteneurs : IEnumerable, ICollection, Ilist, IDictionary

Les classes conteneurs sont toutes les classes fournies par le framework .NET qui permettent de stocker des données. Le premier type de conteneur utilisé est le tableau (type dérivé de Array). D'autres classes génériques permettent de stocker des données. Les conteneurs se distinguent par le type de service qu'ils rendent.