Skip to content

3. Clases, estructuras, interfaces

3.1. El objeto a través del ejemplo

3.1.1. Generalidades

Ahora abordaremos, a través de un ejemplo, la programación orientada a objetos. Un objeto es una entidad que contiene datos que definen su estado (denominados «propiedades») y funciones (denominadas «métodos»). Un objeto se crea según un modelo denominado «clase»:

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

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

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

A partir de la clase C1 anterior, se pueden crear numerosos objetos O1, O2,… Todos tendrán las propiedades p1, p2,… y los métodos m3, m4,… Pero tendrán valores diferentes para sus propiedades pi, por lo que cada uno tendrá un estado propio. Por analogía, la declaración

    dim i, j as integer

crea dos objetos (el término es incorrecto aquí) de tipo (clase) Integer. Su única propiedad es su valor. Si O1 es un objeto de tipo C1, O1.p1 designa la propiedad p1 de O1 y O1.m1 el método m1 de O1. Consideremos un primer modelo de objeto: la clase personne.

3.1.2. Definición de la clase persona

La definición de la clase personne será la siguiente:


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

    ' método
    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étodo
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub
End Class

Aquí tenemos la definición de una clase, es decir, de un tipo de datos. Cuando creemos variables de este tipo, las llamaremos objetos o instancias de clases. Una clase es, por tanto, un molde a partir del cual se construyen los objetos. Los miembros o campos de una clase pueden ser datos (atributos), métodos (funciones) o propiedades. Las propiedades son métodos especiales que sirven para conocer o fijar el valor de los atributos del objeto. Estos campos pueden ir acompañados de una de las tres palabras clave siguientes:

privé
Un campo privado (private) solo es accesible mediante los métodos internos de la clase
public
Un campo público (public) es accesible desde cualquier función, esté definida o no dentro de la clase
protégé
Un campo protegido (protected) solo es accesible mediante los métodos internos de la clase o de un objeto derivado (véase más adelante el concepto de herencia).

En general, los datos de una clase se declaran privados, mientras que sus métodos y propiedades se declaran públicos. Esto significa que el usuario de un objeto (el programador):

  • no tendrá acceso directo a los datos privados del objeto
  • podrá invocar los métodos públicos del objeto y, en particular, aquellos que le den acceso a sus datos privados.

La sintaxis de declaración de una clase es la siguiente:


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

El orden de declaración de los atributos private, protected y public es arbitrario.

3.1.3. El método initialize

Volvamos a nuestra clase [personne] declarada como:


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

    ' método
    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étodo
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub
End Class

¿Cuál es la función del método initialise? Dado que nom, prenom y age son datos privados de la clase personne, las instrucciones:

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

son ilegales. Debemos inicializar un objeto de tipo personne mediante un método público. Esa es la función del método initialise. Escribiremos:

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

La escritura p1.initialise es válida porque initialise es de acceso público.

3.1.4. El operador new

La secuencia de instrucciones

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

es incorrecta. La instrucción

dim p1 as personne

declara p1 como una referencia a un objeto de tipo personne. Este objeto aún no existe y, por lo tanto, p1 no está inicializado. Es como si se escribiera:

dim p1 as personne=nothing

donde se indica explícitamente con la palabra clave nothing que la variable p1 aún no hace referencia a ningún objeto. Cuando luego se escribe

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

se invoca el método initialise del objeto al que hace referencia p1. Sin embargo, este objeto aún no existe y el compilador señalará el error. Para que p1 haga referencia a un objeto, hay que escribir:

dim p1 as personne=new personne()

Esto tiene como efecto la creación de un objeto de tipo personne aún sin inicializar: los atributos nom y prenom, que son referencias a objetos de tipo String, tendrán el valor nothing, y age tendrá el valor 0. Por lo tanto, hay una inicialización por defecto. Ahora que p1 hace referencia a un objeto, la instrucción de inicialización de este objeto

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

es válida.

3.1.5. La palabra clave Me

Veamos el código del método 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    

La instrucción Me.prenom=P significa que el atributo prenom del objeto actual (Me) recibe el valor P. La palabra clave Me designa el objeto actual: aquel en el que se encuentra el método ejecutado. ¿Cómo lo sabemos? Veamos cómo se inicializa el objeto al que hace referencia p1 en el programa llamante:

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

Se llama al método initialise del objeto p1. Cuando en este método se hace referencia al objeto Me, en realidad se hace referencia al objeto p1. El método initialise también se podría haber escrito de la siguiente manera:


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

Cuando un método de un objeto hace referencia a un atributo A de dicho objeto, la escritura Me.A es implícita. Debe utilizarse explícitamente cuando existe un conflicto de identificadores. Este es el caso de la instrucción:


Me.age=age;

donde age hace referencia a un atributo del objeto actual, así como al parámetro age recibido por el método. Por lo tanto, hay que resolver la ambigüedad designando el atributo age como Me.age.

3.1.6. Un programa de prueba

A continuación se muestra un breve programa de prueba:


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

    ' método
    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étodo
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub
End Class

y los resultados obtenidos:

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. Utilizar un archivo de clases compiladas (assembly)

Cabe señalar que en el ejemplo anterior hay dos clases en nuestro programa de prueba: las clases personne y test1. Hay otra forma de proceder:

  • se compila la clase persona en un archivo específico denominado ensamblado (assembly). Este archivo tiene la extensión .dll

  • se compila la clase test1 haciendo referencia al ensamblado que contiene la clase persona.

Los dos archivos fuente quedan así:

test.vb

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

' opciones
Option Explicit On
Option Strict On

' espacios de nombres
Imports System

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

    ' método
    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étodo
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub    'identifie
End Class 'personne

La clase personne se compila mediante la siguiente instrucción:

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 compilación ha generado un archivo personne2.dll. Es la opción de compilación /t:library la que indica que se genere un archivo «assembly». Ahora compilemos el archivo 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

La opción de compilación /r:personne2.dll indica al compilador que encontrará ciertas clases en el archivo personne2.dll. Cuando en el archivo fuente test.vb encuentre una referencia a la clase personne, clase no declarada en el archivo fuente test.vb, buscará la clase personne en los archivos .dll referenciados por la opción /r. Aquí encontrará la clase personne en el ensamblado personne2.dll. Se podrían haber incluido otras clases en este ensamblado. Para utilizar varios archivos de clases compiladas durante la compilación, se escribirá:

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

La ejecución del programa test1.exe da los siguientes resultados:

dos>test
Jean,Dupont,30

3.1.8. Otro método inicializa

Consideremos de nuevo la clase personne y añadamos el siguiente método:


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

Ahora tenemos dos métodos con el nombre initialise: esto es válido siempre que admitan parámetros diferentes. Este es el caso aquí. El parámetro es ahora una referencia P a una persona. Los atributos de la persona P se asignan entonces al objeto actual (Me). Cabe señalar que el método initialise tiene acceso directo a los atributos del objeto P, aunque estos sean de tipo private. Esto siempre es así: un objeto O1 de una clase C siempre tiene acceso a los atributos de los objetos de la misma clase C. A continuación se muestra una prueba de la nueva clase personne, que se ha compilado en personne.dll tal y como se ha explicado anteriormente:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
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

y sus resultados:

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

3.1.9. Constructores de la clase persona

