Blog ENI : Toute la veille numérique !
-25€ dès 75€ sur les livres en ligne, vidéos... avec le code FUSEE25. J'en profite !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici

La programmation orientée objet avec C#

Les principes de la programmation orientée objet

La notion d’objet est omniprésente lorsque l’on développe avec C#. Nous allons donc voir dans un premier temps ce que représente cette notion, puis nous verrons comment la mettre en œuvre.

La programmation procédurale, telle qu’utilisée avec des langages comme C ou Pascal, définit un programme comme un flot de données qui seront transformées, au fil de l’exécution, par des procédures et fonctions. Aucun lien fort n’existe entre données et actions.

La programmation orientée objet (POO) introduit la notion d’ensembles cohérents de données et d’actions en transposant au monde du développement des concepts communs et intuitifs issus du monde qui nous entoure.

En effet, nous utilisons au quotidien des objets ayant tous des propriétés et des actions qui leur sont associées. Ils peuvent également interagir ou être composés les uns des autres, ce qui permet de former des systèmes complexes.

Une infinité d’analogies existe pour matérialiser ce concept, mais nous choisirons pour cette introduction celle de l’automobile, à la fois suffisamment simple et suffisamment complexe pour illustrer parfaitement les notions associées à la POO.

Une voiture a des propriétés qui lui sont propres, comme sa marque ou sa couleur, ainsi que des actions qui lui sont associées, comme le fait de démarrer, de freiner...

Les classes et les structures

L’utilisation d’objets étant au cœur de la POO, nous commencerons par voir comment les créer, de la déclaration de la classe ou de la structure qui servira de modèle, jusqu’à l’instanciation. Nous verrons aussi comment ajouter des propriétés et des fonctionnalités à nos objets.

1. Classes

La majorité des types définis dans le framework .NET sont des classes, aussi il est très important de comprendre comment les manipuler. En effet, toute application écrite en C# comportera au minimum une classe écrite par le développeur, et utilisera probablement des dizaines/centaines de classes issues du framework .NET.

a. Déclaration

Une classe se définit et est utilisée au travers de son nom. Il faut respecter certaines règles pour ce nommage, sous peine de se voir dans l’impossibilité de compiler. Les possibilités d’utilisation de la classe sont quant à elles définies par le modificateur d’accès associé.

Nom d’une classe

Le nom d’une classe n’est valide que s’il respecte les règles suivantes :

  • Il ne contient que des caractères alphanumériques ou le caractère _.

  • Il ne commence pas par un chiffre.

Par convention, il est d’usage de nommer les classes en respectant le style "UpperCamelCase" (aussi appelé PascalCase), c’est-à-dire que la première lettre de chaque mot composant le nom de la classe doit être en majuscule, tandis que le reste est écrit en minuscules. Toutes les classes du framework .NET respectent cette convention.

MaClasse, MurEnBrique ou PorteManteau sont des noms respectant la casse "UpperCamelCase".

ecranPlat est un nom accepté mais ne respectant pas cette convention.

Syntaxe

Une classe est déclarée en utilisant le mot-clé class suivi d’un nom et d’un bloc de code délimité par les caractères { et }.

La syntaxe générale de déclaration d’une classe est la suivante :

<modificateur d'accès> [partial] class <nom de la classe> :  
[<Classe de base>, <Interface1>, <Interface2>, ...] 
{ 
} 

Les notions de classe de base et d’interface...

Les espaces de noms

Le code d’une application peut très vite contenir plusieurs dizaines, centaines ou milliers de types différents. Afin d’organiser le projet, il est tout à fait possible de créer une structure hiérarchique de dossiers regroupant les types par utilité ou par contexte d’utilisation. En plus de cette organisation physique, il est possible de créer une organisation logique à l’aide du concept d’espace de noms (namespace en anglais).

1. Nomenclature

Un espace de noms est composé de plusieurs identificateurs séparés par l’opérateur ., chacun des identificateurs faisant office de conteneur logique. Voyons quelques exemples d’espaces de noms :

System 
System.Windows 
System.Data.SqlClient 

Ces trois espaces de noms font partie du framework .NET. System contient les types de base de .NET, comme les types primitifs, System.Windows est le conteneur logique des types de base pour la création d’applications fenêtrées. Enfin System.Data.SqlClient contient les types de la brique ADO.NET spécifiques aux bases de données SQL Server.

