Skip to content

3. Classes, stuctures, interfaces

3.1. L' objet par l'exemple

3.1.1. Généralités

Nous abordons maintenant, par l'exemple, la programmation objet. Un objet est une entité qui contient des données qui définissent son état (on les appelle des propriétés) et des fonctions (on les appelle des méthodes). Un objet est créé selon un modèle qu'on appelle une classe :

Public Class c1
    ' attributs
    Private p1 As type1
    Private p2 As type2
....

    ' méthode
    Public Sub m1(....)
...
    End Sub    

    ' méthode
    Public Function m2(...)
....
    End Function    
End Class

A partir de la classe C1 précédente, on peut créer de nombreux objets O1, O2,… Tous auront les propriétés p1, p2,… et les méthodes m3, m4, … Mais ils auront des valeurs différentes pour leurs propriétés pi ayant ainsi chacun un état qui leur est propre. Par analogie la déclaration

    dim i, j as integer

crée deux objets (le terme est incorrect ici) de type (classe) Integer. Leur seule propriété est leur valeur. Si O1 est un objet de type C1, O1.p1 désigne la propriété p1 de O1 et O1.m1 la méthode m1 de O1. Considérons un premier modèle d'objet : la classe personne.

3.1.2. Définition de la classe personne

La définition de la classe personne sera la suivante :


Public Class personne
    ' attributs
    Private prenom As String
    Private nom As String
    Private age As Integer

    ' méthode
    Public Sub initialise(ByVal P As String, ByVal N As String, ByVal age As Integer)
        Me.prenom = P
        Me.nom = N
        Me.age = age
    End Sub

    ' méthode
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub
End Class

Nous avons ici la définition d'une classe, donc d'un type de données. Lorsqu'on va créer des variables de ce type, on les appellera des objets ou des instances de classes. Une classe est donc un moule à partir duquel sont construits des objets. Les membres ou champs d'une classe peuvent être des données (attributs), des méthodes (fonctions), des propriétés. Les propriétés sont des méthodes particulières servant à connaître ou fixer la valeur d'attributs de l'objet. Ces champs peuvent être accompagnés de l'un des trois mots clés suivants :