Un constructor es un procedimiento que lleva el nombre New y que se invoca al crear el objeto. Se suele utilizar para inicializarlo. Si una clase tiene un constructor que admite n argumentos argi, la declaración e inicialización de un objeto de esta clase se puede realizar de la forma:


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

o


        dim objet as classe

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

Cuando una clase tiene uno o varios constructores, es obligatorio utilizar uno de ellos para crear un objeto de dicha clase. Si una clase C no tiene ningún constructor, tiene uno por defecto, que es el constructor sin parámetros: public New(). Los atributos del objeto se inicializan entonces con valores por defecto. Esto es lo que ocurrió en los programas anteriores, donde habíamos escrito:

    dim p1 as personne
    p1=new personne

Creemos dos constructores para nuestra clase personne:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

' la clase persona
Public Class personne
    ' atributos
    Private prenom As String
    Private nom As String
    Private age As Integer

    ' constructores
    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étodos de inicialización del objeto
    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étodo
    Public Sub identifie()
        Console.Out.WriteLine((prenom & "," & nom & "," & age))
    End Sub
End Class

Nuestros dos constructores se limitan a llamar a los métodos initialise correspondientes. Recordemos que cuando en un constructor encontramos la notación initialise(P), por ejemplo, el compilador la traduce como Me.initialise(P). En el constructor, se invoca el método initialise para trabajar sobre el objeto al que hace referencia Me, es decir, el objeto actual, el que se está construyendo. He aquí un breve programa de prueba:


' opciones 
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

' página de prueba
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

y los resultados obtenidos:

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

3.1.10. Las referencias de objetos

Seguimos utilizando la misma clase personne. El programa de prueba queda así:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

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

        ' p2 hace referencia al mismo objeto que p1
        Dim p2 As personne = p1
        Console.Out.Write("p2=")
        p2.identifie()

        ' p3 hace referencia a un objeto que será una copia del objeto al que hace referencia p1
        Dim p3 As New personne(p1)
        Console.Out.Write("p3=")
        p3.identifie()

        ' se cambia el estado del objeto al que hace referencia p1
        p1.initialise("Micheline", "Benoît", 67)
        Console.Out.Write("p1=")
        p1.identifie()

        ' como p2 = p1, el objeto al que hace referencia p2 debe haber cambiado de estado
        Console.Out.Write("p2=")
        p2.identifie()

        ' como p3 no hace referencia al mismo objeto que p1, el objeto al que hace referencia p3 no debe de haber cambiado
        Console.Out.Write("p3=")
        p3.identifie()
    End Sub
End Module

Los resultados obtenidos son los siguientes:

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

Al declarar la variable p1 mediante

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

p1 hace referencia al objeto personne("Jean","Dupont",30), pero no es el objeto en sí. En C, se diría que es un puntero, c.a.d, a la dirección del objeto creado. Si a continuación escribimos:

    p1=nothing

No es el objeto personne("Jean","Dupont",30) el que se modifica, sino que es la referencia p1 la que cambia de valor. El objeto personne("Jean","Dupont",30) se «perderá» si no lo referencia ninguna otra variable.

Cuando se escribe:

dim p2 as personne=p1

se inicializa el puntero p2: «apunta» al mismo objeto (designa el mismo objeto) que el puntero p1. Así, si se modifica el objeto «apuntado» (o referenciado) por p1, se modifica el referenciado por p2.

Cuando se escribe:

dim p3 as personne =new personne(p1);

se crea un nuevo objeto, copia del objeto al que hace referencia p1. Este nuevo objeto será referenciado por p3. Si se modifica el objeto «apuntado» (o referenciado) por p1, no se modifica en absoluto el referenciado por p3. Esto es lo que muestran los resultados obtenidos.

3.1.11. Los objetos temporales

En una expresión, se puede invocar explícitamente al constructor de un objeto: este se construye, pero no tenemos acceso a él (para modificarlo, por ejemplo). Este objeto temporal se construye para evaluar la expresión y luego se descarta. El espacio de memoria que ocupaba será recuperado automáticamente más tarde por un programa denominado «reciclador de basura», cuya función es recuperar el espacio de memoria ocupado por objetos a los que ya no hacen referencia los datos del programa. Consideremos el siguiente nuevo programa de prueba:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

y modifiquemos los constructores de la clase personne para que muestren un mensaje:


    ' constructores
    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

Obtenemos los siguientes resultados:

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

lo que muestra la construcción sucesiva de los dos objetos temporales.

3.1.12. Métodos de lectura y escritura de los atributos privados

Añadimos a la clase personne los métodos necesarios para leer o modificar el estado de los atributos de los objetos:


Imports System

Public Class personne

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

    ' constructores
    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

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

    ' accesores
    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

    'modificadores
    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

Probamos la nueva clase con el siguiente programa:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

' páginas de prueba
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

y obtenemos los siguientes resultados:

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

3.1.13. Las propiedades

Existe otra forma de acceder a los atributos de una clase: creando propiedades. Estas nos permiten manipular atributos privados como si fueran públicos. Consideremos la siguiente clase personne, en la que los accesores y modificadores anteriores han sido sustituidos por propiedades de lectura y escritura:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

' clase persona
Public Class personne

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


    ' constructores
    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

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

    ' propiedades
    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)
            ' ¿edad válida?
            If Value >= 0 Then
                _age = Value
            Else
                Throw New Exception("âge (" & Value & ") invalide")
            End If
        End Set
    End Property
End Class

Una propiedad permite leer (get) o establecer (set) el valor de un atributo. En nuestro ejemplo, hemos antepuesto el carácter _ a los nombres de los atributos para que las propiedades tengan el mismo nombre que los atributos primitivos. De hecho, una propiedad no puede tener el mismo nombre que el atributo que gestiona, ya que entonces se produciría un conflicto de nombres en la clase. Por lo tanto, hemos llamado a nuestros atributos _prenom, _nom, _age y hemos modificado los constructores y métodos en consecuencia. A continuación, hemos creado tres propiedades: nom, prenom y age. Una propiedad se declara de la siguiente manera:


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

donde Type debe ser el tipo del atributo gestionado por la propiedad. Puede tener dos métodos llamados get y set. El método get suele encargarse de devolver el valor del atributo que gestiona (podría devolver otra cosa, nada se lo impide). El método set recibe un parámetro llamado value que normalmente asigna al atributo que gestiona. Puede aprovechar para comprobar la validez del valor recibido y, en su caso, lanzar una excepción si el valor resulta inválido. Esto es lo que se hace aquí con la edad.

¿Cómo se invocan estos métodos get y set? Consideremos el siguiente programa de prueba:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

' página de prueba
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

En la instrucción

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

se busca obtener los valores de las propiedades prenom, nom y age de la persona P. Es el método get de estas propiedades el que se invoca y el que devuelve el valor del atributo que gestionan.

En la instrucción

        P.age = 56

se quiere establecer el valor de la propiedad age. En ese caso, se invoca el método set de dicha propiedad. Recibirá 56 en su parámetro value.

Una propiedad P de una clase C que solo definiera el método get se denomina de solo lectura. Si c es un objeto de la clase C, el compilador rechazará la operación c.P=valor.

La ejecución del programa de prueba anterior da los siguientes resultados:

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

Las propiedades nos permiten, por tanto, manipular atributos privados como si fueran públicos.

