Développement C# (.NET)

Développement C# (.NET)

Mis à jour le mardi 8 janvier 2013
Notions fondamentales

La programmation orientée objet (3/3)

Très bien, maintenant vous savez créer vos objets, mais savez vous ce qui se déroule réellement en mémoire avec vos objets ? Même si cette partie est théorique et assez "bas niveau" (c'est à dire qu'on se soucie de manière "importante" du fonctionnement de l'ordinateur et de ses contraintes), c'est une des choses qu'il faut garder constamment à l'esprit, mais rassurez-vous, avec un peu d'habitude on pense directement en espace mémoire :D
S'ensuivra ensuite la création et l'utilisation de constructeurs qui nous permettrons d'initialiser nos objets dès leur instanciation.

Les objets dans la mémoire

Type valeurs et types références

Lorsqu'on utilise le concept de programmation orienté objet, il est important de savoir ce qu'il se passe en mémoire lorsque l'on programme, même si dans notre cas en C# gère automatiquement certains aspects "obscurs" de la mémoire.

En effet, savoir ce qui se passe en mémoire va être important, car le comportement ne sera pas le même en fonction des types de donnés que l'on manipule !!

Pour voir les différents types de comportement, regardons le code suivant et son résultat (cet exemple utilise la classe Personne des chapitres sur laquelle ont été ajoutées des propriétés sur chaque champ ) :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Formation
{
    static class Program
    {
        static void Main()
        {
            Personne maPersonne = new Personne();//Instanciation de la classe avec le mot clé New
            maPersonne.Age = 30;
            int nombre = 10;
            Console.WriteLine("Avant l'appel, nombre vaut " + nombre);
            IncrementeNombre(nombre);
            Console.WriteLine("Apres l'appel, nombre vaut " + nombre + "\r\n");// \r\n permet de sauteur une ligne supplémentaire sans rappelle Console.WriteLine() sans paramètre
            Console.WriteLine("Avant l'appel, Age vaut " + maPersonne.Age);
            IncrementeAge(maPersonne);
            Console.WriteLine("Apres l'appel, Age vaut " + maPersonne.Age);
            Console.ReadKey();
        }

        static void IncrementeNombre(int nombre)
        {
            nombre++;
            Console.WriteLine("Dans la méthode, nombre vaut " + nombre);
        }

        static void IncrementeAge(Personne personneAChanger)
        {
            personneAChanger.Age++;
            Console.WriteLine("Dans la méthode, Age vaut " + personneAChanger.Age);
        }
    }
}
Avant l'appel, nombre vaut 10
Dans la méthode, nombre vaut 11
Apres l'appel, nombre vaut 10

Avant l'appel, Age vaut 30
Dans la méthode, Age vaut 31
Apres l'appel, Age vaut 31

La raison est simple : les types "primaires" sont manipulés par valeur, et les objets sont manipulés par référence.

Manipulation par valeur, manipulation par référence, qu'est ce que ça veut dire ?

Tout d'abord, il faut savoir que lorsque l'on passe des paramètres à une méthode, à l'exécution c'est toujours une copie que l'on passe à la méthode. Ce qui veut dire que lorsque le paramètre "nombre" a été passé, c'est une copie de ce nombre qui a été modifié, et non le nombre lui même.

Par contre, concernant l'objet maPersonne, cet objet n'est pas vraiment de type "Personne" mais est une référence sur un objet de type personne!!
C'est à dire que dans votre mémoire vive, à l'emplacement de "maPersonne" il ne va pas y avoir d'objet personne, mais une adresse mémoire pointant sur l'objet personne en lui même. Donc lorsque l'on va passer "maPersonne" en paramètre, on va faire une copie... de la référence, or les deux références pointent sur le même objet, ce qui fait que quand on va modifier la valeur de _age, on va modifier la valeur du vrai objet "maPersonne".

Si cela vous parait compliqué, je vais essayer d'illustrer avec un petit exemple imagé:
-Vous avez créé un site web, mais vous êtes nul en design, vous copiez donc vos fichiers sources sur une clé usb et que vous passez à un designer qui va les modifier, mais par contre, cela ne va pas modifier les fichiers qui sont toujours sur votre ordinateur, pour récupérer les modifications, il faut qu'il vous retourne la clé usb. Voilà la manipulation par valeur, on fait une copie de la valeur pour la transmettre.
-A présent, vous créez toujours un site web que vous avez hébergé sur un ftp (un dépôt de fichier sur internet), vous êtes toujours aussi mauvais en design, donc vous engagez un designer, mais cette fois-ci vous lui donnez l'adresse du ftp, donc les modifications qu'il fera seront répercutées directement sur votre travail. Voilà la manipulation par référence, on fait une copie de la référence (ici copié collé du lien) qui "pointent" toute les deux sur la même chose (le ftp ici).

Forcer un passage par référence

Vous vous souvenez que les types primitifs sont dit "types valeurs", c'est à dire que lorsque nous les passons en paramètres d'une méthode, ce n'est pas les vrais variables qu'on passe en paramètre mais une copie de ces variables. Généralement, cela ne va pas beaucoup nous gêner car nous avons souvent seulement besoin de récupérer la valeur de ces variables sans avoir à la modifier. Mais qu'est ce que cela va donner si nous modifions nos variables dans une méthode ? Voyons l'exemple suivant qui échange la valeur de deux entiers :

static void Main(string[] args)
{
    int firstInt = 1;
    int secondInt = 2;
    SwitchInt(firstInt, secondInt);
    Console.WriteLine("Entier un vaut : " + firstInt.ToString() + " et le second vaut : " + secondInt.ToString());
    Console.ReadKey();

}

static void SwitchInt(int firstInt, int secondInt)
{
    int entierTampon = firstInt;
    firstInt = secondInt;
    secondInt = entierTampon;
}

Si vous exécutez ce code, vous verrez que malgré le fait que nous modifions firstInt et secondInt dans la méthode, une fois sortis de cette dernière, les variables ont "retrouvés" leurs valeurs de départ. C'est tout à fait normal puisque c'est une copie de nos variables et non nos vrais variables qui sont utilisées par la méthode.

Heureusement, il utilise deux manières de passer nos variables de types primitifs par référence et non par valeur à nos méthodes : le mot clé refet le mot clé out. Mais voyons leur utilisation.

Le mot clé ref

Le mot clé refpermet de passer par référence une variable initialisée en paramètre d'une méthode. Dans ce cas, il nous faut explicitement indiquer en signature de la méthode que les paramètres doivent être passés par référence en précédent le type du paramètre par le mot clé ref de la manière suivante:

static void SwitchInt(ref int firstInt,ref int secondInt)
{
    int entierTampon = firstInt;
    firstInt = secondInt;
    secondInt = entierTampon;
}

Vous remarquez que le corps de notre méthode n'a pas changé, seule sa signature a été modifiée en ajoutant par deux fois le mot clé ref.

Lors de l'appel de la méthode, nous devons aussi indiquer explicitement que nous passons les paramètres par références, toujours par le même mot clé ref (je le répète beaucoup, mais vous devez vous en souvenir :p ).

static void Main(string[] args)
{
    int firstInt = 1;
    int secondInt = 2;
    SwitchInt(ref firstInt, ref secondInt);//Seule cette ligne a changé
    Console.WriteLine("Entier un vaut : " + firstInt.ToString() + " et le second vaut : " + secondInt.ToString());
    Console.ReadKey();

}

Si vous exécutez le code modifié, vous remarquerez que cette fois-ci les valeurs de nos entiers ont bien été échangés !

Le mot clé out

Le mot clé out a une utilisation différente. Il s'utilise dans des méthodes qui ne s'attendent pas à recevoir des variables initialisées. Si nous prenons notre code précédent en remplaçant les mot clés ref par out, ce code ne compilera même pas car nous essayons de récupérer les valeurs de firstInt et secondInt qui peuvent ne pas avoir de valeur.

Mais à quoi ça sert alors si nous ne pouvons pas récupérer la valeur d'un paramètre ?

La question est légitime. Mais si vous vous rappelez le cours sur les conversions de données de types string en d'autres types, vous verrez peut être l'intérêt... avec la méthode TryParse ! En effet, celle ci nous retournait un booléen pour nous indiquer si la conversion avait fonctionné ou non, mais il nous fallait bien récupérer la variable convertie. Nous utilisions donc le mot clé out pour récupérer cette valeur. Mais voyons un exemple d'utilisation pour bien comprendre :

string intEnString = "42";
int intResult;
bool conversionSucceeded = int.TryParse(intEnString, out intResult);
Console.WriteLine("Conversion réussie : " + conversionSucceeded.ToString() + " et le resultat est donc : " + intResult);

Ici la chaine de caractères est juste lue par la méthode, nous la passons donc simplement par valeur, par contre nous devons récupérer la valeur de l'entier et nous nous moquons bien d'avoir une valeur pour cette variable avant l'appel de la méthode TryParse puisque nous si nous l'appelons, c'est bien pour récupérer une valeur :-° . Nous utilisons donc le mot clé out pour fournir à TryParse un entier non initialisé (vous pouvez lui mettre une valeur avant, mais c'est inutile pour cette valeur sera "écrasée" lors de l'appel de la méthode).

En bref

Vous n'aurez peut être pas tout les jours besoin d'utiliser ces mot-clés pour passer vos variables par référence et non par valeur. Mais sachez que quand vous en aurez besoin, vous n'aurez souvent pas d'autre choix que de les utiliser, donc il faut absolument que vous sachiez le faire. Il vous faut aussi savoir que l'intérêt du passage par
référence n'est pas juste pour les types primitifs mais aussi pour nos propres objets, même si ils sont déjà passés par référence! Notre méthode SwitchInt peut s'appliquer pour n'importe quel type de donnée, essayons donc de faire une méthode SwitchPersonne qui permet d' "échanger" deux personnes tout d'abord sans mot clé ref puis avec ces mots-clés ;) .

Les constructeurs

Initialisation d'objets

Nous avons vu dans un de nos exemples précédant le code suivant:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Formation
{
    static class Program
    {
        static void Main()
        {
            Personne maPersonne = new Personne();//Instanciation de la classe avec le mot clé New
            maPersonne._nom = "Prion";//on modifie le nom, le prénom et l'age
            maPersonne._prenom = "Cynthia";
            maPersonne._age = 30;
            Console.WriteLine(maPersonne.GetIdentite());//On affiche l'identité grâce à GetIdentite()
            maPersonne.SetAge(32);//On modifie l'age grâce à la méthode que nous avons créée
            Console.WriteLine(maPersonne.GetIdentite());

            Console.ReadKey();
        }
    }
}

Celui-ci instanciait la classe Personne, puis modifiait les champs de l'objet créé. Nous avions donc une instruction pour l'instanciation, et une instruction par modification de valeur de champs.

A présent, il pourrait être intéressant de pouvoir, en une seule instruction, instancier notre classe et affecter des valeurs à ses champs. Depuis la version 3.5 de .net, il serait possible de faire ainsi:

Personne maPersonne = new Personne()
{
    Age = 30,//On initialise explicitement chaque champ
    Nom = "Prion",//On sépare chaque assignation par une virgule
    Prenom = "Cynthia"
};//Et on termine l'instruction par un point virgule

Nous avons donc en une seule instruction l'instanciation et l'assignation des champs. Mais disons que cette manière de faire n'est pas très élégante, et elle n'est possible que pour les champs de la classe qui sont publics !! D'ailleurs, cet exemple de code est là pour montrer que cela existe, mais cette manière de faire ne doit pas être reproduite. Heureusement, la Programmation Orientée Objet nous propose un outil que nous avons utilisé sans le savoir : le constructeur.

Le constructeur par défaut

Création du constructeur

Tout d'abord, un constructeur va être en quelque sorte une méthode publique sans type de retour et portant le nom de la classe, ce qui va donner pour notre classe Personne:

class Personne // j'ai volontairement raccourci le code de la partie précédente, je n'ai laissé que ce dont on avait besoin ici
{
    private string _nom;
    private string _prenom;
    private int _age;

    public Personne()//Le constructeur
    {
        _nom = "Prion";
        _prenom = "Cynthia";
        _age = 30;
    }
}

Comme vous le voyez, dans notre constructeur nous effectuons l'initialisation de valeurs. Au lieu de le faire pour chaque instance de notre classe, nous le faisons dans le constructeur, l'initialisation sera ainsi faite automatiquement pour chaque instance!!

L'appel du constructeur

A présent, voyons comment nous appelons ce constructeur:

Personne maPersonne = new Personne();

Et c'est tout ? Depuis tout à l'heure on utilisait le constructeur sans le savoir ? Pourtant avant nous ne créions pas de constructeur et nous pouvions quand même l'appeler, comment ça se fait ?

Et oui, c'est tout... Effectivement, quand nous créions l'objet tout à l'heure, nous appelions le constructeur dit "par défaut", c'est à dire un constructeur que nous n'avions pas besoin de créer pour qu'il fonctionne. Ce constructeur par défaut serait équivalent à celui que nous avions créé si nous n'aurions pas fait d'initialisation à l'intérieur.

Mais les constructeurs seraient bien peu intéressants s'ils faisaient tous la même chose, par exemple, toute nos personnes ne vont pas s'appeler Cynthia Prion !! Heureusement, comme pour les méthodes, nous pouvons passer des paramètres au constructeur!

Constructeurs avec paramètres

Création des constructeurs

A la suite du constructeur par défaut, nous pouvons créer un (ou même plusieurs) autre constructeurs avec des paramètres, la seule contrainte est qu'il n'y ai aucune ambiguïté entre les signatures des différents paramètres. Toujours pour notre classe Personne, nous pouvons avoir les quatre constructeurs suivants:

#region Constructeurs
public Personne()//Le constructeur sans paramètre
{
    _nom = "Prion";
    _prenom = "Cynthia";
    _age = 30;
}

public Personne(string nomComplet)//Constructeur avec un seul paramètre
{
    _prenom = nomComplet.Split(' ')[0];//Permet de prendre la partie du nom complet avant l'espace
    _nom = nomComplet.Split(' ')[1];//Permet de prendre la partie du nom complet après l'espace
    _age = 30;
}

public Personne(string prenom, string nom)//Constructeur avec deux paramètres
{
    _prenom = prenom;
    _nom = nom;
    _age = 30;
}

public Personne(string prenom, string nom, int age) // Constructeur avec trois paramètres
{
    _prenom = prenom;
    _nom = nom;
    _age = age;
}
#endregion

Chacun de ces constructeurs fait une action différente, nous n'avons donc plus que l'embarras du choix ;)