privé
Un champ privé (private) n'est accessible que par les seules méthodes internes de la classe
public
Un champ public (public) est accessible par toute fonction définie ou non au sein de la classe
protégé
Un champ protégé (protected) n'est accessible que par les seules méthodes internes de la classe ou d'un objet dérivé (voir ultérieurement le concept d'héritage).

En général, les données d'une classe sont déclarées privées alors que ses méthodes et propriétés sont déclarées publiques. Cela signifie que l'utilisateur d'un objet (le programmeur) :

  • n'aura pas accès directement aux données privées de l'objet
  • pourra faire appel aux méthodes publiques de l'objet et notamment à celles qui donneront accès à ses données privées.

La syntaxe de déclaration d'une clase est la suivante :


public class classe
    private  donnée ou méthode ou propriété privée
    public  donnée ou méthode ou propriété publique
    protected  donnée ou méthode ou propriété protégée
end class

L'ordre de déclaration des attributs private, protected et public est quelconque.

3.1.3. La méthode initialise

Revenons à notre classe [personne] déclarée comme :


Public Class personne
    ' attributs
    Private prenom As String
    Private nom As String
    Private age As Integer

    ' méthode
    Public Sub initialise(ByVal P As String, ByVal N As String, ByVal age As Integer)
        Me.prenom = P
        Me.nom = N
        Me.age = age
    End Sub

    ' méthode
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub
End Class

Quel est le rôle de la méthode initialise ? Parce que nom, prenom et age sont des données privées de la classe personne, les instructions :

dim p1 as personne p1
p1.prenom="Jean"
p1.nom="Dupont"
p1.age=30

sont illégales. Il nous faut initialiser un objet de type personne via une méthode publique. C'est le rôle de la méthode initialise. On écrira :

dim p1 as personne
p1.initialise("Jean","Dupont",30)

L'écriture p1.initialise est légale car initialise est d'accès public.

3.1.4. L'opérateur new

La séquence d'instructions

dim p1 as personne
p1.initialise("Jean","Dupont",30)

est incorrecte. L'instruction

dim p1 as personne

déclare p1 comme une référence à un objet de type personne. Cet objet n'existe pas encore et donc p1 n'est pas initialisé. C'est comme si on écrivait :

dim p1 as personne=nothing

où on indique explicitement avec le mot clé nothing que la variable p1 ne référence encore aucun objet. Lorsqu'on écrit ensuite

p1.initialise("Jean","Dupont",30)

on fait appel à la méthode initialise de l'objet référencé par p1. Or cet objet n'existe pas encore et le compilateur signalera l'erreur. Pour que p1 référence un objet, il faut écrire :

dim p1 as personne=new personne()

Cela a pour effet de créer un objet de type personne non encore initialisé : les attributs nom et prenom qui sont des références d'objets de type String auront la valeur nothing, et age la valeur 0. Il y a donc une initialisation par défaut. Maintenant que p1 référence un objet, l'instruction d'initialisation de cet objet

p1.initialise("Jean","Dupont",30)

est valide.

3.1.5. Le mot clé Me

Regardons le code de la méthode initialise :


    Public Sub initialise(ByVal P As String, ByVal N As String, ByVal age As Integer)
        Me.prenom = P
        Me.nom = N
        Me.age = age
    End Sub    

L'instruction Me.prenom=P signifie que l'attribut prenom de l'objet courant (Me) reçoit la valeur P. Le mot clé Me désigne l'objet courant : celui dans lequel se trouve la méthode exécutée. Comment le connaît-on ? Regardons comment se fait l'initialisation de l'objet référencé par p1 dans le programme appelant :

p1.initialise("Jean","Dupont",30)

C'est la méthode initialise de l'objet p1 qui est appelée. Lorsque dans cette méthode, on référence l'objet Me, on référence en fait l'objet p1. La méthode initialise aurait aussi pu être écrite comme suit :


    Public Sub initialise(ByVal P As String, ByVal N As String, ByVal age As Integer)
        prenom = P
        nom = N
        Me.age = age
    End Sub    

Lorsqu'une méthode d'un objet référence un attribut A de cet objet, l'écriture Me.A est implicite. On doit l'utiliser explicitement lorsqu'il y a conflit d'identificateurs. C'est le cas de l'instruction :


Me.age=age;

age désigne un attribut de l'objet courant ainsi que le paramètre age reçu par la méthode. Il faut alors lever l'ambiguïté en désignant l'attribut age par Me.age.

3.1.6. Un programme de test

Voici un court programme de test :


Public Class personne
    ' attributs
    Private prenom As String
    Private nom As String
    Private age As Integer

    ' méthode
    Public Sub initialise(ByVal P As String, ByVal N As String, ByVal age As Integer)
        Me.prenom = P
        Me.nom = N
        Me.age = age
    End Sub

    ' méthode
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub
End Class

et les résultats obtenus :

dos>vbc personne1.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dos>personne1
Jean,Dupont,30

3.1.7. Utiliser un fichier de classes compilées (assembly)

On notera que dans l'exemple précédent il y a deux classes dans notre programme de test : les classes personne et test1. Il y a une autre façon de procéder :

  • on compile la classe personne dans un fichier particulier appelé un assemblage (assembly). Ce fichier a une extension .dll

  • on compile la classe test1 en référençant l'assemblage qui contient la classe personne.

Les deux fichiers source deviennent les suivants :

test.vb

Module test1
    Sub Main()
        Dim p1 As New personne
        p1.initialise("Jean", "Dupont", 30)
        p1.identifie()
    End Sub
End module
personne2.vb

' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

Public Class personne
    ' attributs
    Private prenom As String
    Private nom As String
    Private age As Integer

    ' méthode
    Public Sub initialise(ByVal P As String, ByVal N As String, ByVal age As Integer)
        Me.prenom = P
        Me.nom = N
        Me.age = age
    End Sub    'initialise

    ' méthode
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub    'identifie
End Class 'personne

La classe personne est compilée par l'instruction suivante :

dos>vbc /t:library personne2.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573

dos>dir
24/02/2004  16:50                  509 personne2.vb
24/02/2004  16:49                  143 test.vb
24/02/2004  16:50                3 584 personne2.dll

La compilation a produit un fichier personne2.dll. C'est l'option de compilation /t:library qui indique de produire un fichier "assembly". Maintenant compilons le fichier test.vb :

dos>vbc /r:personne2.dll test.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dos>dir
24/02/2004  16:50                  509 personne2.vb
24/02/2004  16:49                  143 test.vb
24/02/2004  16:50                3 584 personne2.dll
24/02/2004  16:51                3 072 test.exe

L'option de compilation /r:personne2.dll indique au compilateur qu'il trouvera certaines classes dans le fichier personne2.dll. Lorsque dans le fichier source test.vb, il trouvera une référence à la classe personne classe non déclarée dans le source test.vb, il cherchera la classe personne dans les fichiers .dll référencés par l'option /r. Il trouvera ici la classe personne dans l'assemblage personne2.dll. On aurait pu mettre dans cet assemblage d'autres classes. Pour utiliser lors de la compilation plusieurs fichiers de classes compilées, on écrira :

vbc /r:fic1.dll /r:fic2.dll ... fichierSource.vb

L'exécution du programme test1.exe donne les résultats suivants :

dos>test
Jean,Dupont,30

3.1.8. Une autre méthode initialise

Considérons toujours la classe personne et rajoutons-lui la méthode suivante :


    ' méthode
    Public Sub initialise(ByVal P As personne)
        prenom = P.prenom
        nom = P.nom
        Me.age = P.age
    End Sub

On a maintenant deux méthodes portant le nom initialise : c'est légal tant qu'elles admettent des paramètres différents. C'est le cas ici. Le paramètre est maintenant une référence P à une personne. Les attributs de la personne P sont alors affectés à l'objet courant (Me). On remarquera que la méthode initialise a un accès direct aux attributs de l'objet P bien que ceux-ci soient de type private. C'est toujours vrai : un objet O1 d'une classe C a toujours accès aux attributs des objets de la même classe C. Voici un test de la nouvelle classe personne, celle-ci ayant été compilée dans personne.dll comme il a été expliqué précédemment :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

Module test1
    Sub Main()
        Dim p1 As New personne
        p1.initialise("Jean", "Dupont", 30)
        Console.Out.Write("p1=")
        p1.identifie()
        Dim p2 As New personne
        p2.initialise(p1)
        Console.Out.Write("p2=")
        p2.identifie()
    End Sub
End Module

et ses résultats :

p1=Jean,Dupont,30
p2=Jean,Dupont,30

3.1.9. Constructeurs de la classe personne

Un constructeur est une procédure qui porte le nom New et qui est appelée lors de la création de l'objet. On s'en sert généralement pour l'initialiser. Si une classe a un constructeur acceptant n arguments argi, la déclaration et l'initialisation d'un objet de cette classe pourra se faire sous la forme :


        dim objet as classe =new classe(arg1,arg2, ... argn)

ou


        dim objet as classe

        objet=new classe(arg1,arg2, ... argn)

Lorsqu'une classe a un ou plusieurs constructeurs, l'un de ces constructeurs doit être obligatoirement utilisé pour créer un objet de cette classe. Si une classe C n'a aucun constructeur, elle en a un par défaut qui est le constructeur sans paramètres : public New(). Les attributs de l'objet sont alors initialisés avec des valeurs par défaut. C'est ce qui s'est passé dans les programmes précédents, où on avait écrit :

    dim p1 as personne
    p1=new personne

Créons deux constructeurs à notre classe personne :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' la classe personne
Public Class personne
    ' attributs
    Private prenom As String
    Private nom As String
    Private age As Integer

    ' constructeurs
    Public Sub New(ByVal P As [String], ByVal N As [String], ByVal age As Integer)
        initialise(P, N, age)
    End Sub

    Public Sub New(ByVal P As personne)
        initialise(P)
    End Sub

    ' méthodes d'initialisation de l'objet
    Public Sub initialise(ByVal P As String, ByVal N As String, ByVal age As Integer)
        Me.prenom = P
        Me.nom = N
        Me.age = age
    End Sub

    Public Sub initialise(ByVal P As personne)
        prenom = P.prenom
        nom = P.nom
        Me.age = P.age
    End Sub

    ' méthode
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub
End Class

Nos deux constructeurs se contentent de faire appel aux méthodes initialise correspondantes. On rappelle que lorsque dans un constructeur, on trouve la notation initialise(P) par exemple, le compilateur traduit par Me.initialise(P). Dans le constructeur, la méthode initialise est donc appelée pour travailler sur l'objet référencé par Me, c'est à dire l'objet courant, celui qui est en cours de construction. Voici un court programme de test :


' options 
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' pg de test
Module test
    Sub Main()
        Dim p1 As New personne("Jean", "Dupont", 30)
        Console.Out.Write("p1=")
        p1.identifie()
        Dim p2 As New personne(p1)
        Console.Out.Write("p2=")
        p2.identifie()
    End Sub
End Module

et les résultats obtenus :

p1=Jean,Dupont,30
p2=Jean,Dupont,30

3.1.10. Les références d'objets

Nous utilisons toujours la même classe personne. Le programme de test devient le suivant :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' pg de test
Module test
    Sub Main()
        ' p1
        Dim p1 As New personne("Jean", "Dupont", 30)
        Console.Out.Write("p1=")
        p1.identifie()

        ' p2 référence le même objet que p1
        Dim p2 As personne = p1
        Console.Out.Write("p2=")
        p2.identifie()

        ' p3 référence un objet qui sera une copie de l'objet référencé par p1
        Dim p3 As New personne(p1)
        Console.Out.Write("p3=")
        p3.identifie()

        ' on change l'état de l'objet référencé par p1
        p1.initialise("Micheline", "Benoît", 67)
        Console.Out.Write("p1=")
        p1.identifie()

        ' comme p2=p1, l'objet référencé par p2 a du changer d'état
        Console.Out.Write("p2=")
        p2.identifie()

        ' comme p3 ne référence pas le même objet que p1, l'objet référencé par p3 n'a pas du changer
        Console.Out.Write("p3=")
        p3.identifie()
    End Sub
End Module

Les résultats obtenus sont les suivants :

p1=Jean,Dupont,30
p2=Jean,Dupont,30
p3=Jean,Dupont,30
p1=Micheline,Benoît,67
p2=Micheline,Benoît,67
p3=Jean,Dupont,30

Lorsqu'on déclare la variable p1 par

dim p1 as personne=new personne("Jean","Dupont",30)

p1 référence l'objet personne("Jean","Dupont",30) mais n'est pas l'objet lui-même. En C, on dirait que c'est un pointeur, c.a.d. l'adresse de l'objet créé. Si on écrit ensuite :

    p1=nothing

Ce n'est pas l'objet personne("Jean","Dupont",30) qui est modifié, c'est la référence p1 qui change de valeur. L'objet personne("Jean","Dupont",30) sera "perdu" s'il n'est référencé par aucune autre variable.

Lorsqu'on écrit :

dim p2 as personne=p1

on initialise le pointeur p2 : il "pointe" sur le même objet (il désigne le même objet) que le pointeur p1. Ainsi si on modifie l'objet "pointé" (ou référencé) par p1, on modifie celui référencé par p2.

Lorsqu'on écrit :

dim p3 as personne =new personne(p1);

il y a création d'un nouvel objet, copie de l'objet référencé par p1. Ce nouvel objet sera référencé par p3. Si on modifie l'objet "pointé" (ou référencé) par p1, on ne modifie en rien celui référencé par p3. C'est ce que montrent les résultats obtenus.

3.1.11. Les objets temporaires

Dans une expression, on peut faire appel explicitement au constructeur d'un objet : celui-ci est construit, mais nous n'y avons pas accès (pour le modifier par exemple). Cet objet temporaire est construit pour les besoins d'évaluation de l'expression puis abandonné. L'espace mémoire qu'il occupait sera automatiquement récupéré ultérieurement par un programme appelé "ramasse-miettes" dont le rôle est de récupérer l'espace mémoire occupé par des objets qui ne sont plus référencés par des données du programme. Considérons le nouveau programme de test suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' pg de test
Module test
    Sub Main()
        Dim p As New personne(New personne("Jean", "Dupont", 30))
        p.identifie()
    End Sub
End Module

et modifions les constructeurs de la classe personne afin qu'ils affichent un message :


    ' constructeurs
    Public Sub New(ByVal P As [String], ByVal N As [String], ByVal age As Integer)
        Console.Out.WriteLine("Constructeur personne(String, String, integer)")
        initialise(P, N, age)
    End Sub

    Public Sub New(ByVal P As personne)
        Console.Out.WriteLine("Constructeur personne(personne)")
        initialise(P)
    End Sub

Nous obtenons les résultats suivants :

dos>test
Constructeur personne(String, String, integer)
Constructeur personne(personne)
Jean,Dupont,30

montrant la construction successive des deux objets temporaires.

3.1.12. Méthodes de lecture et d'écriture des attributs privés

Nous rajoutons à la classe personne les méthodes nécessaires pour lire ou modifier l'état des attributs des objets :


Imports System

Public Class personne

    ' attributs
    Private prenom As [String]
    Private nom As [String]
    Private age As Integer

    ' constructeurs
    Public Sub New(ByVal P As [String], ByVal N As [String], ByVal age As Integer)
        Me.prenom = P
        Me.nom = N
        Me.age = age
    End Sub

    Public Sub New(ByVal P As personne)
        Me.prenom = P.prenom
        Me.nom = P.nom
        Me.age = P.age
    End Sub

    ' identifie
    Public Sub identifie()
        Console.Out.WriteLine((prenom + "," + nom + "," + age))
    End Sub

    ' accesseurs
    Public Function getPrenom() As [String]
        Return prenom
    End Function

    Public Function getNom() As [String]
        Return nom
    End Function

    Public Function getAge() As Integer
        Return age
    End Function

    'modifieurs
    Public Sub setPrenom(ByVal P As [String])
        Me.prenom = P
    End Sub

    Public Sub setNom(ByVal N As [String])
        Me.nom = N
    End Sub

    Public Sub setAge(ByVal age As Integer)
        Me.age = age
    End Sub
End Class

Nous testons la nouvelle classe avec le programme suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' pg de test
Public Module test
    Sub Main()
        Dim P As New personne("Jean", "Michelin", 34)
        Console.Out.WriteLine(("P=(" & P.getPrenom() & "," & P.getNom() & "," & P.getAge() & ")"))
        P.setAge(56)
        Console.Out.WriteLine(("P=(" & P.getPrenom() & "," & P.getNom() & "," & P.getAge() & ")"))
    End Sub
End Module

et nous obtenons les résultats suivants :

P=(Jean,Michelin,34)
P=(Jean,Michelin,56)

3.1.13. Les propriétés

Il existe une autre façon d'avoir accès aux attributs d'une classe c'est de créer des propriétés. Celles-ci nous permettent de manipuler des attributs privés comme s'ils étaient publics. Considérons la classe personne suivante où les accesseurs et modifieurs précédents ont été remplacés par des propriétés en lecture et écriture :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' classe personne
Public Class personne

    ' attributs
    Private _prenom As [String]
    Private _nom As [String]
    Private _age As Integer


    ' constructeurs
    Public Sub New(ByVal P As [String], ByVal N As [String], ByVal age As Integer)
        Me._prenom = P
        Me._nom = N
        Me._age = age
    End Sub


    Public Sub New(ByVal P As personne)
        Me._prenom = P._prenom
        Me._nom = P._nom
        Me._age = P._age
    End Sub

    ' identifie
    Public Sub identifie()
        Console.Out.WriteLine((_prenom & "," & _nom & "," & _age))
    End Sub

    ' propriétés
    Public Property prenom() As String
        Get
            Return _prenom
        End Get
        Set(ByVal Value As String)
            _prenom = Value
        End Set
    End Property

    Public Property nom() As String
        Get
            Return _nom
        End Get
        Set(ByVal Value As String)
            _nom = Value
        End Set
    End Property

    Public Property age() As Integer
        Get
            Return _age
        End Get
        Set(ByVal Value As Integer)
            ' age valide ?
            If Value >= 0 Then
                _age = Value
            Else
                Throw New Exception("âge (" & Value & ") invalide")
            End If
        End Set
    End Property
End Class

Une propriété Property permet de lire (get) ou de fixer (set) la valeur d'un attribut. Dans notre exemple, nous avons préfixé les noms des attributs du signe _ afin que les propriétés portent le nom des attributs primitifs. En effet, une propriété ne peut porter le même nom que l'attribut qu'elle gère car alors il y a un conflit de noms dans la classe. Nous avons donc appelé nos attributs _prenom, _nom, _age et modifié les constructeurs et méthodes en conséquence. Nous avons ensuite créé trois propriétés nom, prenom et age. Une propriété est déclarée comme suit :


    Public Property nom() As Type
        Get
...
        End Get
        Set(ByVal Value As Type)
...
        End Set    
    End Property

Type doit être le type de l'attribut géré par la propriété. Elle peut avoir deux méthodes appelées get et set. La méthode get est habituellement chargée de rendre la valeur de l'attribut qu'elle gère (elle pourrait rendre autre chose, rien ne l'empêche). La méthode set reçoit un paramètre appelé value qu'elle affecte normalement à l'attribut qu'elle gère. Elle peut en profiter pour faire des vérifications sur la validité de la valeur reçue et éventuellement lancer un exception si la valeur se révèle invalide. C'est ce qui est fait ici pour l'âge.