3.1.14. Métodos y atributos de clase

Supongamos que queremos contar el número de objetos [personne] creados en una aplicación. Podemos gestionar nosotros mismos un contador, pero corremos el riesgo de olvidarnos de los objetos temporales que se crean aquí y allá. Parecería más seguro incluir en los constructores de la clase [personne] una instrucción que incremente un contador. El problema es pasar una referencia de este contador para que el constructor pueda incrementarlo: hay que pasarles un nuevo parámetro. También se puede incluir el contador en la definición de la clase. Como se trata de un atributo de la propia clase y no de un objeto concreto de dicha clase, se declara de forma diferente con la palabra clave Shared:

    Private Shared _nbPersonnes As Long = 0

Para hacer referencia a él, se escribe personne._nbPersonnes para indicar que es un atributo de la propia clase personne. Aquí hemos creado un atributo privado al que no se tendrá acceso directo fuera de la clase. Por lo tanto, creamos una propiedad pública para dar acceso al atributo de clase nbPersonnes. Para establecer el valor de nbPersonnes, el método get de esta propiedad no necesita un objeto personne concreto: de hecho, _nbPersonnes no es el atributo de un objeto concreto, sino el atributo de toda una clase. Por lo tanto, necesitamos una propiedad declarada también como Shared:

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

que desde el exterior se llamará con la sintaxis personne.nbPersonnes. La propiedad se declara de solo lectura (ReadOnly) ya que no ofrece ningún método set. A continuación se muestra un ejemplo. La clase personne queda de la siguiente manera:


Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

' clase
Public Class personne
    ' atributos de clase
    Private Shared _nbPersonnes As Long = 0

    ' atributos de instancia
    Private _prenom As [String]
    Private _nom As [String]
    Private _age As Integer

    ' constructores
    Public Sub New(ByVal P As [String], ByVal N As [String], ByVal age As Integer)
        ' una persona más
        _nbPersonnes += 1
        Me._prenom = P
        Me._nom = N
        Me._age = age
    End Sub

    Public Sub New(ByVal P As personne)
        ' una persona más
        _nbPersonnes += 1
        Me._prenom = P._prenom
        Me._nom = P._nom
        Me._age = P._age
    End Sub

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

    ' propiedad de clase
    Public Shared ReadOnly Property nbPersonnes() As Long
        Get
            Return _nbPersonnes
        End Get
    End Property

    ' propiedades de instancia
    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)
            ' ¿edad válida?
            If Value >= 0 Then
                _age = Value
            Else
                Throw New Exception("âge (" & Value & ") invalide")
            End If
        End Set
    End Property
End Class

Con el siguiente programa:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

' página de prueba
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

se obtienen los siguientes resultados:

    Nombre de personnes créées : 2

3.1.15. Pasar un objeto a una función

Ya hemos dicho que, por defecto, VB.NET pasa los parámetros efectivos de una función por valor: los valores de los parámetros efectivos se copian en los parámetros formales. En el caso de un objeto, no hay que dejarse engañar por el uso incorrecto del lenguaje que se hace sistemáticamente al hablar de objeto en lugar de referencia de objeto. Un objeto solo se manipula a través de una referencia (un puntero) sobre él. Por lo tanto, lo que se transmite a una función no es el objeto en sí, sino una referencia a dicho objeto. Así pues, es el valor de la referencia y no el valor del objeto en sí lo que se duplica en el parámetro formal: no se crea un nuevo objeto. Si se transmite una referencia de objeto R1 a una función, se copiará en el parámetro formal correspondiente R2. Por lo tanto, las referencias R2 y R1 designan al mismo objeto. Si la función modifica el objeto al que apunta R2, evidentemente modifica también el objeto al que hace referencia R1, ya que se trata del mismo.

Esto es lo que muestra el siguiente ejemplo:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

' páginas de prueba
Module test
    Sub Main()
        ' una persona p1
        Dim p1 As New personne("Jean", "Dupont", 30)

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

        ' modificación p1
        modifie(p1)

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

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

Los resultados obtenidos son los siguientes:

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

Se observa que solo se ha creado un objeto: el de la persona p1 del procedimiento Main, y que el objeto ha sido modificado correctamente por la función modifie.

3.1.16. Una tabla de personas

Un objeto es un dato como cualquier otro y, como tal, se pueden agrupar varios objetos en una tabla:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System

' página de prueba
Module test
    Sub Main()
        ' una tabla de personas
        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)
        ' visualización
        Console.Out.WriteLine("----------------")
        Dim i As Integer
        For i = 0 To amis.Length - 1
            amis(i).identifie()
        Next i
    End Sub
End Module

La instrucción Dim amigos(2) As persona crea una matriz de 3 elementos de tipo personne. Estos 3 elementos se inicializan aquí con los valores nothing y c.a.d, ya que no hacen referencia a ningún objeto. De nuevo, por abuso de lenguaje, hablamos de matriz de objetos cuando en realidad se trata solo de una matriz de referencias a objetos. La creación de la matriz de objetos, que es en sí misma un objeto, no crea ningún objeto del tipo de sus elementos: hay que hacerlo después. Se obtienen los siguientes resultados:

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

3.2. La herencia a través del ejemplo

3.2.1. Generalidades

Aquí abordamos el concepto de herencia. El objetivo de la herencia es «personalizar» una clase existente para que satisfaga nuestras necesidades. Supongamos que queremos crear una clase enseignant: un profesor es una persona concreta. Tiene atributos que otra persona no tendrá: la asignatura que imparte, por ejemplo. Pero también tiene los atributos de cualquier persona: nombre, apellidos y edad. Por lo tanto, un profesor forma parte plenamente de la clase personne, pero tiene atributos adicionales. En lugar de escribir una clase enseignant desde cero, preferiríamos aprovechar lo ya creado de la clase personne y adaptarlo al carácter particular de los profesores. Es el concepto de herencia el que nos permite hacer esto. Para expresar que la clase enseignant hereda las propiedades de la clase personne, escribiremos:


Public Class enseignant
    Inherits personne

Cabe destacar la sintaxis particular en dos líneas. La clase personne se denomina clase padre (o madre) y la clase enseignant, clase derivada (o hija). Un objeto enseignant tiene todas las características de un objeto personne: tiene los mismos atributos y los mismos métodos. Estos atributos y métodos de la clase padre no se repiten en la definición de la clase hija: basta con indicar los atributos y métodos añadidos por la clase hija. Suponemos que la clase personne se define de la siguiente manera:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

' clase persona
Public Class personne

    ' atributos de clase
    Private Shared _nbPersonnes As Long = 0

    ' atributos de instancia
    Private _prenom As [String]
    Private _nom As [String]
    Private _age As Integer


    ' constructores
    Public Sub New(ByVal P As [String], ByVal N As [String], ByVal age As Integer)
        ' una persona más
        _nbPersonnes += 1
        ' construcción
        Me._prenom = P
        Me._nom = N
        Me._age = age
        ' seguimiento
        Console.Out.WriteLine("Construction personne(string, string, int)")
    End Sub


    Public Sub New(ByVal P As personne)
        ' una persona más
        _nbPersonnes += 1
        ' construcción
        Me._prenom = P._prenom
        Me._nom = P._nom
        Me._age = P._age
        ' seguimiento
        Console.Out.WriteLine("Construction personne(string, string, int)")
    End Sub

    ' propiedad de clase
    Public Shared ReadOnly Property nbPersonnes() As Long
        Get
            Return _nbPersonnes
        End Get
    End Property

    ' propiedades de instancia
    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)
            ' ¿edad válida?
            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