Appel des constructeurs

Pour appeler les différents constructeurs, nous instancions nos classes normalement en passant les paramètres demandés par le constructeurs voulu. L'extrait de code suivant montre quatre instanciations différentes de la classe Personne en utilisant les quatre constructeurs de notre classe.

Personne maPersonne1 = new Personne();//Cynthia Prion 30 ans
Personne maPersonne2 = new Personne("Obiwan Kenobi");//Obiwan Kenoby 30 ans
Personne maPersonne3 = new Personne("Anakin", "Skywalker");//Anakin Skywalker 30 ans
Personne maPersonne4 = new Personne("Chuck", "Norris", 60);//Chuck Norris 60 ans

Ce code ne devrait pas poser de difficultés particulière. Remarquez que chacun de ces objets a des champs différents grâce aux différents constructeurs

Bonus : faire autant en écrivant moins

Je ne sais pas pour vous, mais pour ce qui est de moi, je trouve long de ré-écrire un code semblable, surtout avec les deux derniers constructeurs, c'est quasiment du copié collé. Heureusement, les constructeurs peuvent s'appeler les uns les autres. Ainsi, nous écrivons seulement le code entier du constructeur prenant le plus de paramètres, et nous l'appelons à partir des autres. Voyez le code suivant:

#region Constructeurs
public Personne() : this("Cynthia", "Prion", 30) { }