Comment ces méthodes get et set sont-elles appelées ? Considérons le programme de test suivant :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' pg de test
Module test
    Sub Main()
        Dim P As New personne("Jean", "Michelin", 34)
        Console.Out.WriteLine(("P=(" & P.prenom & "," & P.nom & "," & P.age & ")"))
        P.age = 56
        Console.Out.WriteLine(("P=(" & P.prenom & "," & P.nom & "," & P.age & ")"))
        Try
            P.age = -4
        Catch ex As Exception
            Console.Error.WriteLine(ex.Message)
        End Try
    End Sub
End Module

Dans l'instruction

        Console.Out.WriteLine(("P=(" & P.prenom & "," & P.nom & "," & P.age & ")"))

on cherche à avoir les valeurs des propriétés prenom, nom et age de la personne P. C'est la méthode get de ces propriétés qui est alors appelée et qui rend la valeur de l'attribut qu'elles gèrent.

Dans l'instruction

        P.age = 56

on veut fixer la valeur de la propriété age. C'est alors la méthode set de cette propriété qui est alors appelée. Elle recevra 56 dans son paramètre value.

Une propriété P d'une classe C qui ne définirait que la méthode get est dite en lecture seule. Si c est un objet de classe C, l'opération c.P=valeur sera alors refusée par le compilateur.

L'exécution du programme de test précédent donne les résultats suivants :

P=(Jean,Michelin,34)
P=(Jean,Michelin,56)
âge (-4) invalide

Les propriétés nous permettent donc de manipuler des attributs privés comme s'ils étaient publics.

3.1.14. Les méthodes et attributs de classe

Supposons qu'on veuille compter le nombre d'objets [personne] créées dans une application. On peut soi-même gérer un compteur mais on risque d'oublier les objets temporaires qui sont créés ici ou là. Il semblerait plus sûr d'inclure dans les constructeurs de la classe [personne], une instruction incrémentant un compteur. Le problème est de passer une référence de ce compteur afin que le constructeur puisse l'incrémenter : il faut leur passer un nouveau paramètre. On peut aussi inclure le compteur dans la définition de la classe. Comme c'est un attribut de la classe elle-même et non d'un objet particulier de cette classe, on le déclare différemment avec le mot clé Shared :

    Private Shared _nbPersonnes As Long = 0

Pour le référencer, on écrit personne._nbPersonnes pour montrer que c'est un attribut de la classe personne elle-même. Ici, nous avons créé un attribut privé auquel on n'aura pas accès directement en-dehors de la classe. On crée donc une propriété publique pour donner accès à l'attribut de classe nbPersonnes. Pour rendre la valeur de nbPersonnes la méthode get de cette propriété n'a pas besoin d'un objet personne particulier : en effet _nbPersonnes n'est pas l'attribut d'un objet particulier, il est l'attribut de toute une classe. Aussi a-t-on besoin d'une propriété déclarée elle-aussi Shared :

    Public Shared ReadOnly Property nbPersonnes() As Long
        Get
            Return _nbPersonnes
        End Get
    End Property

qui de l'extérieur sera appelée avec la syntaxe personne.nbPersonnes. La propriété est déclarée en lecture seule (ReadOnly) car elle ne propose pas de méthode set. Voici un exemple. La classe personne devient la suivante :


Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' classe
Public Class personne
    ' attributs de classe
    Private Shared _nbPersonnes As Long = 0

    ' attributs d'instance
    Private _prenom As [String]
    Private _nom As [String]
    Private _age As Integer

    ' constructeurs
    Public Sub New(ByVal P As [String], ByVal N As [String], ByVal age As Integer)
        ' une personne de plus
        _nbPersonnes += 1
        Me._prenom = P
        Me._nom = N
        Me._age = age
    End Sub

    Public Sub New(ByVal P As personne)
        ' une personne de plus
        _nbPersonnes += 1
        Me._prenom = P._prenom
        Me._nom = P._nom
        Me._age = P._age
    End Sub

    ' identifie
    Public Sub identifie()
        Console.Out.WriteLine((_prenom & "," & _nom & "," & _age))
    End Sub

    ' propriété de classe
    Public Shared ReadOnly Property nbPersonnes() As Long
        Get
            Return _nbPersonnes
        End Get
    End Property

    ' propriétés d'instance
    Public Property prenom() As String
        Get
            Return _prenom
        End Get
        Set(ByVal Value As String)
            _prenom = Value
        End Set
    End Property

    Public Property nom() As String
        Get
            Return _nom
        End Get
        Set(ByVal Value As String)
            _nom = Value
        End Set
    End Property

    Public Property age() As Integer
        Get
            Return _age
        End Get
        Set(ByVal Value As Integer)
            ' age valide ?
            If Value >= 0 Then
                _age = Value
            Else
                Throw New Exception("âge (" & Value & ") invalide")
            End If
        End Set
    End Property
End Class

Avec le programme suivant :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' pg de test
Module test
    Sub Main()
        Dim p1 As New personne("Jean", "Dupont", 30)
        Dim p2 As New personne(p1)
        Console.Out.WriteLine(("Nombre de personnes créées : " & personne.nbPersonnes))
    End Sub
End Module

on obtient les résultats suivants :

    Nombre de personnes créées : 2

3.1.15. Passage d'un objet à une fonction

Nous avons déjà dit que par défaut VB.NET passait les paramètres effectifs d'une fonction par valeur : les valeurs des paramètres effectifs sont recopiées dans les paramètres formels. Dans le cas d'un objet, il ne faut pas se laisser tromper par l'abus de langage qui est fait systématiquement en parlant d'objet au lieu de référence d'objet. Un objet n'est manipulé que via une référence (un pointeur) sur lui. Ce qui est donc transmis à une fonction, n'est pas l'objet lui-même mais une référence sur cet objet. C'est donc la valeur de la référence et non la valeur de l'objet lui-même qui est dupliquée dans le paramètre formel : il n'y a pas construction d'un nouvel objet. Si une référence d'objet R1 est transmise à une fonction, elle sera recopiée dans le paramètre formel correspondant R2. Aussi les références R2 et R1 désignent-elles le même objet. Si la fonction modifie l'objet pointé par R2, elle modifie évidemment celui référencé par R1 puisque c'est le même.

C'est ce que montre l'exemple suivant :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' pg de test
Module test
    Sub Main()
        ' une personne p1
        Dim p1 As New personne("Jean", "Dupont", 30)

        ' affichage p1
        Console.Out.Write("Paramètre effectif avant modification : ")
        p1.identifie()

        ' modification p1
        modifie(p1)

        ' affichage p1
        Console.Out.Write("Paramètre effectif après modification : ")
        p1.identifie()
    End Sub

    Sub modifie(ByVal P As personne)
        ' affichage personne P
        Console.Out.Write("Paramètre formel avant modification : ")
        P.identifie()
        ' modification P
        P.prenom = "Sylvie"
        P.nom = "Vartan"
        P.age = 52
        ' affichage P
        Console.Out.Write("Paramètre formel après modification : ")
        P.identifie()
    End Sub
End Module

Les résultats obtenus sont les suivants :

Paramètre effectif avant modification : Jean,Dupont,30
Paramètre formel avant modification : Jean,Dupont,30
Paramètre formel après modification : Sylvie,Vartan,52
Paramètre effectif après modification : Sylvie,Vartan,52

On voit qu'il n'y a construction que d'un objet : celui de la personne p1 de la procédure Main et que l'objet a bien été modifié par la fonction modifie.

3.1.16. Un tableau de personnes

Un objet est une donnée comme une autre et à ce titre plusieurs objets peuvent être rassemblés dans un tableau :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System

' pg de test
Module test
    Sub Main()
        ' un tableau de personnes
        Dim amis(2) As personne
        amis(0) = New personne("Jean", "Dupont", 30)
        amis(1) = New personne("Sylvie", "Vartan", 52)
        amis(2) = New personne("Neil", "Armstrong", 66)
        ' affichage
        Console.Out.WriteLine("----------------")
        Dim i As Integer
        For i = 0 To amis.Length - 1
            amis(i).identifie()
        Next i
    End Sub
End Module

L'instruction Dim amis(2) As personne crée un tableau de 3 éléments de type personne. Ces 3 éléments sont initialisés ici avec la valeur nothing, c.a.d. qu'ils ne référencent aucun objet. De nouveau, par abus de langage, on parle de tableau d'objets alors que ce n'est qu'un tableau de références d'objets. La création du tableau d'objets, qui est un objet lui-même ne crée aucun objet du type de ses éléments : il faut le faire ensuite. On obtient les résultats suivants :

Jean,Dupont,30
Sylvie,Vartan,52
Neil,Armstrong,66

3.2. L'héritage par l'exemple

3.2.1. Généralités

Nous abordons ici la notion d'héritage. Le but de l'héritage est de "personnaliser" une classe existante pour qu'elle satisfasse à nos besoins. Supposons qu'on veuille créer une classe enseignant : un enseignant est une personne particulière. Il a des attributs qu'une autre personne n'aura pas : la matière qu'il enseigne par exemple. Mais il a aussi les attributs de toute personne : prénom, nom et âge. Un enseignant fait donc pleinement partie de la classe personne mais a des attributs supplémentaires. Plutôt que d'écrire une classe enseignant à partir de rien, on préfèrerait reprendre l'acquis de la classe personne qu'on adapterait au caractère particulier des enseignants. C'est le concept d'héritage qui nous permet cela. Pour exprimer que la classe enseignant hérite des propriétés de la classe personne, on écrira :


Public Class enseignant
    Inherits personne

On notera la syntaxe particulière sur deux lignes. La classe personne est appelée la classe parent (ou mère) et la classe enseignant la classe dérivée (ou fille). Un objet enseignant a toutes les qualités d'un objet personne : il a les mêmes attributs et les mêmes méthodes. Ces attributs et méthodes de la classe parent ne sont pas répétées dans la définition de la classe fille : on se contente d'indiquer les attributs et méthodes rajoutés par la classe fille. Nous supposons que la classe personne est définie comme suit :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' classe personne
Public Class personne

    ' attributs de classe
    Private Shared _nbPersonnes As Long = 0

    ' attributs d'instance
    Private _prenom As [String]
    Private _nom As [String]
    Private _age As Integer


    ' constructeurs
    Public Sub New(ByVal P As [String], ByVal N As [String], ByVal age As Integer)
        ' une personne de plus
        _nbPersonnes += 1
        ' construction
        Me._prenom = P
        Me._nom = N
        Me._age = age
        ' suivi
        Console.Out.WriteLine("Construction personne(string, string, int)")
    End Sub


    Public Sub New(ByVal P As personne)
        ' une personne de plus
        _nbPersonnes += 1
        ' construction
        Me._prenom = P._prenom
        Me._nom = P._nom
        Me._age = P._age
        ' suivi
        Console.Out.WriteLine("Construction personne(string, string, int)")
    End Sub

    ' propriété de classe
    Public Shared ReadOnly Property nbPersonnes() As Long
        Get
            Return _nbPersonnes
        End Get
    End Property

    ' propriétés d'instance
    Public Property prenom() As String
        Get
            Return _prenom
        End Get
        Set(ByVal Value As String)
            _prenom = Value
        End Set
    End Property

    Public Property nom() As String
        Get
            Return _nom
        End Get
        Set(ByVal Value As String)
            _nom = Value
        End Set
    End Property

    Public Property age() As Integer
        Get
            Return _age
        End Get
        Set(ByVal Value As Integer)
            ' age valide ?
            If Value >= 0 Then
                _age = Value
            Else
                Throw New Exception("âge (" & Value & ") invalide")
            End If
        End Set
    End Property

    Public ReadOnly Property identite() As String
        Get
            Return "personne(" & _prenom & "," & _nom & "," & age & ")"
        End Get
    End Property