El método identifie ha sido sustituido por la propiedad identité, de solo lectura, que identifica a la persona. Creamos una clase enseignant que hereda de la clase personne:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

Public Class enseignant
    Inherits personne
    ' atributos
    Private _section As Integer

    ' constructor
    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
        ' seguimiento
        Console.Out.WriteLine("Construction enseignant(string,string,int,int)")
    End Sub

    ' propiedad de sección
    Public Property section() As Integer
        Get
            Return _section
        End Get
        Set(ByVal Value As Integer)
            _section = Value
        End Set
    End Property
End Class 

La clase enseignant añade a los métodos y atributos de la clase personne:

  • un atributo section, que es el número de sección a la que pertenece el profesor dentro del cuerpo docente (una sección por disciplina, en términos generales)
  • un nuevo constructor que permite inicializar todos los atributos de un profesor

La declaración


Public Class enseignant
    Inherits personne

indica que la clase enseignant deriva de la clase personne.

3.2.2. Creación de un objeto profesor

El constructor de la clase enseignant es el siguiente:


    ' constructor
    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
        ' seguimiento
        Console.Out.WriteLine("Construction enseignant(string,string,int,int)")
    End Sub    

La declaración


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

indica que el constructor recibe cuatro parámetros: P, N, age y section. Debe pasar tres de ellos, (P,N,age), a su clase base, en este caso la clase personne. Sabemos que esta clase tiene un constructor persona(string, string, int) que permitirá construir una persona con los parámetros pasados (P,N,age). La clase [enseignant] pasa los parámetros (P, N, edad) a su clase base de la siguiente manera:


        MyBase.New(P, N, age)

Una vez finalizada la construcción de la clase base, la construcción del objeto enseignant continúa con la ejecución del cuerpo del constructor:


        Me._section = section

En resumen, el constructor de una clase derivada:

  • pasa a su clase base los parámetros que esta necesita para construirse
  • utiliza los demás parámetros para inicializar los atributos que le son propios

Se podría haber optado por escribir:


    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
        ' seguimiento
        Console.Out.WriteLine("Construction enseignant(string,string,int,int)")
    End Sub    

Es imposible. La clase personne ha declarado privados (private) sus tres campos _prenom, _nom y _age. Solo los objetos de la misma clase tienen acceso directo a estos campos. Todos los demás objetos, incluidos los objetos derivados como en este caso, deben pasar por métodos públicos para acceder a ellos. Esto habría sido diferente si la clase personne hubiera declarado protegidos (protected) los tres campos: en ese caso, habría permitido a las clases derivadas tener acceso directo a los tres campos. En nuestro ejemplo, utilizar el constructor de la clase padre era, por tanto, la solución correcta y es el método habitual: al construir un objeto hijo, primero se llama al constructor del objeto padre y luego se completan las inicializaciones propias del objeto hijo (section en nuestro ejemplo).

Compilemos las clases personne y enseignant en ensamblados:

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

Cabe destacar que, para compilar la clase hija enseignant, ha sido necesario hacer referencia al archivo personne.dll, que contiene la clase personne. Probemos con un primer programa de prueba:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

Este programa se limita a crear un objeto enseignant (new) e identificarlo. La clase enseignant no tiene el método identité, pero su clase padre sí lo tiene y, además, es público: por herencia, se convierte en un método público de la clase enseignant. Los resultados obtenidos son los siguientes:

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)

Se observa que:

  • se ha creado un objeto personne antes que el objeto enseignant
  • la identidad obtenida es la del objeto personne

3.2.3. Sobrecarga de un método o una propiedad

En el ejemplo anterior, obtuvimos la identidad de la parte personne del profesor, pero falta cierta información específica de la clase enseignant (la sección). Por lo tanto, debemos escribir una propiedad que permita identificar al profesor:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

Public Class enseignant
    Inherits personne
    ' atributos
    Private _section As Integer

    ' constructor
    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
        ' seguimiento
        Console.Out.WriteLine("Construction enseignant(string,string,int,int)")
    End Sub

    ' propiedad de sección
    Public Property section() As Integer
        Get
            Return _section
        End Get
        Set(ByVal Value As Integer)
            _section = Value
        End Set
    End Property

    ' sobrecarga de la propiedad identidad
    Public Shadows ReadOnly Property identite() As String
        Get
            Return "enseignant(" & MyBase.identite & "," & _section & ")"
        End Get
    End Property
End Class

El método identite de la clase enseignant se basa en el método identite de su clase padre (MyBase.identite) para mostrar su parte «personne» y, a continuación, se completa con el campo _section, que es propio de la clase enseignant. Cabe destacar la declaración de la propiedad identite:


    Public Shadows ReadOnly Property identite() As String

que indica que la propiedad identite «oculta» el método del mismo nombre que podría existir en la clase padre. Sea un objeto enseignant E. Este objeto contiene en su interior un objeto personne:

La propiedad «identity» se define tanto en la clase enseignant como en su clase padre personne. En la clase hija enseignant, la propiedad identite debe ir precedida de la palabra clave shadows para indicar que se redefine una nueva propiedad identite para la clase enseignant.


    Public Shadows ReadOnly Property identite() As String

La clase enseignant dispone ahora de dos propiedades identite:

  • la heredada de la clase padre personne
  • la propia

Si E es un objeto enseignant, E.identite designa el método identite de la clase enseignant. Se dice que la propiedad identite de la clase madre está «redefinida» por la propiedad identite de la clase hija. En general, si O es un objeto y M un método, para ejecutar el método O.M, el sistema busca un método M en el siguiente orden:

  • en la clase del objeto O
  • en su clase padre, si la tiene
  • en la clase madre de su clase madre, si existe
  • etc…

La herencia permite, por tanto, redefinir en la clase hija métodos o propiedades con el mismo nombre que en la clase madre. Esto es lo que permite adaptar la clase hija a sus propias necesidades. Junto con el polimorfismo, que veremos más adelante, la sobrecarga de métodos o propiedades es el principal interés de la herencia. Consideremos el mismo ejemplo que antes:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

Los resultados obtenidos esta vez son los siguientes:

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

3.2.4. El polimorfismo

Consideremos una línea de clases: C0  C1  C2  … Cn donde Ci  Cj indica que la clase Cj deriva de la clase Ci. Esto implica que la clase Cj tiene todas las características de la clase Ci más otras adicionales. Sean Oi objetos de tipo Ci. Es válido escribir:

    Oi=Oj avec j>i

De hecho, por herencia, la clase Cj tiene todas las características de la clase Ci más otras. Por lo tanto, un objeto Oj de tipo Cj contiene en sí mismo un objeto de tipo Ci. La operación

Oi=Oj

hace que Oi sea una referencia al objeto de tipo Ci contenido en el objeto Oj.

El hecho de que una variable Oi de la clase Ci pueda, de hecho, hacer referencia no solo a un objeto de la clase Ci, sino a cualquier objeto derivado de la clase Ci, se denomina polimorfismo: la capacidad de una variable para hacer referencia a diferentes tipos de objetos. Tomemos un ejemplo y consideremos la siguiente función independiente de cualquier clase:


    Sub affiche(ByVal p As personne)