public Personne(string nomComplet):this(nomComplet.Split(' ')[0], nomComplet.Split(' ')[1], 30){}//Constructeur avec un seul paramètre

public Personne(string prenom, string nom):this(prenom, nom, 30){}

public Personne(string prenom, string nom, int age) // Constructeur avec trois paramètres
{
    _prenom = prenom;
    _nom = nom;
    _age = age;
} 
#endregion

Le mot clé this dans une classe désigne une instance de cette classe, par exemple au lieu d'écrire:
_age = 10;

Nous pourrions écrire :
this._age = 10;

Dans le cas des constructeurs, nous l'utilisons pour en appeler un à partir d'un autre, ainsi
public Personne() : this("Cynthia", "Prion", 30) { } appelle le quatrième constructeur à partir du premier,
public Personne(string prenom, string nom):this(prenom, nom, 30){} appelle le quatrième constructeur à partir du troisème, etc...

Nous voyons aussi que les paramètres peuvent être passés d'un constructeur à l'autre.

L'héritage

En POO, l'héritage est avec l'encapsulation, un des concepts les plus importants. Mais avant de voir pourquoi l'héritage est si fabuleux, nous allons voir un cas où il va nous manquer.

Le problème

Repartons de l'exemple de la classe Personne, mais cette fois ci nous allons aussi créer une classe Enfant et une classe Monstre:

class Personne
{
    private string _nom;
    private string _prenom;
    private int _age;

    public Personne() : this("Skywalker", "Anakin", 25) { }

    public Personne(string nom, string prenom, int age)
    {
        this._nom = nom;
        this._prenom = prenom;
        this._age = age;
    }

    public void SePresenter()
    {
        Console.WriteLine("Bonjour, je suis " + this._prenom + " " + this._nom + " et j'ai " + this._age);
    }
}
class Enfant
{
    private string _nom;
    private string _prenom;
    private int _age;
    private string _niveauScolaire;

    public Enfant() : this("Petit", "Nicolas", 8) { }

    public Enfant(string nom, string prenom, int age)
    {
        this._nom = nom;
        this._prenom = prenom;
        this._age = age;
    }

    public Enfant(string nom, string prenom, int age, string niveauScolaire)
    {
        this._nom = nom;
        this._prenom = prenom;
        this._age = age;
        this._niveauScolaire = classe;
    }

    public void SePresenter()
    {
        Console.WriteLine("Bonjour, je suis " + this._prenom + " " + this._nom + " et j'ai " + this._age);
    }
}
class Monstre
{
    private string _nom;
    private string _prenom;
    private int _age;
    private string _race;

    public Monstre() : this("Tic-Tac", "George", 40) { }