End Class

La méthode identifie a été remplacée par la propriété identité en lecture seule et qui identifie la personne. Nous créons une classe enseignant héritant de la classe personne :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Public Class enseignant
    Inherits personne
    ' attributs
    Private _section As Integer

    ' constructeur
    Public Sub New(ByVal P As String, ByVal N As String, ByVal age As Integer, ByVal section As Integer)
        MyBase.New(P, N, age)
        Me._section = section
        ' suivi
        Console.Out.WriteLine("Construction enseignant(string,string,int,int)")
    End Sub

    ' propriété section
    Public Property section() As Integer
        Get
            Return _section
        End Get
        Set(ByVal Value As Integer)
            _section = Value
        End Set
    End Property
End Class 

La classe enseignant rajoute aux méthodes et attributs de la classe personne :

  • un attribut section qui est le n° de section auquel appartient l'enseignant dans le corps des enseignants (une section par discipline en gros)
  • un nouveau constructeur permettant d'initialiser tous les attributs d'un enseignant

La déclaration


Public Class enseignant
    Inherits personne

indique que la classe enseignant dérive de la classe personne.

3.2.2. Construction d'un objet enseignant

Le constructeur de la classe enseignant est le suivant :


    ' constructeur
    Public Sub New(ByVal P As String, ByVal N As String, ByVal age As Integer, ByVal section As Integer)
        MyBase.New(P, N, age)
        Me._section = section
        ' suivi
        Console.Out.WriteLine("Construction enseignant(string,string,int,int)")
    End Sub    

La déclaration


    Public Sub New(ByVal P As String, ByVal N As String, ByVal age As Integer, ByVal section As Integer)

déclare que le constructeur reçoit quatre paramètres P, N, age, section. Elle doit en passer trois (P,N,age) à sa classe de base, ici la classe personne. On sait que cette classe a un constructeur personne(string, string, int) qui va permettre de construire une personne avec les paramètres passsés (P,N,age). La classe [enseignant] passe les paramètres (P,N,age) à sa classe de base de la façon suivante :


        MyBase.New(P, N, age)

Une fois la construction de la classe de base terminée, la construction de l'objet enseignant se poursuit par l'exécution du corps du constructeur :


        Me._section = section

En résumé, le constructeur d'une classe dérivée :

  • passe à sa classe de base les paramètres dont elle a besoin pour se construire
  • utilise les autres paramètres pour initialiser les attributs qui lui sont propres

On aurait pu préférer écrire :


    Public Sub New(ByVal P As String, ByVal N As String, ByVal age As Integer, ByVal section As Integer)
        Me._prenom = P
        Me._nom = N
        Me._age = age
        Me._section = section
        ' suivi
        Console.Out.WriteLine("Construction enseignant(string,string,int,int)")
    End Sub    

C'est impossible. La classe personne a déclaré privés (private) ses trois champs _prenom, _nom et _age. Seuls des objets de la même classe ont un accès direct à ces champs. Tous les autres objets, y compris des objets fils comme ici, doivent passer par des méthodes publiques pour y avoir accès. Cela aurait été différent si la classe personne avait déclaré protégés (protected) les trois champs : elle autorisait alors des classes dérivées à avoir un accès direct aux trois champs. Dans notre exemple, utiliser le constructeur de la classe parent était donc la bonne solution et c'est la méthode habituelle : lors de la construction d'un objet fils, on appelle d'abord le constructeur de l'objet parent puis on complète les initialisations propres cette fois à l'objet fils (section dans notre exemple).

Compilons les classes personne et enseignant dans des assemblages :

dos>vbc /t:library personne.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573

dos>vbc /r:personne.dll /t:library enseignant.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573

dos>dir
25/02/2004  10:08                1 828 personne.vb
25/02/2004  10:11                  675 enseignant.vb
25/02/2004  10:12                  223 test.vb
25/02/2004  10:16                4 096 personne.dll
25/02/2004  10:16                3 584 enseignant.dll

On remarquera que pour compiler la classe fille enseignant, il a fallu référencer le fichier personne.dll qui contient la classe personne. Tentons un premier programme de test :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Module test
    Sub Main()
        Console.Out.WriteLine(New enseignant("Jean", "Dupont", 30, 27).identite)
    End Sub
End Module

Ce programme ce contente de créer un objet enseignant (new) et de l'identifier. La classe enseignant n'a pas de méthode identité mais sa classe parent en a une qui de plus est publique : elle devient par héritage une méthode publique de la classe enseignant. Les résultats obtenus sont les suivants :

dos>vbc /r:personne.dll /r:enseignant.dll test.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573

dos>test
Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
personne(Jean,Dupont,30)

On voit que :

  • un objet personne a été construit avant l'objet enseignant
  • l'identité obtenue est celle de l'objet personne

3.2.3. Surcharge d'une méthode ou d'une propriété

Dans l'exemple précédent, nous avons eu l'identité de la partie personne de l'enseignant mais il manque certaines informations propres à la classe enseignant (la section). On est donc amené à écrire une propriété permettant d'identifier l'enseignant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Public Class enseignant
    Inherits personne
    ' attributs
    Private _section As Integer

    ' constructeur
    Public Sub New(ByVal P As String, ByVal N As String, ByVal age As Integer, ByVal section As Integer)
        MyBase.New(P, N, age)
        Me._section = section
        ' suivi
        Console.Out.WriteLine("Construction enseignant(string,string,int,int)")
    End Sub

    ' propriété section
    Public Property section() As Integer
        Get
            Return _section
        End Get
        Set(ByVal Value As Integer)
            _section = Value
        End Set
    End Property

    ' surcharge propriété identité
    Public Shadows ReadOnly Property identite() As String
        Get
            Return "enseignant(" & MyBase.identite & "," & _section & ")"
        End Get
    End Property
End Class

La méthode identite de la classe enseignant s'appuie sur la méthode identite de sa classe mère (MyBase.identite) pour afficher sa partie "personne" puis complète avec le champ _section qui est propre à la classe enseignant. Notons la déclaration de la propriété identite :


    Public Shadows ReadOnly Property identite() As String

qui indique que la propriété identite "cache" la méthode de même nom qui pourrait exister dans la classe parent. Soit un objet enseignant E. Cet objet contient en son sein un objet personne :

La propriété identité est définie à la fois dans la classe enseignant et sa classe mère personne. Dans la classe fille enseignant, la propriété identite doit être précédée du mot clé shadows pour indiquer qu'on redéfinit une nouvelle propriété identite pour la classe enseignant.


    Public Shadows ReadOnly Property identite() As String

La classe enseignant dispose maintenant de deux propriétés identite :

  • celle héritée de la classe parent personne
  • la sienne propre

Si E est un ojet enseignant, E.identite désigne la méthode identite de la classe enseignant. On dit que la propriété identite de la classe mère est "redéfinie" par la propriété identite de la classe fille. De façon générale, si O est un objet et M une méthode, pour exécuter la méthode O.M, le système cherche une méthode M dans l'ordre suivant :

  • dans la classe de l'objet O
  • dans sa classe mère s'il en a une
  • dans la classe mère de sa classe mère si elle existe
  • etc…

L'héritage permet donc de redéfinir dans la classe fille des méthodes/propriétés de même nom dans la classe mère. C'est ce qui permet d'adapter la classe fille à ses propres besoins. Associée au polymorphisme que nous allons voir un peu plus loin, la surcharge de méthodes/propriétés est le principal intérêt de l'héritage. Considérons le même exemple que précédemment :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Module test
    Sub Main()
        Console.Out.WriteLine(New enseignant("Jean", "Dupont", 30, 27).identite)
    End Sub
End Module

Les résultats obtenus sont cette fois les suivants :

Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
enseignant(personne(Jean,Dupont,30),27)

3.2.4. Le polymorphisme

Considérons une lignée de classes : C0  C1  C2  … Cn où Ci Cj indique que la classe Cj est dérivée de la classe Ci. Cela entraîne que la classe Cj a toutes les caractéristiques de la classe Ci plus d'autres. Soient des objets Oi de type Ci. Il est légal d'écrire :

    Oi=Oj avec j>i

En effet, par héritage, la classe Cj a toutes les caractéristiques de la classe Ci plus d'autres. Donc un objet Oj de type Cj contient en lui un objet de type Ci. L'opération

Oi=Oj

fait que Oi est une référence à l'objet de type Ci contenu dans l'objet Oj.

Le fait qu'une variable Oi de classe Ci puisse en fait référencer non seulement un objet de la classe Ci mais en fait tout objet dérivé de la classe Ci est appelé polymorphisme : la faculté pour une variable de référencer différents types d'objets. Prenons un exemple et considérons la fonction suivante indépendante de toute classe :


    Sub affiche(ByVal p As personne)

On pourra aussi bien écrire

    dim p as personne
    ...
    affiche(p);

que

    dim e as enseignant
    ...
    affiche(e);

Dans ce dernier cas, le paramètre formel de type personne de la fonction affiche va recevoir une valeur de type enseignant. Comme le type enseignant dérive du type personne, c'est légal.

3.2.5. Redéfinition et polymorphisme

Complétons notre procédure affiche :


    Sub affiche(ByVal p As personne)
        ' affiche identité de p
        Console.Out.WriteLine(p.identite)
    End Sub    

La méthode p.identite rend une chaîne de caractères identifiant l'objet personne. Que se passe-t-il dans le cas de notre exemple précédent dans le cas d'un objet enseignant :


        Dim e As New enseignant("Lucile", "Dumas", 56, 61)
        affiche(e)

Regardons l'exemple suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Module test
    Sub Main()
        ' un enseignant
        Dim e As New enseignant("Lucile", "Dumas", 56, 61)
        affiche(e)
        ' une personne
        Dim p As New personne("Jean", "Dupont", 30)
        affiche(p)
    End Sub

    ' affiche
    Sub affiche(ByVal p As personne)
        ' affiche identité de p
        Console.Out.WriteLine(p.identite)
    End Sub
End Module