También podríamos escribir

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

que

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

En este último caso, el parámetro formal de tipo personne de la función affiche recibirá un valor de tipo enseignant. Dado que el tipo enseignant deriva del tipo personne, esto es válido.

3.2.5. Redefinición y polimorfismo

Completemos nuestro procedimiento affiche:


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

El método p.identite devuelve una cadena de caracteres que identifica el objeto personne. ¿Qué ocurre en el caso de nuestro ejemplo anterior con un objeto enseignant?


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

Veamos el siguiente ejemplo:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

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

Los resultados obtenidos son los siguientes:

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

La ejecución muestra que la instrucción p.identite ejecutó cada vez la propiedad identite de una personne, la persona contenida en la enseignant e, y luego la propia personne p. No se ha adaptado al objeto realmente pasado como parámetro a affiche. Hubiéramos preferido tener la identidad completa del enseignant e. Para ello, habría sido necesario que la notación p.identite hiciera referencia a la propiedad identite del objeto al que realmente apunta p, en lugar de a la propiedad identite de la parte «personne» del objeto al que apunta realmente p. Es posible obtener este resultado declarando identite como una propiedad redefinible (overridable) en la clase base personne:


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

La palabra clave overridable convierte a identite en una propiedad redefinible o virtual. Esta palabra clave también se puede aplicar a los métodos. Las clases derivadas que redefinen una propiedad o método virtual deben utilizar entonces la palabra clave overrides en lugar de shadows para calificar su propiedad/método redefinido. Así, en la clase enseignant, la propiedad identite se define de la siguiente manera:


    ' sobrescrito propiedad identidad
    Public Overrides ReadOnly Property identite() As String
        Get
            Return "enseignant(" & MyBase.identite & "," & _section & ")"
        End Get
    End Property

El programa de prueba:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

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

produce entonces los siguientes resultados:

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)

Esta vez sí que hemos obtenido la identidad completa del profesor. Ahora redefinamos un método en lugar de una propiedad. La clase object es la clase «madre» de todas las clases VB.NET. Así, cuando escribimos:

    public class personne

se escribe implícitamente:

    public class personne 
         inherits object

La clase object define un método virtual ToString:

Image

El método ToString devuelve el nombre de la clase a la que pertenece el objeto, tal y como se muestra en el siguiente ejemplo:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

Los resultados obtenidos son los siguientes:

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

Cabe señalar que, aunque no hayamos redefinido el método ToString en las clases personne y enseignant, se puede observar que el método ToString de la clase object es capaz de mostrar el nombre real de la clase del objeto. Redefinamos el método ToString en las clases personne y enseignant:


    ' ToString
    Public Overrides Function ToString() As String
        ' se establece la propiedad «identidad»
        Return identite
    End Function

La definición es la misma en ambas clases. Consideremos el siguiente programa de prueba:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

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

Los resultados de la ejecución son los siguientes:

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. Definir un indexador para una clase

Consideremos la clase [ArrayList] predefinida en la plataforma .NET. Esta clase permite almacenar objetos en una lista. Pertenece al espacio de nombres [System.Collections]. En el siguiente ejemplo, utilizamos esta clase para almacenar una lista de personas (en sentido amplio):


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System
Imports System.Collections

' clase listeDePersonnes
Public Class listeDePersonnes
    Inherits ArrayList

    ' para añadir una persona a la lista
    Public Overloads Sub Add(ByVal p As personne)
        MyBase.Add(p)
    End Sub

    ' un indexador
    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

    ' otro indexador
    Default Public Shadows ReadOnly Property Item(ByVal N As String) As Integer
        Get
            ' se busca a la persona llamada 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
        ' devuelve (el1, el2, ..., eln)
        Dim liste As String = "("
        Dim i As Integer
        ' se recorre la matriz dinámica
        For i = 0 To (Count - 2)
            liste += "[" + Me(i).ToString + "]" + ","
        Next i        'for
        ' último elemento
        If Count <> 0 Then
            liste += "[" + Me(i).ToString + "]"
        End If
        liste += ")"
        Return liste
    End Function
End Class

Veamos algunos detalles sobre ciertas propiedades y métodos de la clase [ArrayList]:

Count
atributo que indica el número de elementos de la lista
Add(Object)
método que permite añadir un objeto a la lista
Item(Integer i)
método que devuelve el elemento i de la lista

Observamos que, para obtener el elemento n.º i de la lista, no hemos escrito [liste.Item(i)], sino directamente [liste(i)], lo que a primera vista parece erróneo. Sin embargo, esto es posible porque la clase [ArrayList] define una propiedad por defecto [Item] con una sintaxis similar a la siguiente:


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

Cuando el compilador encuentra la notación [liste(i)], comprueba si la clase [ArrayList] ha definido una propiedad con la siguiente firma:


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

Aquí encontrará el procedimiento [Item]. A continuación, traducirá la escritura [liste(i)] a [liste.Item(i)]. La propiedad [Item] se denomina propiedad indexada por defecto de la clase [ArrayList]. La ejecución del programa anterior da los siguientes resultados:

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)

Creemos una clase llamada [listeDePersonnes] que sea una lista de personas, es decir, una lista específica que parece natural derivar de la clase [ArrayList]:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System
Imports System.Collections

' clase listeDePersonnes
Public Class listeDePersonnes
    Inherits ArrayList

    ' para añadir una persona a la lista
    Public Shadows Sub Add(ByVal p As Object)
        ' p debe ser una persona
        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 indexador
    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
        ' devuelve (el1, el2, ..., eln)
        Dim liste As String = "("
        Dim i As Integer
        ' recorremos la matriz dinámica
        For i = 0 To (Count - 2)
            liste += "[" + Me(i).ToString + "]" + ","
        Next i        'for
        ' último elemento
        If Count <> 0 Then
            liste += "[" + Me(i).ToString + "]"
        End If
        liste += ")"
        Return liste
    End Function
End Class

La clase presenta los siguientes métodos y propiedades:

ToString
devuelve una cadena de caracteres que «representa» el contenido de la lista
Add(personne)
método que permite añadir una persona a la lista
Item(Integer i)
propiedad indexada por defecto que devuelve a la persona i de la lista

Veamos las novedades:

Se ha creado un nuevo método [Add].


    ' para añadir una persona a la lista
    Public Shadows Sub Add(ByVal p As Object)
        ' p debe ser una persona
        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

Ya existe uno en la clase padre [ArrayList] con la misma firma, de ahí la palabra clave [Shadows] para indicar que el nuevo procedimiento sustituye al de la clase padre. El método [Add] de la clase hija comprueba que el objeto añadido sea efectivamente de tipo [personne] o un derivado con la función [TypeOf]. Si no es así, se lanza una excepción con la instrucción [Throw]. Si el objeto añadido es efectivamente de tipo [personne], se utiliza el método [Add] de la clase base para realizar la adición.

Se crea una propiedad indexada:


    ' un índice
    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