En plus de l’aide à la structuration, l’utilisation d’espaces de noms permet d’avoir plusieurs types dont le nom est identique : pour éviter toute ambiguïté entre ces types, il est possible d’utiliser le nom pleinement...

L’héritage

L’héritage permet de représenter et d’implémenter une relation de spécialisation entre une classe de base et une classe dérivée. Il permet donc la transmission des caractéristiques et du comportement du type de base vers le type dérivé, ainsi que leur modification.

1. Mise en œuvre

Pour déclarer une relation d’héritage entre deux classes, il est nécessaire de modifier la déclaration de celle que l’on veut définir comme étant la classe dérivée. Cette modification consiste en l’ajout du symbole ":" et du nom de la classe de base après la déclaration du type :

public class ClasseDeBase 
{ 
    public int Identifiant;  
 
    public void AfficherIdentifiant()
          => Console.WriteLine(Identifiant) 
}  
 
public class ClasseDerivee : ClasseDeBase  
{ 
} 

Contrairement à d’autres langages comme le C++, C# n’autorise pas à faire dériver un type de plusieurs classes de base.

Une fois que cette relation d’héritage est mise en place, il est parfaitement possible d’écrire le code suivant :

ClasseDerivee monObjet = new ClasseDerivee(); 
monObjet.Identifiant = 42; 
monObjet.AfficherIdentifiant(); 
//Affiche 42 dans la console 

En effet, ClasseDerivee peut accéder aux membres public, internal et protected de sa classe de base, dont la variable Identifiant et la méthode AfficherIdentifiant.

2. Les mots-clés this et base

Le mot-clé this renvoie l’instance courante de la classe dans laquelle il est utilisé. Il permet par exemple de passer une référence de l’objet courant à un autre objet.

namespace ThisEtBase 
{  
    public class Voiture 
    { 
          public decimal Longueur;  
 