Les résultats obtenus sont les suivants :

Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
personne(Lucile,Dumas,56)
Construction personne(string, string, int)
personne(Jean,Dupont,30)

L'exécution montre que l'instruction p.identite a exécuté à chaque fois la propriété identite d'une personne, la personne contenue dans l'enseignant e, puis la personne p elle-même. Elle ne s'est pas adaptée à l'objet réellement passé en paramètre à affiche. On aurait préféré avoir l'identité complète de l'enseignant e. Il aurait fallu pour cela que la notation p.identite référence la propriété identite de l'objet réellement pointé par p plutôt que la propriété identite de partie "personne" de l'objet réellement pointé par p. Il est possible d'obtenir ce résultat en déclarant identite comme une propriété redéfinissable (overridable) dans la classe de base personne :


    Public Overridable ReadOnly Property identite() As String
        Get
            Return "personne(" & _prenom & "," & _nom & "," & age & ")"
        End Get
    End Property

Le mot clé overridable fait de identite une propriété réfinissable ou virtuelle. Ce mot clé peut s'appliquer également aux méthodes. Les classes filles qui redéfinissent une propriété ou méthode virtuelle doivent utiliser alors le mot clé overrides au lieu de shadows pour qualifier leur propriété/méthode redéfinie. Ainsi dans la classe enseignant, la propriété identite est définie comme suit :


    ' surcharge propriété identité
    Public Overrides ReadOnly Property identite() As String
        Get
            Return "enseignant(" & MyBase.identite & "," & _section & ")"
        End Get
    End Property

Le programme de test :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Module test
    Sub Main()
        ' un enseignant
        Dim e As New enseignant("Lucile", "Dumas", 56, 61)
        affiche(e)
        ' une personne
        Dim p As New personne("Jean", "Dupont", 30)
        affiche(p)
    End Sub

    ' affiche
    Sub affiche(ByVal p As personne)
        ' affiche identité de p
        Console.Out.WriteLine(p.identite)
    End Sub
End Module

produit alors les résultats suivants :

Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
enseignant(personne(Lucile,Dumas,56),61)
Construction personne(string, string, int)
personne(Jean,Dupont,30)

Cette fois-ci, on a bien eu l'identité complète de l'enseignant. Redéfinissons maintenant une méthode plutôt qu'une propriété. La classe object est la classe "mère" de toutes les classes VB.NET. Ainsi lorsqu'on écrit :

    public class personne

on écrit implicitement :

    public class personne 
         inherits object

La classe object définit une méthode virtuelle ToString :

Image

La méthode ToString rend le nom de la classe à laquelle appartient l'objet comme le montre l'exemple suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Module test2
    Sub Main()
        ' un enseignant
        Console.Out.WriteLine(New enseignant("Lucile", "Dumas", 56, 61).ToString())
        ' une personne
        Console.Out.WriteLine(New personne("Jean", "Dupont", 30).ToString())
    End Sub
End Module

Les résultats produits sont les suivants :

Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
enseignant
Construction personne(string, string, int)
personne

On remarquera que bien que nous n'ayons pas redéfini la méthode ToString dans les classes personne et enseignant, on peut cependant constater que la méthode ToString de la classe object est quand même capable d'afficher le nom réel de la classe de l'objet. Redéfinissons la méthode ToString dans les classes personne et enseignant :


    ' ToString
    Public Overrides Function ToString() As String
        ' on rend la propriété identite
        Return identite
    End Function

La définition est la même dans les deux classes. Considérons le programme de test suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Module test
    Sub Main()
        ' un enseignant
        Dim e As New enseignant("Lucile", "Dumas", 56, 61)
        affiche(e)
        ' une personne
        Dim p As New personne("Jean", "Dupont", 30)
        affiche(p)
    End Sub

    ' affiche
    Sub affiche(ByVal p As personne)
        ' affiche identité de p
        Console.Out.WriteLine(p.identite)
    End Sub
End Module

Les résultats d'exécution sont les suivants :

Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
enseignant(personne(Lucile,Dumas,56),61)
Construction personne(string, string, int)
personne(Jean,Dupont,30)

3.3. Définir un indexeur pour une classe

Considérons la classe [ArrayList] prédéfinie dans la plate-forme .NET. Cette classe permet de mémoriser des objets dans une liste. Elle appartient à l'espace de noms [System.Collections]. Dans l'exemple suivant, nous utilisons cette classe pour mémoriser une liste de personnes (au sens large) :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System
Imports System.Collections

' classe listeDePersonnes
Public Class listeDePersonnes
    Inherits ArrayList

    ' pour ajouter une personne à la liste
    Public Overloads Sub Add(ByVal p As personne)
        MyBase.Add(p)
    End Sub

    ' un indexeur
    Default Public Shadows Property Item(ByVal i As Integer) As personne
        Get
            Return CType(MyBase.Item(i), personne)
        End Get
        Set(ByVal Value As personne)
            MyBase.Item(i) = Value
        End Set
    End Property

    ' un autre indexeur
    Default Public Shadows ReadOnly Property Item(ByVal N As String) As Integer
        Get
            ' on recherche la personne de nom N
            Dim i As Integer
            For i = 0 To Count - 1
                If CType(Me(i), personne).nom = N Then
                    Return i
                End If
            Next i
            Return -1
        End Get
    End Property

    ' toString
    Public Overrides Function ToString() As String
        ' rend (él1, él2, ..., éln)
        Dim liste As String = "("
        Dim i As Integer
        ' on parcourt le tableau dynamique
        For i = 0 To (Count - 2)
            liste += "[" + Me(i).ToString + "]" + ","
        Next i        'for
        ' dernier élément
        If Count <> 0 Then
            liste += "[" + Me(i).ToString + "]"
        End If
        liste += ")"
        Return liste
    End Function
End Class

Donnons quelques informations sur certaines propriétés et méthodes de la classe [ArrayList] :

Count
attribut donnant le nombre d'éléments de la liste
Add(Object)
méthode permettant d'ajouter un objet à la liste
Item(Integer i)
méthode donnant l'élément i de la liste

Nous remarquons que pour obtenir l'élément n° i de liste, nous n'avons pas écrit [liste.Item(i)] mais directement [liste(i)], ce qui à priori semble erroné. Ceci est néanmoins possible parce que la classe [ArrayList] définit une propriété par défaut [Item] selon la syntaxe analogue à la suivante :


    Default Public Property Item(ByVal i As Integer) As Object
        Get
...
        End Get
        Set(ByVal Value As personne)
...
        End Set    
    End Property

Lorsque le compilateur rencontre la notation [liste(i)], il cherche si la classe [ArrayList] a défini une propriété avec la signature suivante :


    Default Public Property Proc(ByVal var As Integer) As Type

Ici, il trouvera la procédure [Item]. Il traduira alors l'écriture [liste(i)] en [liste.Item(i)]. On appelle la propriété [Item], la propriété indexée par défaut de la classe [ArrayList]. L'exécution du programme précédent donne les résultats suivants :

dos>vbc /r:personne.dll /r:enseignant.dll lstpersonnes1.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dos>dir
11/03/2004  18:26             3 584 enseignant.dll
12/03/2004  15:34             3 584 lstpersonnes1.exe
12/03/2004  13:39               661 lstpersonnes1.vb
11/03/2004  18:26             4 096 personne.dll
dos>lstpersonnes1
Construction personne(string, string, int)
Construction personne(string, string, int)
Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
personne(paul,chenou,31)
personne(nicole,chenou,11)
enseignant(personne(jacques,sileau,33),61)

Créons une classe appelé [listeDePersonnes] qui serait une liste de personnes, donc une liste particulière qu'il semble naturel de dériver de la classe [ArrayList] :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System
Imports System.Collections

' classe listeDePersonnes
Public Class listeDePersonnes
    Inherits ArrayList

    ' pour ajouter une personne à la liste
    Public Shadows Sub Add(ByVal p As Object)
        ' il faut que p soit une personne
        If Not TypeOf (p) Is personne Then
            Throw New Exception("L'objet ajouté (" + p.ToString + ") n'est pas une personne")
        Else
            MyBase.Add(p)
        End If
    End Sub

    ' un indexeur
    Default Public Shadows Property Index(ByVal i As Integer) As personne
        Get
            Return CType(MyBase.Item(i), personne)
        End Get
        Set(ByVal Value As personne)
            MyBase.Item(i) = Value
        End Set
    End Property

    ' toString
    Public Overrides Function ToString() As String
        ' rend (él1, él2, ..., éln)
        Dim liste As String = "("
        Dim i As Integer
        ' on parcourt le tableau dynamique
        For i = 0 To (Count - 2)
            liste += "[" + Me(i).ToString + "]" + ","
        Next i        'for
        ' dernier élément
        If Count <> 0 Then
            liste += "[" + Me(i).ToString + "]"
        End If
        liste += ")"
        Return liste
    End Function
End Class

La classe présente les méthodes et propriétés suivantes :

ToString
rend une chaîne de caractères "représentant" le contenu de la liste
Add(personne)
méthode permettant d'ajouter une personne à la liste
Item(Integer i)
propriété indexée par défaut donnant la personne i de la liste

Considérons les points nouveaux :

Une nouvelle méthode [Add] est créée.


    ' pour ajouter une personne à la liste
    Public Shadows Sub Add(ByVal p As Object)
        ' il faut que p soit une personne
        If Not TypeOf (p) Is personne Then
            Throw New Exception("L'objet ajouté (" + p.ToString + ") n'est pas une personne")
        Else
            MyBase.Add(p)
        End If
    End Sub

Il en existe déjà une dans la classe parent [ArrayList] avec la même signature, d'où le mot clé [Shadows] pour indiquer que la nouvelle procédure remplace celle de la classe parent. La méthode [Add] de la classe fille vérifie que l'objet ajouté est bien de type [personne] ou dérivé avec la fonction [TypeOf]. Si ce n'est pas le cas, une exception est lancée avec l'instruction [Throw]. Si l'objet ajouté est bien de type [personne], la méthode [Add] de la classe de base est utilisée pour faire l'ajout.

Une propriété indexée est créée :


    ' un indexeur
    Default Public Shadows Property Index(ByVal i As Integer) As personne
        Get
            Return CType(MyBase.Item(i), personne)
        End Get
        Set(ByVal Value As personne)
            MyBase.Item(i) = Value
        End Set
    End Property

Il existe déjà une propriété par défaut appelée [Item] dans la classe de base avec la même signature. Aussi est-on obligé d'utiliser le mot clé [Shadows] pour indiquer que la nouvelle propriété indexée [Index] va "cacher" celle de la classe de base [Item]. On notera que ceci est vrai même si les deux propriétés ne portent pas le même nom. La propriété [Index] permet de référencer la personne n° i de la liste. Elle s'appuie sur la propriété [Item] de la classe de base pour avoir accès à l'élément n° i de l'objet [ArrayList] sous-jacent. Des changements de type sont opérés pour tenir compte que la propriété [Item] travaille avec des éléments de type [Object] alors que la propriété [Index] travaille avec des éléments de type [personne].