Ya existe una propiedad predeterminada llamada [Item] en la clase base con la misma firma. Por lo tanto, es necesario utilizar la palabra clave [Shadows] para indicar que la nueva propiedad indexada [Index] «ocultará» la de la clase base [Item]. Cabe señalar que esto es así incluso si las dos propiedades no tienen el mismo nombre. La propiedad [Index] permite hacer referencia a la persona n.º i de la lista. Se basa en la propiedad [Item] de la clase base para acceder al elemento n.º i del objeto subyacente [ArrayList]. Se realizan cambios de tipo para tener en cuenta que la propiedad [Item] trabaja con elementos de tipo [Object], mientras que la propiedad [Index] trabaja con elementos de tipo [personne].

Por último, redefinimos (Overrides) el método [ToString] de la clase [ArrayList]:


    ' toString
    Public Overrides Function ToString() As String
        ' devuelve (el1, el2, ..., eln)
        Dim liste As String = "("
        Dim i As Integer
        ' recorremos la matriz dinámica
        For i = 0 To (Count - 2)
            liste += "[" + Me(i).ToString + "]" + ","
        Next i        'for
        ' último elemento
        If Count <> 0 Then
            liste += "[" + Me(i).ToString + "]"
        End If
        liste += ")"
        Return liste
    End Function

Este método devuelve una cadena de caracteres con el formato «(e1,e2,...,en)», donde e1, e2, ..., en son los elementos de la lista. Cabe destacar la notación [Me(i)], que designa el elemento n.º i del objeto actual [Me]. Se utiliza la propiedad indexada por defecto. Así, [Me(i)] es equivalente a [Me.Index(i)].

El código de la clase se coloca en el archivo [lstpersonnes2.vb] y se compila:

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

Se crea un programa de prueba:


' opciones
Option Explicit On 
Option Strict On

' espacios de nombres
Imports System
Imports System.Collections

Module test
    Sub Main()
        ' creación de una lista vacía de personas
        Dim liste As listeDePersonnes = New listeDePersonnes
        ' creación de personas
        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)
        ' rellenar la lista
        liste.Add(p1)
        liste.Add(p2)
        liste.Add(e1)
        ' visualización de la lista
        Console.Out.WriteLine(liste.ToString)
        ' Añadir un objeto distinto de persona
        Try
            liste.Add(4)
        Catch e As Exception
            Console.Error.WriteLine(e.Message)
        End Try
    End Sub
End Module

Se compila:

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

y luego ejecutado:

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

Podríamos querer escribir

dim p as personne=l("nom")

donde l sería del tipo [listeDePersonnes]. En este caso, queremos indexar la lista l no por un número de elemento, sino por el nombre de una persona. Para ello, definimos una nueva propiedad indexada por defecto:


    ' otro indexador
    Default Public Shadows ReadOnly Property Item(ByVal N As String) As Integer
        Get
            ' se busca a la persona llamada 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 primera línea


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

indica que se vuelve a crear una propiedad indexada por defecto. Todas las propiedades por defecto deben tener el mismo nombre, en este caso [Index]. La nueva propiedad [Index] indexa la clase listeDePersonnes mediante una cadena de caracteres N. El resultado de listeDePersonnes(N) es un entero. Este entero será la posición en la lista de la persona con el nombre N o -1 si dicha persona no está en la lista. Solo se define la propiedad get, lo que impide la escritura de listeDePersonnes ("nom")=valeur, que habría requerido la definición de la propiedad set). De ahí la palabra clave [ReadOnly]. La palabra clave [Shadows] es necesaria para ocultar la propiedad por defecto de la clase base (aunque no tenga la misma firma).

En el cuerpo de get, se recorre la lista de personas en busca del nombre N pasado como parámetro. Si se encuentra en la posición i, se devuelve i; de lo contrario, se devuelve -1.

Un nuevo programa de prueba podría ser el siguiente:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

' página de prueba
Module test
    Sub Main()
        ' una lista de personas
        Dim l As New listeDePersonnes
        ' Añadir personas
        l.Add(New personne("jean", "dumornet", 10))
        l.Add(New personne("pauline", "duchemin", 12))
        ' visualización
        Console.Out.WriteLine(("l=" + l.ToString))
        l.Add(New personne("jacques", "tartifume", 27))
        Console.Out.WriteLine(("l=" + l.ToString))
        ' cambio del elemento 1
        l(1) = New personne("sylvie", "cachan", 5)
        ' visualización del elemento 1
        Console.Out.WriteLine(("l[1]=" + l(1).ToString))
        ' visualización de la lista l
        Console.Out.WriteLine(("l=" + l.ToString))
        ' búsqueda de personas
        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

La ejecución da los siguientes resultados:

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. Las estructuras

La estructura VB.NET se deriva directamente de la estructura del lenguaje C y es muy similar a una clase. Una estructura se define de la siguiente manera:


Structure spersonne
' atributos
    ...
' propiedades
...
' constructores
...
' métodos
End Structure

A pesar de la similitud en su declaración, existen diferencias importantes entre las clases y las estructuras. Por ejemplo, el concepto de herencia no existe en las estructuras. Si escribimos una clase que no debe derivarse, ¿cuáles son las diferencias entre una estructura y una clase que nos ayudarán a elegir entre ambas? Veamos el siguiente ejemplo para descubrirlo:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

' clase persona
Class cpersonne
    Public nom As String
    Public age As Integer
End Class

' un módulo de prueba
Public Module test
    Sub Main()
        ' una persona p1
        Dim sp1 As spersonne
        sp1.nom = "paul"
        sp1.age = 10
        Console.Out.WriteLine(("sp1=spersonne(" & sp1.nom & "," & sp1.age & ")"))
        ' una persona p2
        Dim sp2 As spersonne = sp1
        Console.Out.WriteLine(("sp2=spersonne(" & sp2.nom & "," & sp2.age & ")"))
        ' sp2 se modifica
        sp2.nom = "nicole"
        sp2.age = 30
        ' verificación de sp1 y sp2
        Console.Out.WriteLine(("sp1=cpersonne(" & sp1.nom & "," & sp1.age & ")"))
        Console.Out.WriteLine(("sp2=cpersonne(" & sp2.nom & "," & sp2.age & ")"))

        ' una persona cp1
        Dim cp1 As New cpersonne
        cp1.nom = "paul"
        cp1.age = 10
        Console.Out.WriteLine(("cP1=cpersonne(" & cp1.nom & "," & cp1.age & ")"))
        ' una persona cp2 P2
        Dim cp2 As cpersonne = cp1
        Console.Out.WriteLine(("cP2=cpersonne(" & cp2.nom & "," & cp2.age & ")"))
        ' P2 se modifica
        cp2.nom = "nicole"
        cp2.age = 30
        ' verificación de P1 y P2
        Console.Out.WriteLine(("cP1=cpersonne(" & cp1.nom & "," & cp1.age & ")"))
        Console.Out.WriteLine(("cP2=cpersonne(" & cp2.nom & "," & cp2.age & ")"))
    End Sub
End Module

Si ejecutamos este programa, obtenemos los siguientes resultados:

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)

Mientras que en las páginas anteriores de este capítulo se utilizaba una clase personne, ahora utilizamos una estructura spersonne:


' estructura de persona
Structure spersonne
    Public nom As String
    Public age As Integer
End Structure

La declaración


        Dim sp1 As spersonne

crea una estructura (nombre, edad) y el valor de sp1 es esta misma estructura.

La declaración


        Dim cp1 As New cpersonne

crea un objeto [cpersonne] (más o menos el equivalente a nuestra estructura) y cp1 es entonces la dirección (la referencia) de este objeto.