          //Constructeur de la classe Voiture 
          public Voiture() 
          { 
                 //Les 2 lignes suivantes ont exactement le même 
       ...

Les interfaces

Pour manipuler différents types d’objets possédant des fonctionnalités similaires, il est possible d’utiliser un contrat définissant les données et les comportements communs à ces types. En C#, on appelle ce contrat une interface.

Une interface est un type qui possède uniquement des membres publics. En général, ces membres ne possèdent pas d’implémentation : ce sont les types qui respecteront ce contrat qui implémenteront chacun des membres. Nous verrons très vite que cette règle n’est pas absolue.

Les interfaces représentent une couche d’abstraction particulièrement utile pour rendre une application modulaire, puisqu’elles permettent d’utiliser n’importe quelle implémentation pour un même contrat.

1. Création

Les interfaces sont déclarées d’une manière semblable aux classes, les différences étant les suivantes :

  • Il faut utiliser le mot-clé interface au lieu de class.

  • Seuls les membres destinés à être visibles publiquement doivent être définis dans l’interface, et aucun modificateur de visibilité n’est autorisé sur ces membres.

  • Généralement, les membres de l’interface n’ont pas d’implémentation.

La syntaxe générale pour la création d’une interface est la suivante :

<modificateur d'accès> interface <nom> [: interfaces de base ] 
{ 
 
} 

Par convention, le nom des interfaces en C# commence par un I (i majuscule).

Considérons le cas de la lecture de fichiers audio. Il est tout à fait possible de lire des fichiers aux formats MP3, WAV, OGG, MWA, et bien d’autres encore. La lecture de chacun de ces formats nécessitant un décodage particulier, il semble pertinent de créer une classe pour chacun des types de fichiers supportés par une application : LecteurAudioMp3, LecteurAudioOgg, etc.

Il est fort probable que ces classes soient très similaires et doivent être utilisées dans les mêmes circonstances, ce qui fait donc de cet ensemble...

Les énumérations

Une énumération est un type de données représentant un ensemble fini de valeurs constantes pouvant être utilisées dans les mêmes circonstances.

La déclaration d’une énumération doit être faite en utilisant la syntaxe suivante :

<modificateur d'accès> enum <nom> 
{ 
    <nom de constante 1> [ = <valeur numérique>], 
    <nom de constante 2> [ = <valeur numérique>], 
    ... 
} 

Le framework .NET définit un nombre important de types d’énumérations. Parmi elles, on trouve le type System.Windows.Visibility, qui définit l’état visuel d’un contrôle WPF. Sa définition est la suivante :

public enum Visibility 
{ 
    Visible = 0, 
    Hidden = 1, 
    Collapsed = 2 
} 

On utilise une valeur d’énumération en écrivant le nom de l’énumération suivi de l’opérateur point (.) et du nom d’une constante d’énumération.

Pour masquer un contrôle System.Windows.Grid, on peut par exemple écrire ceci :

//grid est un objet de type System.Windows.Grid 
grid.Visibility = System.Windows.Visibility.Collapsed; 

Les délégués

Un délégué est un type représentant une référence à une méthode. Grâce aux délégués, il est possible de spécifier qu’un paramètre de méthode doit être une fonction possédant une liste de paramètres et un type de retour précis. Il est ensuite possible d’appeler cette fonction dans le corps de notre méthode sans la connaître à l’avance.

1. Création

La déclaration d’un délégué utilise le mot-clé delegate suivi d’une signature de procédure ou fonction. Le nom spécifié dans cette signature est le nom du type délégué créé.

//Création d'un délégué pour une fonction prenant 
//2 paramètres de type double et renvoyant un double 
public delegate double OperationMathematique(double operande1, 
double operande2); 

Le code ci-dessus crée un nouveau type utilisable dans l’application : OperationMathematique.

2. Utilisation

Le type OperationMathematique est utilisable pour toute variable ou tout paramètre de méthode. Une variable de ce type peut être utilisée comme une méthode, c’est-à-dire que l’on peut lui fournir des paramètres et récupérer...

Les événements

Les événements sont au cœur du développement d’applications avec C#. Ils permettent de baser la logique de l’application sur une série de procédures et de fonctions exécutées lorsque l’un de ses composants demande l’exécution. C’est par exemple le cas avec les composants graphiques : ceux-ci peuvent déclencher des événements lorsque l’utilisateur effectue une action comme la sélection d’un élément dans une liste ou un clic sur un bouton.

1. Déclaration et déclenchement

Les événements de C# sont basés sur l’utilisation de délégués. L’idée générale est que chaque événement peut accepter un ou plusieurs gestionnaires d’événements dont la signature est définie par un type de délégué.

Les événements générés par les classes du framework utilisent fréquemment le type de délégué EventHandler pour définir les gestionnaires d’événements qui peuvent leur être associés. Ce délégué est défini de la manière suivante :

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

Le paramètre sender correspond à l’objet qui a généré l’événement, tandis que le paramètre de type EventArgs, nommé e, est utilisé pour fournir des informations aux méthodes qui traitent l’événement. Si aucune valeur ne doit être passée aux gestionnaires d’événements...

Les génériques

Les génériques sont des éléments de programme capables de s’adapter de manière à fournir les mêmes fonctionnalités pour différents types de données.

Ils ont été introduits avec l’arrivée du framework .NET 2.0 dans l’objectif de fournir des services adaptés à plusieurs types de données tout en gardant un typage fort.

Les frameworks .NET 1.0 et .NET 1.1 fournissaient la classe ArrayList pour la gestion de listes dynamiques. Ce type, quoique très pratique, avait l’inconvénient de ne contenir que des objets de type System.Object, ce qui induisait un nombre important de transtypages dont il était souhaitable de se passer. Les génériques sont la réponse apportée par Microsoft à ce problème : la classe List<T> remplace avantageusement (dans la plupart des cas) la classe ArrayList en permettant de définir le type des objets qu’elle contient.

Ceci simplifie le code tout en le rendant plus sûr puisque le développeur élimine une grosse partie des problèmes de transtypage potentiels.

Les éléments génériques sont reconnaissables aux caractères < et > présents dans leurs noms. Ces symboles encadrent les noms de types associés à une instance d’un élément générique.

Les génériques seront ici étudiés au travers du développement d’un type permettant de gérer une file d’éléments. Les files sont des collections suivant le principe FIFO (First In, First Out) : il n’est possible d’accéder qu’au premier élément de la collection, et lorsque l’on ajoute un élément, celui-ci est automatiquement placé en dernière position de la collection. Un type similaire existe dans le framework .NET : System.Collections.Generic.Queue<T>.

1. Classes

Les classes génériques sont conçues pour manipuler d’une manière unifiée plusieurs types de données. Ces types sont désignés par un ou plusieurs alias dans le nom de la classe, et les types concrets sont assignés à ces alias à l’instanciation d’un...

Les collections

Il est fréquent qu’une application doive manipuler de grandes quantités de données. Pour ceci, le framework .NET fournit plusieurs structures de données, regroupées sous l’appellation collections. Celles-ci sont adaptées à différents types de situations : le stockage désordonné de données disparates, le stockage de données par type, le stockage de données par nom...

1. Types existants

Les différentes classes permettant la gestion de collections sont regroupées dans deux espaces de noms :

  • System.Collections

  • System.Collections.Generic

Le premier comporte les types "classiques", tandis que le second comporte les classes génériques équivalentes permettant de travailler avec des objets fortement typés.

a. Array

La classe Array ne se trouve pas dans l’espace de noms System.Collections, mais elle peut tout de même être considérée comme une collection. Elle implémente en effet plusieurs interfaces propres aux collections : IList, ICollection et IEnumerable. Cette classe est la classe de base pour tous les tableaux utilisés en C#.

Cette classe n’est néanmoins pas très utilisée directement : on lui préfère dans la majorité des cas la syntaxe C#.

La classe Array étant abstraite, il n’est pas possible de l’instancier avec l’opérateur new. Pour cela, on utilisera l’une des surcharges de la méthode statique Array.CreateInstance.

Array tableau = Array.CreateInstance(typeof(int), 5); 

Cette déclaration de variable est équivalente à celle-ci :

int[] tableau = new int[5]; 

b. ArrayList et List<T>

Les classes ArrayList et leur contrepartie générique List<T> sont des évolutions de la classe Array. Elles apportent certaines améliorations par rapport aux tableaux :

  • La taille d’un objet ArrayList ou List<T> est dynamique et s’ajuste en fonction des besoins.

  • Ces classes apportent des méthodes pour l’ajout, l’insertion ou la suppression d’un ou plusieurs éléments.

En revanche, les listes n’ont qu’une dimension, ce qui peut compliquer certains traitements.

Les collections de type ArrayList peuvent aussi poser des problèmes de performance...

La programmation dynamique

Depuis ses débuts, C# est un langage fortement et statiquement typé, ce qui signifie que le compilateur est capable de connaître le type de chaque variable utilisée dans une application. Ceci lui permet de savoir comment traiter les appels de méthodes et de propriétés sur chaque objet, et permet de générer des erreurs de compilation lorsqu’une opération est invalide.

L’arrivée de C# 4 et de l’environnement .NET en version 4.0 a introduit un nouveau type de donnée singulier : dynamic. Ce type s’adresse à des problématiques très particulières car il permet d’utiliser des variables dont le type n’est connu qu’au moment de l’exécution.

Le compilateur n’effectue ainsi pas de vérifications pour les opérations effectuées sur des variables dynamic. Elles ne sont effectuées qu’au moment de l’exécution de l’application : toute opération détectée comme invalide à ce stade génère une erreur d’exécution.

La fonction ci-dessous utilise deux paramètres de type dynamic et leur applique l’opérateur +.

private static dynamic Additionner(dynamic operande1, dynamic  
operande2) 
{ 
    var resultat = operande1 + operande2; 
 ...

La programmation asynchrone

Il est de plus en plus nécessaire de développer des applications réactives et capables d’effectuer plusieurs tâches simultanément. Le framework .NET a apporté des solutions à ce problème depuis ses débuts avec les classes Thread ou BackgroundWorker, entre autres. Avec le framework .NET 4.0 sont arrivées la classe Task et sa contrepartie générique Task<TResult>. Ces types ont simplifié le travail du développeur en permettant de gérer simplement le lancement ou l’attente de l’exécution de blocs de code, mais en fournissant aussi le moyen d’exécuter plusieurs traitements asynchrones à la suite.

L’arrivée de C# 5 a encore simplifié la tâche du développeur avec l’intégration de l’asynchronisme directement dans le langage. En effet, les mots-clés async et await permettent d’écrire du code asynchrone de manière séquentielle, comme du code... synchrone !

1. Les objets Task

Les classes Task et Task<TResult> permettent l’exécution de code asynchrone en encapsulant l’utilisation de threads.

Fonctionnement des threads

Les threads sont des unités d’exécution qui peuvent travailler, selon l’architecture de la machine, en parallèle ou en pseudo-parallèle (chaque thread s’exécute pendant une petite durée, puis cède sa place à un autre thread, qui cédera de nouveau sa place à un autre thread, etc.). C’est le système d’exploitation qui décide du temps processeur alloué à chaque thread. Pour cette raison, il est impossible...