Enfin, nous redéfinissons (Overrides) la méthode [ToString] de la classe [ArrayList] :


     ' toString
    Public Overrides Function ToString() As String
        ' rend (él1, él2, ..., éln)
        Dim liste As String = "("
        Dim i As Integer
        ' on parcourt le tableau dynamique
        For i = 0 To (Count - 2)
            liste += "[" + Me(i).ToString + "]" + ","
        Next i        'for
        ' dernier élément
        If Count <> 0 Then
            liste += "[" + Me(i).ToString + "]"
        End If
        liste += ")"
        Return liste
    End Function

Cette méthode rend une chaîne de caractères de la forme "(e1,e2,...,en)" où ei sont les éléments de la liste. On remarquera la notation [Me(i)] qui désigne l'élément n° i de l'objet courant [Me]. C'est la propriété indexée par défaut qui est utilisée. Ainsi [Me(i)] est équivalent à [Me.Index(i)].

Le code de la classe est placé dans le fichier [lstpersonnes2.vb] et compilé :

dos>dir
11/03/2004  18:26             3 584 enseignant.dll
12/03/2004  15:45               970 lstpersonnes2.vb
11/03/2004  18:26             4 096 personne.dll
dos>vbc /r:personne.dll /r:enseignant.dll /t:library lstpersonnes2.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dos>dir
11/03/2004  18:26             3 584 enseignant.dll
12/03/2004  15:50             3 584 lstpersonnes2.dll
12/03/2004  15:45               970 lstpersonnes2.vb
11/03/2004  18:26             4 096 personne.dll

Un programme de test est construit :


' options
Option Explicit On 
Option Strict On

' espaces de noms
Imports System
Imports System.Collections

Module test
    Sub Main()
        ' création d'une liste vide de personnes
        Dim liste As listeDePersonnes = New listeDePersonnes
        ' création de personnes
        Dim p1 As personne = New personne("paul", "chenou", 31)
        Dim p2 As personne = New personne("nicole", "chenou", 11)
        Dim e1 As enseignant = New enseignant("jacques", "sileau", 33, 61)
        ' remplissage de la liste
        liste.Add(p1)
        liste.Add(p2)
        liste.Add(e1)
        ' affichage de la liste
        Console.Out.WriteLine(liste.ToString)
        ' ajout d'un objet différent de personne
        Try
            liste.Add(4)
        Catch e As Exception
            Console.Error.WriteLine(e.Message)
        End Try
    End Sub
End Module

Il est compilé :

dos>vbc /r:personne.dll /r:enseignant.dll /r:lstpersonnes2.dll test.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dos>dir
11/03/2004  18:26             3 584 enseignant.dll
12/03/2004  15:50             3 584 lstpersonnes2.dll
12/03/2004  15:45               970 lstpersonnes2.vb
11/03/2004  18:26             4 096 personne.dll
12/03/2004  15:50             3 584 test.exe
12/03/2004  15:49               623 test.vb

puis exécuté :

dos>test
Construction personne(string, string, int)
Construction personne(string, string, int)
Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
([personne(paul,chenou,31)],[personne(nicole,chenou,11)],[enseignant(personne(jacques,sileau,33),61)])
L'objet ajouté (4) n'est pas une personne

On pourrait vouloir écrire

dim p as personne=l("nom")

où l serait de type [listeDePersonnes]. Ici, on veut indexer la liste l non plus par un n° d'élément mais par un nom de personne. Pour cela on définit une nouvelle propriété indexée par défaut :


    ' un autre indexeur
    Default Public Shadows ReadOnly Property Item(ByVal N As String) As Integer
        Get
            ' on recherche la personne de nom N
            Dim i As Integer
            For i = 0 To Count - 1
                If CType(Me(i), personne).nom = N Then
                    Return i
                End If
            Next i
            Return -1
        End Get
    End Property

La première ligne


    Default Public Shadows ReadOnly Property Index(ByVal N As String) As Integer

indique qu'on crée de nouveau une propriété indexée par défaut. Toutes les propriétés par défaut doivent porter le meme nom, ici [Index]. La nouvelle propriété [Index] indexe la classe listeDePersonnes par une chaîne de caractères N. Le résultat de listeDePersonnes(N) est un entier. Cet entier sera la position dans la liste, de la personne portant le nom N ou -1 si cette personne n'est pas dans la liste. On ne définit que la propriété get interdisant ainsi l'écriture listeDePersonnes ("nom")=valeur qui aurait nécessité la définition de la propriété set. D'où le mot clé [ReadOnly]. Le mot clé [Shadows] est nécessaire pour cacher la propriété par défaut de la classe de base (bien qu'elle n'ait pas la même signature).

Dans le corps du get, on parcourt la liste des personnes à la recherche du nom N passé en paramètre. Si on le trouve en position i, on renvoie i sinon on renvoie -1.

Un nouveau programme de test pourrait être le suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' pg de test
Module test
    Sub Main()
        ' une liste de personnes
        Dim l As New listeDePersonnes
        ' ajout de personnes
        l.Add(New personne("jean", "dumornet", 10))
        l.Add(New personne("pauline", "duchemin", 12))
        ' affichage
        Console.Out.WriteLine(("l=" + l.ToString))
        l.Add(New personne("jacques", "tartifume", 27))
        Console.Out.WriteLine(("l=" + l.ToString))
        ' changement élément 1
        l(1) = New personne("sylvie", "cachan", 5)
        ' affichage élément 1
        Console.Out.WriteLine(("l[1]=" + l(1).ToString))
        ' affichage liste l
        Console.Out.WriteLine(("l=" + l.ToString))
        ' recherche de personnes
        Dim noms() As String = New [String]() {"cachan", "inconnu"}
        Dim i As Integer
        For i = 0 To noms.Length - 1
            Dim inom As Integer = l(noms(i))
            If inom <> -1 Then
                Console.Out.WriteLine(("personne(" & noms(i) & ")=" & l(inom).ToString))
            Else
                Console.Out.WriteLine(("personne(" + noms(i) + ") n'existe pas"))
            End If
        Next i
    End Sub    
End Module

L'exécution donne les résultats suivants :

personne(string, string, int)
Construction personne(string, string, int)
l=([personne(jean,dumornet,10)],[personne(pauline,duchemin,12)])
Construction personne(string, string, int)
l=([personne(jean,dumornet,10)],[personne(pauline,duchemin,12)],[personne(jacques,tartifume,27)])
Construction personne(string, string, int)
l[1]=personne(sylvie,cachan,5)
l=([personne(jean,dumornet,10)],[personne(sylvie,cachan,5)],[personne(jacques,tartifume,27)])
personne(cachan)=personne(sylvie,cachan,5)
personne(inconnu) n'existe pas

3.4. Les structures

La structure VB.NET est directement issue de la structure du langage C et est très proche de la classe. Une structure est définie comme suit :


Structure spersonne
' attributs
    ...
' propriétés
...
' constructeurs
...
' méthodes
End Structure

Il y a, malgré une similitude de déclaration, des différences importantes entre classe et structure. La notion d'héritage n'existe par exemple pas avec les structures. Si on écrit une classe qui ne doit pas être dérivée, quelles sont les différences entre structure et classe qui vont nous aider à choisir entre les deux ? Aidons-nous de l'exemple suivant pour le découvrir :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' structure spersonne
Structure spersonne
    Public nom As String
    Public age As Integer
End Structure

' classe cpersonne
Class cpersonne
    Public nom As String
    Public age As Integer
End Class

' une module de test
Public Module test
    Sub Main()
        ' une spersonne p1
        Dim sp1 As spersonne
        sp1.nom = "paul"
        sp1.age = 10
        Console.Out.WriteLine(("sp1=spersonne(" & sp1.nom & "," & sp1.age & ")"))
        ' une spersonne p2
        Dim sp2 As spersonne = sp1
        Console.Out.WriteLine(("sp2=spersonne(" & sp2.nom & "," & sp2.age & ")"))
        ' sp2 est modifié
        sp2.nom = "nicole"
        sp2.age = 30
        ' vérification sp1 et sp2
        Console.Out.WriteLine(("sp1=cpersonne(" & sp1.nom & "," & sp1.age & ")"))
        Console.Out.WriteLine(("sp2=cpersonne(" & sp2.nom & "," & sp2.age & ")"))

        ' une cpersonne cp1
        Dim cp1 As New cpersonne
        cp1.nom = "paul"
        cp1.age = 10
        Console.Out.WriteLine(("cP1=cpersonne(" & cp1.nom & "," & cp1.age & ")"))
        ' une cpersonne P2
        Dim cp2 As cpersonne = cp1
        Console.Out.WriteLine(("cP2=cpersonne(" & cp2.nom & "," & cp2.age & ")"))
        ' P2 est modifié
        cp2.nom = "nicole"
        cp2.age = 30
        ' vérification P1 et P2
        Console.Out.WriteLine(("cP1=cpersonne(" & cp1.nom & "," & cp1.age & ")"))
        Console.Out.WriteLine(("cP2=cpersonne(" & cp2.nom & "," & cp2.age & ")"))
    End Sub
End Module

Si on exécute ce programme, on obtient les résultats suivants :

sp1=spersonne(paul,10)
sp2=spersonne(paul,10)
sp1=cpersonne(paul,10)
sp2=cpersonne(nicole,30)
cP1=cpersonne(paul,10)
cP2=cpersonne(paul,10)
cP1=cpersonne(nicole,30)
cP2=cpersonne(nicole,30)

Là où dans les pages précédentes de ce chapitre on utilisait une classe personne, nous utilisons maintenant une structure spersonne :


' structure spersonne
Structure spersonne
    Public nom As String
    Public age As Integer
End Structure

La déclaration


        Dim sp1 As spersonne

crée une structure (nom,age) et la valeur de sp1 est cette structure elle-même.

La déclaration


        Dim cp1 As New cpersonne