Resumamos

  • en el caso de la estructura, el valor de sp1 es la propia estructura
  • en el caso de la clase, el valor de p1 es la dirección del objeto creado

Cuando en el programa se escribe


        Dim sp2 As spersonne = sp1

se crea una nueva estructura (nombre, edad) y se inicializa con el valor de p1, es decir, la propia estructura.

La estructura de sp1 se duplica, por tanto, en sp2. Se trata de una copia de valores.

La instrucción


        Dim cp2 As cpersonne = cp1

funciona de manera diferente. El valor de cp1 se copia en cp2, pero como ese valor es, en realidad, la dirección del objeto, este no se duplica. Simplemente tiene dos referencias que apuntan a él:

En el caso de la estructura, si se modifica el valor de sp2, no se modifica el valor de sp1, tal y como muestra el programa. En el caso del objeto, si se modifica el objeto al que apunta cp2, el que apunta cp1 se modifica, ya que es el mismo. Esto es lo que muestran también los resultados del programa.

De estas explicaciones se deduce, por tanto, que:

  • el valor de una variable de tipo estructura es la propia estructura
  • el valor de una variable de tipo objeto es la dirección del objeto al que apunta

Una vez comprendida esta diferencia fundamental, la estructura resulta muy similar a la clase, como muestra el siguiente ejemplo:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

' estructura de persona
Structure personne
    ' atributos
    Private _nom As String
    Private _age As Integer

    ' propiedades
    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

    ' Constructor
    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 módulo de prueba
Module test
    Sub Main()
        ' una persona p1
        Dim p1 As New personne("paul", 10)
        Console.Out.WriteLine(("p1=" & p1.ToString))
        ' una persona p2
        Dim p2 As personne = p1
        Console.Out.WriteLine(("p2=" & p2.ToString))
        ' p2 se modifica
        p2.nom = "nicole"
        p2.age = 30
        ' verificación de p1 y p2
        Console.Out.WriteLine(("p1=" & p1.ToString))
        Console.Out.WriteLine(("p2=" & p2.ToString))
    End Sub
End Module

Se obtienen los siguientes resultados de ejecución:

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

La única diferencia notable aquí entre la estructura y la clase es que, con una clase, los objetos p1 y p2 habrían tenido el mismo valor al final del programa, el de p2.

3.5. Las interfaces

Una interfaz es un conjunto de prototipos de métodos o propiedades que forman un contrato. Una clase que decide implementar una interfaz se compromete a proporcionar una implementación de todos los métodos definidos en la interfaz. Es el compilador quien verifica esta implementación. A continuación se muestra, por ejemplo, la definición de la interfaz Istats:

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

Cualquier clase que implemente esta interfaz se declarará como

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

Los métodos [moyenne] y [écartType] deberán definirse en la clase C. Consideremos el siguiente código:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

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

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

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

    ' constructor
    Public Sub New(ByVal MATIERE As String, ByVal ELEVES() As élève)
        ' almacenamiento de alumnos y asignaturas
        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
        ' se concatenan todas las notas
        For i = 0 To (_élèves.Length - 1) - 1
            valeur &= "[" & _élèves(i)._nom & "," & _élèves(i)._note & "],"
        Next i
        'última nota
        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 clase notes recopila las notas de una clase en una asignatura:


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

Los atributos se declaran como protected para que sean accesibles desde una clase derivada. El tipo élève es una estructura que almacena el nombre del alumno y su nota en la asignatura:


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

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

Decidimos derivar esta clase notes en una clase notesStats que tendría dos atributos adicionales, la media y la desviación estándar de las notas:


Public Class notesStats
    Inherits notes
    Implements Istats

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

La clase notesStats implementa la siguiente interfaz Istats:

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

Esto significa que la clase notesStats debe tener dos métodos llamados moyenne y écartType con la firma indicada en la interfaz Istats. La clase notesStats es la siguiente:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

Public Class notesStats
    Inherits notes
    Implements Istats

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

    ' constructor
    Public Sub New(ByVal MATIERE As String, ByVal ELEVES() As élève)
        MyBase.New(MATIERE, ELEVES)
        ' cálculo de la media de las puntuaciones
        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
        ' desviación estándar
        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étodos de la interfaz Istats
    Public Function moyenne() As Double Implements Istats.moyenne
        ' devuelve la media de las puntuaciones
        Return _moyenne
    End Function

    Public Function écartType() As Double Implements Istats.écartType
        ' devuelve la desviación estándar
        Return _écartType
    End Function
End Class

La media _moyenne y la desviación estándar _ecartType se calculan en el momento de la construcción del objeto. Por lo tanto, los métodos moyenne y écartType solo tienen que devolver el valor de los atributos _moyenne y _ecartType. Ambos métodos devuelven -1 si la tabla de alumnos está vacía.

La siguiente clase de prueba:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

Module test
    Sub Main()
        ' algunos alumnos y notas
        Dim ELEVES() As élève = {New élève("paul", 14), New élève("nicole", 16), New élève("jacques", 18)}
        ' que se guardan en un objeto de notas
        Dim anglais As New notes("anglais", ELEVES)
        ' y que se muestran
        Console.Out.WriteLine((anglais.ToString))
        ' lo mismo con la media y la desviación estándar
        anglais = New notesStats("anglais", ELEVES)
        Console.Out.WriteLine((anglais.ToString))
    End Sub
End Module

da los siguientes resultados:

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 clase notesStats podría perfectamente haber implementado los métodos moyenne y écartType por sí misma sin indicar que implementaba la interfaz Istats. ¿Cuál es la utilidad de las interfaces? Es la siguiente: una función puede admitir como parámetro formal un dato que tenga el tipo de una interfaz I. Cualquier objeto de una clase C que implemente la interfaz I podrá entonces ser un parámetro efectivo de dicha función. Consideremos el siguiente ejemplo:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

' una interfaz Iejemplo
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

' una primera clase
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

'una segunda clase
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

La interfaz Iexemple define dos métodos: ajouter y soustraire. Las clases classe1 y classe2 implementan esta interfaz. Cabe señalar que estas clases no hacen nada más, con el fin de simplificar el ejemplo. Consideremos ahora el siguiente ejemplo:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

' clase de prueba
Module test

    'calcular
    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 función Main
    Sub Main()
        ' creación de dos objetos clase1 y clase2
        Dim c1 As New classe1
        Dim c2 As New classe2
        ' llamadas a la función estática calcular
        calculer(4, 3, c1)
        calculer(14, 13, c2)
    End Sub
End Module

La función calculer admite como parámetro un elemento de tipo Iexemple. Por lo tanto, podrá recibir para este parámetro tanto un objeto de tipo classe1 como de tipo classe2. Esto es lo que se hace en el procedimiento Main con los siguientes resultados:

17
21
127
201

Se observa, por tanto, que se trata de una propiedad similar al polimorfismo visto en las clases. Si un conjunto de clases Ci no relacionadas entre sí por herencia (por lo que no se puede utilizar el polimorfismo de la herencia) presenta un conjunto de métodos con la misma firma, puede resultar interesante agrupar estos métodos en una interfaz I de la que heredarían todas las clases en cuestión. Las instancias de estas clases Ci pueden entonces utilizarse como parámetros de funciones que admiten un parámetro de tipo I, c.a.d. Funciones que utilizan únicamente los métodos de los objetos Ci definidos en la interfaz I y no los atributos y métodos particulares de las diferentes clases Ci. Por último, cabe señalar que la herencia de interfaces puede ser múltiple, c.a.d. que se puede escribir