    public Monstre(string nom, string prenom, int age)
    {
        this._nom = nom;
        this._prenom = prenom;
        this._age = age;
    }

    public Monstre(string nom, string prenom, int age, string race)
    {
        this._nom = nom;
        this._prenom = prenom;
        this._age = age;
        this._race = race;
    }

    public void SePresenter()
    {
        Console.WriteLine("Bonjour, je suis " + this._prenom + " " + this._nom + " et j'ai " + this._age);
    }
}

Comment ça? Vous trouvez ce code répétitif ?
Et bien je vais vous apprendre quelque chose... moi aussi je trouve ce code répétitif :lol:

Hériter une classe

Heureusement, il existe un moyen pour raccourcir (on dit aussi refactoriser) ce code. Tout d'abord, observons tout ce qui est communs entre ces trois classes :

  • Deux constructeurs

  • Methode "SePresenter"

  • Trois champs

En bref, tout ce qu'il y a dans Personne se retrouve dans les deux autres classes. Nous allons donc faire hériter Monstre et Enfant de Personne. Pour hériter une classe, la syntaxe est la suivante :

class ClasseEnfant : ClasseMere
{
}

Ici, ClasseEnfant hérite de ClasseMere, cela signifie que sans avoir à ajouter de code dans ClasseEnfant, cette classe implémentera automatiquement tout ce qui est public ou protégé de ClasseMere, nous verrons ce que "public" et "protégé" signifient plus tard, pour l'instant nous mettrons tout en public.
Nos trois classes Personne, Enfant et Monstre factorisées donneront :