crée un objet [cpersonne] (grosso modo l'équivalent de notre structure) et cp1 est alors l'adresse (la référence) de cet objet.

Résumons

  • dans le cas de la structure, la valeur de sp1 est la structure elle-même
  • dans le cas de la classe, la valeur de p1 est l'adresse de l'objet créé

Lorsque dans le programme on écrit


        Dim sp2 As spersonne = sp1

une nouvelle structure (nom,age) est créée et initialisée avec la valeur de p1 donc la structure elle-même.

La structure de sp1 est donc dupliquée dans sp2. C'est une recopie de valeur.

L'instruction


        Dim cp2 As cpersonne = cp1

agit différemment. La valeur de cp1 est recopiée dans cp2, mais comme cette valeur est en fait l'adresse de l'objet, celui-ci n'est pas dupliqué. Il a simplement deux références sur lui :

Dans le cas de la structure, si on modifie la valeur de sp2 on ne modifie pas la valeur de sp1, ce que montre le programme. Dans le cas de l'objet, si on modifie l'objet pointé par cp2, celui pointé par cp1 est modifié puisque c'est le même. C'est ce que montrent également les résultats du programme.

On retiendra donc de ces explications que :

  • la valeur d'une variable de type structure est la structure elle-même
  • la valeur d'une variable de type objet est l'adresse de l'objet pointé

Une fois cette différence fondamentale comprise, la structure se montre très proche de la classe comme le montre le nouvel exemple suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' structure personne
Structure personne
    ' attributs
    Private _nom As String
    Private _age As Integer

    ' propriétés
    Public Property nom() As String
        Get
            Return _nom
        End Get
        Set(ByVal Value As String)
            _nom = value
        End Set
    End Property

    Public Property age() As Integer
        Get
            Return _age
        End Get
        Set(ByVal Value As Integer)
            _age = value
        End Set
    End Property

    ' Constructeur
    Public Sub New(ByVal NOM As String, ByVal AGE As Integer)
        _nom = NOM
        _age = AGE
    End Sub    'New

    ' TOSTRING
    Public Overrides Function ToString() As String
        Return "personne(" & nom & "," & age & ")"
    End Function
End Structure

' un module de test
Module test
    Sub Main()
        ' une personne p1
        Dim p1 As New personne("paul", 10)
        Console.Out.WriteLine(("p1=" & p1.ToString))
        ' une personne p2
        Dim p2 As personne = p1
        Console.Out.WriteLine(("p2=" & p2.ToString))
        ' p2 est modifié
        p2.nom = "nicole"
        p2.age = 30
        ' vérification p1 et p2
        Console.Out.WriteLine(("p1=" & p1.ToString))
        Console.Out.WriteLine(("p2=" & p2.ToString))
    End Sub
End Module

On obtient les résultats d'exécution suivants :

p1=personne(paul,10)
p2=personne(paul,10)
p1=personne(paul,10)
p2=personne(nicole,30)

La seule notable différence ici entre structure et classe, c'est qu'avec une classe, les objets p1 et p2 auraient eu la même valeur à la fin du programme, celle de p2.

3.5. Les interfaces

Une interface est un ensemble de prototypes de méthodes ou de propriétés 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 définies dans l'interface. C'est le compilateur qui vérifie cette implémentation. Voici par exemple la définition de l'interface Istats :

Public Interface Istats
    Function moyenne() As Double
    Function écartType() As Double
End Interface

Toute classe implémentant cette interface sera déclarée comme

public class C 
 Implements Istats
...
function moyenne() as Double Implements Istats.moyenne
...
end function
function écartType () as Double Implements Istats. écartType
...
end function
end class

Les méthodes [moyenne] et [écartType] devront être définies dans la classe C. Considérons le code suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' structure
Public Structure élève
    Public _nom As String
    Public _note As Double

    ' constructeur
    Public Sub New(ByVal NOM As String, ByVal NOTE As Double)
        Me._nom = NOM
        Me._note = NOTE
    End Sub
End Structure

' classe notes
Public Class notes
    ' attribut
    Protected _matière As String
    Protected _élèves() As élève

    ' constructeur
    Public Sub New(ByVal MATIERE As String, ByVal ELEVES() As élève)
        ' mémorisation élèves & matière
        Me._matière = MATIERE
        Me._élèves = ELEVES
    End Sub

    ' ToString
    Public Overrides Function ToString() As String
        Dim valeur As String = "matière=" + _matière + ", notes=("
        Dim i As Integer
        ' on concatène toutes les notes
        For i = 0 To (_élèves.Length - 1) - 1
            valeur &= "[" & _élèves(i)._nom & "," & _élèves(i)._note & "],"
        Next i
        'dernière note
        If _élèves.Length <> 0 Then
            valeur &= "[" & _élèves(i)._nom & "," & _élèves(i)._note & "]"
        End If
        valeur += ")"
        ' fin
        Return valeur
    End Function
End Class

La classe notes rassemble les notes d'une classe dans une matière :


Public Class notes
    ' attribut
    Protected _matière As String
    Protected _élèves() As élève

Les attributs sont déclarés protected pour être accessibles d'une classe dérivée. Le type élève est une structure mémorisant le nom de l'élève et sa note dans la matière :


Public Structure élève
    Public _nom As String
    Public _note As Double

    ' constructeur
    Public Sub New(ByVal NOM As String, ByVal NOTE As Double)
        Me._nom = NOM
        Me._note = NOTE
    End Sub    
End Structure

Nous décidons de dériver cette classe notes dans une classe notesStats qui aurait deux attributs supplémentaires, la moyenne et l'écart-type des notes :


Public Class notesStats
    Inherits notes
    Implements Istats

    ' attributs
    Private _moyenne As Double
    Private _écartType As Double

La classe notesStats implémente l'interface Istats suivante :

Public Interface Istats
    Function moyenne() As Double
    Function écartType() As Double
End Interface

Cela signifie que la classe notesStats doit avoir deux méthodes appelées moyenne et écartType avec la signature indiquée dans l'interface Istats. La classe notesStats est la suivante :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Public Class notesStats
    Inherits notes
    Implements Istats

    ' attributs
    Private _moyenne As Double
    Private _écartType As Double

    ' constructeur
    Public Sub New(ByVal MATIERE As String, ByVal ELEVES() As élève)
        MyBase.New(MATIERE, ELEVES)
        ' calcul moyenne des notes
        Dim somme As Double = 0
        Dim i As Integer
        For i = 0 To ELEVES.Length - 1
            somme += ELEVES(i)._note
        Next i
        If ELEVES.Length <> 0 Then
            _moyenne = somme / ELEVES.Length
        Else
            _moyenne = -1
        End If
        ' écart-type
        Dim carrés As Double = 0
        For i = 0 To ELEVES.Length - 1
            carrés += Math.Pow(ELEVES(i)._note - _moyenne, 2)
        Next i
        If ELEVES.Length <> 0 Then
            _écartType = Math.Sqrt((carrés / ELEVES.Length))
        Else
            _écartType = -1
        End If
    End Sub

    ' ToString
    Public Overrides Function ToString() As String
        Return MyBase.ToString() & ",moyenne=" & _moyenne & ",écart-type=" & _écartType
    End Function    'ToString

    ' méthodes de l'interface Istats
    Public Function moyenne() As Double Implements Istats.moyenne
        ' rend la moyenne des notes
        Return _moyenne
    End Function

    Public Function écartType() As Double Implements Istats.écartType
        ' rend l'écart-type
        Return _écartType
    End Function
End Class

La moyenne _moyenne et l'écart-type _ecartType sont calculés dès la construction de l'objet. Aussi les méthodes moyenne et écartType n'ont-elles qu'à rendre la valeur des attributs _moyenne et _ecartType. Les deux méthodes rendent -1 si le tableau des élèves est vide.

La classe de test suivante :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Module test
    Sub Main()
        ' qqs élèves & notes
        Dim ELEVES() As élève = {New élève("paul", 14), New élève("nicole", 16), New élève("jacques", 18)}
        ' qu'on enregistre dans un objet notes
        Dim anglais As New notes("anglais", ELEVES)
        ' et qu'on affiche
        Console.Out.WriteLine((anglais.ToString))
        ' idem avec moyenne et écart-type
        anglais = New notesStats("anglais", ELEVES)
        Console.Out.WriteLine((anglais.ToString))
    End Sub
End Module

donne les résultats :

matière=anglais, notes=([paul,14],[nicole,16],[jacques,18])
matière=anglais, notes=([paul,14],[nicole,16],[jacques,18]),moyenne=16,écart-type=1,63299316185545

La classe notesStats aurait très bien pu implémenter les méthodes moyenne et écartType pour elle-même sans indiquer qu'elle implémentait l'interface Istats. Quel est l'intérêt des interfaces ? C'est le suivant : une fonction peut admettre pour paramètre formel une donnée ayant le type d'une interface I. Tout objet d'une classe C implémentant l'interface I pourra alors être paramètre effectif de cette fonction. Considérons l'exemple suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' une interface Iexemple
Public Interface Iexemple
    Function ajouter(ByVal i As Integer, ByVal j As Integer) As Integer
    Function soustraire(ByVal i As Integer, ByVal j As Integer) As Integer
End Interface

' une 1ère classe
Public Class classe1
    Implements Iexemple

    Public Function ajouter(ByVal a As Integer, ByVal b As Integer) As Integer Implements Iexemple.ajouter
        Return a + b + 10
    End Function

    Public Function soustraire(ByVal a As Integer, ByVal b As Integer) As Integer Implements Iexemple.soustraire
        Return a - b + 20
    End Function
End Class

'une 2ième classe
Public Class classe2
    Implements Iexemple

    Public Function ajouter(ByVal a As Integer, ByVal b As Integer) As Integer Implements Iexemple.ajouter
        Return a + b + 100
    End Function

    Public Function soustraire(ByVal a As Integer, ByVal b As Integer) As Integer Implements Iexemple.soustraire
        Return a - b + 200
    End Function
End Class

L'interface Iexemple définit deux méthodes ajouter et soustraire. Les classes classe1 et classe2 implémentent cette interface. On remarquera que ces classes ne font rien d'autre, ceci par souci de simplification de l'exemple. Maintenant considérons l'exemple suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' classe de test
Module test

    'calculer
    Sub calculer(ByVal i As Integer, ByVal j As Integer, ByVal inter As Iexemple)
        Console.Out.WriteLine(inter.ajouter(i, j))
        Console.Out.WriteLine(inter.soustraire(i, j))
    End Sub

    ' la fonction Main
    Sub Main()
        ' création de deux objets classe1 et classe2
        Dim c1 As New classe1
        Dim c2 As New classe2
        ' appels de la fonction statique calculer
        calculer(4, 3, c1)
        calculer(14, 13, c2)
    End Sub
End Module

La fonction calculer admet pour paramètre un élément de type Iexemple. Elle pourra donc recevoir pour ce paramètre aussi bien un objet de type classe1 que de type classe2. C'est ce qui est fait dans la procédure Main avec les résultats suivants :

17
21
127
201

On voit donc qu'on a là une propriété proche du polymorphisme vu pour les classes. Si un ensemble de classes Ci non liées entre-elles par héritage (donc on ne peut utiliser le polymorphisme de l'héritage) présente un ensemble de méthodes de même signature, il peut être intéressant de regrouper ces méthodes dans une interface I dont hériteraient toutes les classes concernées. Des instances de ces classes Ci peuvent alors être utilisées comme paramètres de fonctions admettant un paramètre de type I, c.a.d. des fonctions n'utilisant que les méthodes des objets Ci définies dans l'interface I et non les attributs et méthodes particuliers des différentes classes Ci. Notons enfin que l'héritage d'interfaces peut être multiple, c.a.d. qu'on peut écrire