Public Class classe
    Implements I1,I2,...

donde los Ij son interfaces.

3.6. Los espacios de nombres

Para escribir una línea en pantalla, utilizamos la instrucción

Console.Out.WriteLine(...)

Si observamos la definición de la clase Console


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

descubrimos que forma parte del espacio de nombres System. Esto significa que la clase Console debería designarse como System.Console y, de hecho, deberíamos escribir:

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

Esto se evita utilizando una cláusula imports:

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

Se dice que se importa el espacio de nombres System mediante la cláusula imports. Cuando el compilador encuentre el nombre de una clase (en este caso, Console), la buscará en los distintos espacios de nombres importados por las cláusulas imports. Aquí encontrará la clase Console en el espacio de nombres System. Observemos ahora la segunda información asociada a la clase Console:


Assembly: Mscorlib (in Mscorlib.dll)

Esta línea indica en qué «ensamblado» se encuentra la definición de la clase Console. Cuando se compila fuera de Visual Studio.NET y hay que proporcionar las referencias de los diferentes dll que contienen las clases que se deben utilizar, esta información puede resultar útil. Recordemos que para referenciar los dll necesarios para la compilación de una clase, se escribe:

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

Al crear una clase, se puede crear dentro de un espacio de nombres. El objetivo de estos espacios de nombres es evitar conflictos de nombres entre clases cuando estas se comercializan, por ejemplo. Consideremos dos empresas, E1 y E2, que distribuyen clases empaquetadas respectivamente en dll, E1.dll y E2.dll. Supongamos que un cliente C compra estos dos conjuntos de clases en los que ambas empresas han definido una clase personne. El cliente C compila un programa de la siguiente manera:

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

Si el código fuente prog.vb utiliza la clase personne, el compilador no sabrá si debe tomar la clase personne de E1.dll o la de E2.dll. Notificará un error. Si la empresa E1 se encarga de crear sus clases en un espacio de nombres llamado E1 y la empresa E2 en un espacio de nombres llamado E2, las dos clases personne se llamarán entonces E1.personne y E2.personne. El cliente deberá utilizar en sus clases E1.personne o E2.personne, pero no personne. El espacio de nombres permite eliminar la ambigüedad. Para crear una clase en un espacio de nombres, se escribe:


Namespace istia.st
    Public Class personne
        ' definición de la clase
...
  end Class
end Namespace

A modo de ejemplo, creemos en un espacio de nombres nuestra clase personne estudiada anteriormente. Elegiremos istia.st como espacio de nombres. La clase personne pasa a ser:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

' creación del espacio de nombres istia.st
Namespace istia.st
    Public Class personne
        ' atributos
        Private prenom As String
        Private nom As String
        Private age As Integer

        ' método
        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étodo
        Public Sub identifie()
            Console.Out.WriteLine((prenom & "," & nom & "," & age))
        End Sub
    End Class
End Namespace

Esta clase se compila en 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

Ahora utilicemos la clase personne en una clase de prueba:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System
Imports istia.st

' página de prueba
Public Module test
    Sub Main()
        Dim p1 As New personne
        p1.initialise("Jean", "Dupont", 30)
        p1.identifie()
    End Sub
End Module

Para evitar escribir


        Dim p1 As New istia.st.personne

hemos importado el espacio de nombres istia.st con una cláusula imports:


Imports istia.st

Ahora compilemos el programa de prueba:

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

Esto genera un archivo test.exe que, al ejecutarse, ofrece los siguientes resultados:

Jean,Dupont,30

3.7. El ejemplo IMPOTS

Retomamos el cálculo del impuesto ya estudiado en el capítulo anterior y lo procesamos utilizando una clase. Recordemos el problema:

Nos situamos en el caso simplificado de un contribuyente que solo tiene que declarar su salario:

  • se calcula el número de partes del asalariado nbParts=nbEnfants/2 +1 si no está casado, nbEnfants/2+2 si está casado, donde nbEnfants es su número de hijos.
  • si tiene al menos tres hijos, tiene media parte más
  • se calcula su renta imponible R=0,72*S, donde S es su salario anual
  • se calcula su coeficiente familiar QF = R / nbParts
  • se calcula su impuesto I. Consideremos la siguiente tabla:
12620,0
0
0
13190
0,05
631
15640
0,1
1290,5
24 740
0,15
2072,5
31 810
0,2
3309,5
39 970
0,25
4900
48 360
0,3
6898,5
55 790
0,35
9316,5
92 970
0,4
12106
127860
0,45
16754,5
151 250
0,50
231 47,5
172 040
0,55
30710
195 000
0,60
39312
0
0,65
49062

Cada línea tiene 3 campos. Para calcular el impuesto I, se busca la primera línea donde QF<=campo1. Por ejemplo, si QF=23000, se encontrará la línea

    24740        0.15        2072.5

El impuesto I es entonces igual a 0,15*R - 2072,5*nbParts. Si QF es tal que la relación QF<=campo1 nunca se cumple, entonces se utilizan los coeficientes de la última línea. En este caso:

    0                0.65        49062

lo que da el impuesto I=0,65*R - 49062*nbParts.

La clase de impuesto se definirá de la siguiente manera:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System

Public Class impot
    ' los datos necesarios para el cálculo del impuesto
    ' proceden de una fuente externa
    Private limites(), coeffR(), coeffN() As Decimal

    ' fabricante
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' se comprueba que las 3 tablas tengan el mismo tamaño
        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
        ' Todo correcto
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub

    ' cálculo del impuesto
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Long) As Long
        ' cálculo del número de participaciones
        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
        ' cálculo de la renta imponible y del coeficiente familiar
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' cálculo del impuesto
        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

Se crea un objeto impuesto con los datos que permiten calcular el impuesto de un contribuyente. Esta es la parte estable del objeto. Una vez creado este objeto, se puede llamar repetidamente a su método calcular, que calcula el impuesto del contribuyente a partir de su estado civil (casado o soltero), su número de hijos y su salario anual. Un programa de prueba podría ser el siguiente:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System
Imports Microsoft.VisualBasic

Module test
    Sub Main()
        ' programa interactivo de cálculo de impuestos
        ' el usuario introduce tres datos con el teclado: casado nbEnfants salario
        ' el programa muestra entonces el impuesto a pagar
        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"

        ' tablas de datos necesarias para el cálculo del impuesto
        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}

        ' creación de un objeto de impuesto
        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
        ' bucle infinito
        Dim marié As String
        Dim nbEnfants As Integer
        Dim salaire As Long
        While True
            ' se solicitan los parámetros para el cálculo del impuesto
            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()
            ' ¿Hay algo que hacer?
            If paramètres Is Nothing OrElse paramètres = "" Then
                Exit While
            End If
            ' verificación del número de argumentos en la línea introducida
            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
            ' verificación de la validez de los parámetros
            If Not erreur Then
                ' casado
                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
                ' salario
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    erreur = True
                End Try
            End If
            ' si los parámetros son correctos, se calcula el impuesto
            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

A continuación se muestra un ejemplo de ejecución del programa anterior:

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 :