class Personne//Aucun changement ici
{
    private string _nom;
    private string _prenom;
    private int _age;

    public Personne() : this("Skywalker", "Anakin", 25) { }

    public Personne(string nom, string prenom, int age)
    {
        this._nom = nom;
        this._prenom = prenom;
        this._age = age;
    }

    public void SePresenter()
    {
        Console.WriteLine("Bonjour, je suis " + this._prenom + " " + this._nom + " et j'ai " + this._age);
    }
}
class Enfant : Personne
{
    private string _niveauScolaire;

    public string NiveauScolaire
    {
	get { return _niveauScolaire; }
	set { _niveauScolaire = value; }
    }	

    public Enfant(string nom, string prenom, int age, string niveauScolaire)
        : base(nom, prenom, age)//Appel du constructeur "de base" c'est à dire le constructeur de la classe Personne
    {
        this._niveauScolaire = niveauScolaire;
    }
}
class Monstre : Personne
{
    private string _race;

    public string Race
    {
	get { return _race; }
	set { _race = value; }
    }

    public Monstre(string nom, string prenom, int age, string race)
        : base(nom, prenom, age)//Appel du constructeur "de base" c'est à dire le constructeur de la classe Personne
    {
        this._race = race;
    }
}

Notez tout d'abord le mot clé "base" au niveaux de constructeurs, alors que "this" permet d'appeler les champs et méthodes de l'objet en cours, "base" permet d'appeler les champs et méthodes de la classe héritée dans la classe héritant.
Le code est déjà plus court pour Enfant et Monstre, mais à présent qu'on hérite de Personne, il pourrait être intéressant d'interdire l'utilisation directe de la classe Personne vu que Enfant et Monstre font la même chose "en mieux".

Rendre une classe abstraite

Pour empêcher cette instanciation directe, il existe le mot clé "abstract" ("abstrait" en anglais) à placer devant notre classe personne de la manière suivante :

abstract class Personne//On utilise le mot clé abstract pour empêcher l'instanciation directe de cette classe
{
 //Placer ici le même contenu que précédemment
}

Et si nous essayons quand même de l'instancier (dans la méthode Main):

static void Main(string[] args)
{
    Personne maPersonne = new Personne();//Cette ligne empêchera le code de compiler
}

Ce code sera souligné en rouge par Visual Studio indiquant qu'il y a une erreur qui rendra la compilation impossible. Ce message d'erreur nous ferra comprendre que nous ne pouvons pas instancier directement Personne. Par contre nous pouvons très bien avoir une variable de type Personne que nous instancions grâce à un constructeur d'une méthode héritée:

static void Main(string[] args)
{
    Personne maPersonne = new Enfant("Bieber", "Justin", 16, "CP");
}

Ici, nous aurons donc une variable de type Personne mais initialisée grâce au constructeur de Enfant. Dans ce cas nous ne pourrons pas accéder directement aux champs absents de Personne, mais nous pourrons bien sur convertir notre variable ;) :

static void Main(string[] args)
{
    Personne maPersonne = new Enfant("Bieber", "Justin", 16, "CP");
    string classeDeJustin = ((Enfant)maPersonne).NiveauScolaire; // classeDeJustin sera accessible malgré que maPersonne ne soit pas du type Enfant
}

Cette abstraction est de même possible au niveau des méthodes, mais dans ce cas, deux mots-clés permettent cette abstraction : virtual et abstract. La différence va être que si on a une méthode abstraite (mot clé abstract), on sera obligé de proposer une implémentation dans la classe héritante. Par exemple si nous rendons la méthode "SePrésenter" abstraite (seul le code nous intéressant est affiché, néanmoins il doit être toujours présent dans vos fichiers dans Visual Studio ;) ):

abstract class Personne
{
    //Le reste du code de la classe est à placer ici

    public abstract void SePresenter();//Aucune corps de méthode ne doit être présent vu que cette méthode doit être re-implémenter
}

Dans ce cas, lorsque nous héritons de la classe Personne, nous sommes obligé de re-implémenter toute les méthodes marquées comme abstraites, pour faire cette réimplémentation, nous utilisons le mot clé override. Par exemple dans la classe enfant :

class Enfant : Personne
{
    private string _niveauScolaire;
    
    public string NiveauScolaire
    {
	get { return _niveauScolaire; }
	set { _niveauScolaire = value; }
    }

    public override void SePresenter()
    {
        Console.WriteLine("Bonjour, je suis " + this._prenom + " " + this._nom + ", j'ai " + this._age + " et je suis en " +  _niveauScolaire);
    }
}

Nous voyons l'implémentation de la méthode SePresenter avec le mot clé override, pour cela, il nous a suffit de taper "override" et Visual Studio nous a montré les méthodes à surcharger, nous avons donc sélectionné "SePresenter" et effectué notre traitement (ici écrire sur la Console). Sans cette implémentation, notre code n'aurait pas pu être compilé.
Par contre, si nous notons la méthode SePresenter comme virtuelle au lieu d'abstraite, nous pourrons très bien hériter Personne sans avoir à ré implémenter SePresenter, ainsi si la classe Personne et la méthode SePrésenter ressemblent à ça:

abstract class Personne
{
    //Le reste du code de la classe est à placer ici

    public virtual void SePresenter()
    {
        Console.WriteLine("Bonjour, je suis " + this._prenom + " " + this._nom + " et j'ai " + this._age);
    }
}

Nous pourrons très bien ne pas implémenter SePresenter dans Enfant, si on ne le fait pas, ce sera la méthode de base qui sera appelée:

class Enfant : Personne//Meme sans implémentation de la méthode virtuelle, ce code compilera parfaitement
{
    private string _niveauScolaire;

    public Enfant(string nom, string prenom, int age, string niveauScolaire)
        : base(nom, prenom, age)
    {
        this._niveauScolaire = niveauScolaire;
    }
}

A ce niveau là, je reconnais que vous venez d'ingurgiter une grande quantité d'informations après ces deux chapitres sur la POO. Si ce n'est pas clair ou que vous pensez ne pas tout retenir, je vous recommande d'y repasser un peu plus tard. En effet, ces chapitres sur la POO sont essentiels, nous ne pouvons quasiment pas allez plus loin sans eux, mais je vous rassure, ce sont les seuls que je vous recommande de connaitre presque par coeur ^^ , les suivants seront plus "logiques" ou traiterons de choses qui se documentent facilement, je ne vous en voudrais donc pas si vous devez revenir tout les jours sur les prochains chapitres pour relire (ou copier-coller :p ) des morceaux de codes.

Exemple de certificat de réussite
Exemple de certificat de réussite