Public Class classe
    Implements I1,I2,...

où les Ij sont des interfaces.

3.6. Les espaces de noms

Pour écrire une ligne à l'écran, nous utilisons l'instruction

Console.Out.WriteLine(...)

Si nous regardons la définition de la classe Console


Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)

on découvre qu'elle fait partie de l'espace de noms System. Cela signifie que la classe Console devrait être désignée par System.Console et on devrait en fait écrire :

System.Console.Out.WriteLine(...)

On évite cela en utilisant une clause imports :

imports System
...
Console.Out.WriteLine(...)

On dit qu'on importe l'espace de noms System avec la clause imports. Lorsque le compilateur va rencontrer le nom d'une classe (ici Console) il va chercher à la trouver dans les différents espaces de noms importés par les clauses imports. Ici il trouvera la classe Console dans l'espace de noms System. Notons maintenant la seconde information attachée à la classe Console :


Assembly: Mscorlib (in Mscorlib.dll)

Cette ligne indique dans quelle "assemblage" se trouve la définition de la classe Console. Lorsqu'on compile en-dehors de Visual Studio.NET et qu'on doit donner les références des différentes dll contenant les classes que l'on doit utiliser cette information peut s'avérer utile. On rappelle que pour référencer les dll nécessaires à la compilation d'une classe, on écrit :

vbc /r:fic1.dll /r:fic2.dll ... prog.vb

Lorsqu'on crée une classe, on peut la créer à l'intérieur d'un espace de noms. Le but de ces espaces de noms est d'éviter les conflits de noms entre classes lorsque celles-ci sont vendues par exemple. Considérons deux entreprises E1 et E2 distribuant des classes empaquetées respectivement dans les dll, E1.dll et E2.dll. Soit un client C qui achète ces deux ensembles de classes dans lesquelles les deux entreprises ont défini toutes deux une classe personne. Le client C compile un programme de la façon suivante :

vbc /r:E1.dll /r:E2.dll prog.vb

Si le source prog.vb utilise la classe personne, le compilateur ne saura pas s'il doit prendre la classe personne de E1.dll ou celle de E2.dll. Il signalera une erreur. Si l'entreprise E1 prend soin de créer ses classes dans un espace de noms appelé E1 et l'entreprise E2 dans un espace de noms appelé E2, les deux classes personne s'appelleront alors E1.personne et E2.personne. Le client devra employer dans ses classes soit E1.personne, soit E2.personne mais pas personne. L'espace de noms permet de lever l'ambiguïté. Pour créer une classe dans un espace de noms, on écrit :


Namespace istia.st
    Public Class personne
        ' définition de la classe
...
  end Class
end Namespace

Pour l'exemple, créons dans un espace de noms notre classe personne étudiée précédemment. Nous choisirons istia.st comme espace de noms. La classe personne devient :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

' création de l'espace de nom istia.st
Namespace istia.st
    Public Class personne
        ' attributs
        Private prenom As String
        Private nom As String
        Private age As Integer

        ' méthode
        Public Sub initialise(ByVal P As String, ByVal N As String, ByVal age As Integer)
            Me.prenom = P
            Me.nom = N
            Me.age = age
        End Sub

        ' méthode
        Public Sub identifie()
            Console.Out.WriteLine((prenom & "," & nom & "," & age))
        End Sub
    End Class
End Namespace

Cette classe est compilée dans personne.dll :

dos>dir
11/03/2004  18:27               610 personne.vb
dos>vbc /t:library personne.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dos>dir
12/03/2004  18:06             3 584 personne.dll
11/03/2004  18:27               610 personne.vb

Maintenant utilisons la classe personne dans une classe de test :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System
Imports istia.st

' pg test
Public Module test
    Sub Main()
        Dim p1 As New personne
        p1.initialise("Jean", "Dupont", 30)
        p1.identifie()
    End Sub
End Module

Pour éviter d'écrire


        Dim p1 As New istia.st.personne

nous avons importé l'espace de noms istia.st avec une clause imports :


Imports istia.st

Maintenant compilons le programme de test :

dos>dir
12/03/2004  18:06             3 584 personne.dll
11/03/2004  18:27               610 personne.vb
12/03/2004  18:05               254 test.vb
dos>vbc /r:personne.dll test.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dos>dir
12/03/2004  18:06             3 584 personne.dll
11/03/2004  18:27               610 personne.vb
12/03/2004  18:08             3 072 test.exe
12/03/2004  18:05               254 test.vb

Cela produit un fichier test.exe qui exécuté donne les résultats suivants :

Jean,Dupont,30

3.7. L'exemple IMPOTS

On reprend le calcul de l'impôt déjà étudié dans le chapitre précédent et on le traite en utilisant une classe. Rappelons le problème :

On se place dans le cas simplifié d'un contribuable n'ayant que son seul salaire à déclarer :

  • on calcule le nombre de parts du salarié nbParts=nbEnfants/2 +1 s'il n'est pas marié, nbEnfants/2+2 s'il est marié, où nbEnfants est son nombre d'enfants.
  • s'il a au moins trois enfants, il a une demie part de plus
  • on calcule son revenu imposable R=0.72*S où S est son salaire annuel
  • on calcule son coefficient familial QF=R/nbParts
  • on calcule son impôt I. Considérons le tableau suivant :
12620.0
0
0
13190
0.05
631
15640
0.1
1290.5
24740
0.15
2072.5
31810
0.2
3309.5
39970
0.25
4900
48360
0.3
6898.5
55790
0.35
9316.5
92970
0.4
12106
127860
0.45
16754.5
151250
0.50
23147.5
172040
0.55
30710
195000
0.60
39312
0
0.65
49062

Chaque ligne a 3 champs. Pour calculer l'impôt I, on recherche la première ligne où QF<=champ1. Par exemple, si QF=23000 on trouvera la ligne

    24740        0.15        2072.5

L'impôt I est alors égal à 0.15*R - 2072.5*nbParts. Si QF est tel que la relation QF<=champ1 n'est jamais vérifiée, alors ce sont les coefficients de la dernière ligne qui sont utilisés. Ici :

    0                0.65        49062

ce qui donne l'impôt I=0.65*R - 49062*nbParts.

La classe impot sera définie comme suit :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System

Public Class impot
    ' les données nécessaires au calcul de l'impôt
    ' proviennent d'une source extérieure
    Private limites(), coeffR(), coeffN() As Decimal

    ' constructeur
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' on vérifie que les 3 tableaux ont la même taille
        Dim OK As Boolean = LIMITES.Length = COEFFR.Length And LIMITES.Length = COEFFN.Length
        If Not OK Then
            Throw New Exception("Les 3 tableaux fournis n'ont pas la même taille(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
        End If
        ' c'est bon
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub

    ' calcul de l'impôt
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Long) As Long
        ' calcul du nombre de parts
        Dim nbParts As Decimal
        If marié Then
            nbParts = CDec(nbEnfants) / 2 + 2
        Else
            nbParts = CDec(nbEnfants) / 2 + 1
        End If
        If nbEnfants >= 3 Then
            nbParts += 0.5D
        End If
        ' calcul revenu imposable & Quotient familial
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' calcul de l'impôt
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

Un objet impôt est créé avec les données permettant le calcul de l'impôt d'un contribuable. C'est la partie stable de l'objet. Une fois cet objet créé, on peut appeler de façon répétée sa méthode calculer qui calcule l'impôt du contribuable à partir de son statut marital (marié ou non), son nombre d'enfants et son salaire annuel. Un programme de test pourait être le suivant :


' options
Option Strict On
Option Explicit On 

' espaces de noms
Imports System
Imports Microsoft.VisualBasic

Module test
    Sub Main()
        ' programme interactif de calcul d'impôt
        ' l'utilisateur tape trois données au clavier : marié nbEnfants salaire
        ' le programme affiche alors l'impôt à payer
        Const syntaxe As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

        ' tableaux de données nécessaires au calcul de l'impôt
        Dim limites() As Decimal = {12620D, 13190D, 15640D, 24740D, 31810D, 39970D, 48360D, 55790D, 92970D, 127860D, 151250D, 172040D, 195000D, 0D}
        Dim coeffR() As Decimal = {0D, 0.05D, 0.1D, 0.15D, 0.2D, 0.25D, 0.3D, 0.35D, 0.4D, 0.45D, 0.5D, 0.55D, 0.6D, 0.65D}
        Dim coeffN() As Decimal = {0D, 631D, 1290.5D, 2072.5D, 3309.5D, 4900D, 6898.5D, 9316.5D, 12106D, 16754.5D, 23147.5D, 30710D, 39312D, 49062D}

        ' création d'un objet impôt
        Dim objImpôt As impot = Nothing
        Try
            objImpôt = New impot(limites, coeffR, coeffN)
        Catch ex As Exception
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
            Environment.Exit(1)
        End Try
        ' boucle infinie
        Dim marié As String
        Dim nbEnfants As Integer
        Dim salaire As Long
        While True
            ' on demande les paramètres du calcul de l'impôt
            Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
            Dim paramètres As String = Console.In.ReadLine().Trim()
            ' qq chose à faire ?
            If paramètres Is Nothing OrElse paramètres = "" Then
                Exit While
            End If
            ' vérification du nombre d'arguments dans la ligne saisie
            Dim erreur As Boolean = False
            Dim args As String() = paramètres.Split(Nothing)
            Dim nbParamètres As Integer = args.Length
            If nbParamètres <> 3 Then
                Console.Error.WriteLine(syntaxe)
                erreur = True
            End If
            ' vérification de la validité des paramètres
            If Not erreur Then
                ' marié
                marié = args(0).ToLower()
                If marié <> "o" And marié <> "n" Then
                    erreur = True
                End If
                ' nbEnfants
                Try
                    nbEnfants = Integer.Parse(args(1))
                    If nbEnfants < 0 Then
                        Throw New Exception
                    End If
                Catch
                    erreur = True
                End Try
                ' salaire
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    erreur = True
                End Try
            End If
            ' si les paramètres sont corrects - on calcule l'impôt
            If Not erreur Then
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire) & " F"))
            Else
                Console.Error.WriteLine(syntaxe)
            End If
        End While
    End Sub
End Module

Voici un exemple d'exécution du programme précédent :

dir>dir
12/03/2004  18:20             1 483 impots.vb
12/03/2004  18:21             2 805 test.vb
dos>vbc /t:library impots.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dir>dir
12/03/2004  18:24             4 096 impots.dll
12/03/2004  18:20             1 483 impots.vb
12/03/2004  18:21             2 805 test.vb
dos>vbc /r:impots.dll test.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4 pour Microsoft (R) .NET Framework version 1.1.4322.573
dos>dir
12/03/2004  18:24             4 096 impots.dll
12/03/2004  18:20             1 483 impots.vb
12/03/2004  18:26             6 144 test.exe
12/03/2004  18:21             2 805 test.vb
dos>test
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :x x x
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :