Skip to content

4. Clases, estructuras, interfaces

4.1. El objeto a través del ejemplo

4.1.1. Aspectos generales

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 campos, atributos, etc.) y funciones (denominadas métodos). Un objeto se crea según un modelo denominado clase:

public class C1{
    Type1 p1;        // campo p1
    Type2 p2;        // campo p2
    
    Type3 m3(){        // método m3
        
    }
    Type4 m4(){        // método m4
        
    }
    
}

A partir de la clase anterior C1, se pueden crear numerosos objetos O1, O2,… Todos ellos tendrán los campos p1, p2,… y los métodos m3, m4,… Pero tendrán valores diferentes para sus campos pi, por lo que cada uno tendrá un estado propio. 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.

4.1.2. Creación del proyecto en C#

En los ejemplos anteriores, solo teníamos un único archivo fuente en un proyecto: Program.cs. A partir de ahora, podremos tener varios archivos fuente en un mismo proyecto. A continuación mostramos cómo hacerlo.

En [1], crea un nuevo proyecto. En [2], selecciona «Aplicación de consola». En [3], deja el valor por defecto. En [4], confirma. En [5], el proyecto que se ha generado. El contenido de Program.cs es el siguiente:


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

namespace ConsoleApplication1 {
    class Program {
        static void Main(string[] args) {
        }
    }
}

Guardemos el proyecto creado:

En [1], la opción de guardar. En [2], seleccione la carpeta donde desea guardar el proyecto. En [3], asigne un nombre al proyecto. En [5], indique que desea crear una solución. Una solución es un conjunto de proyectos. En [4], asigne un nombre a la solución. En [6], confirme el guardado.

En [1], el proyecto guardado. En [2], añade un nuevo elemento al proyecto.

En [1], indique que desea añadir una clase. En [2], el nombre de la clase. En [3], valide la información. En [4], el proyecto [01] tiene un nuevo archivo fuente Personne.cs:


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

namespace ConsoleApplication1 {
    class Personne {
    }
}

Modificamos el espacio de nombres de cada uno de los archivos fuente en Chap2 y eliminamos la importación de los espacios de nombres innecesarios:


using System;

namespace Chap2 {
    class Personne {
    }
}

using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
        }
    }
}

4.1.3. Definición de la clase «Persona»

La definición de la clase Personne en el archivo fuente [Personne.cs] será la siguiente:


using System;

namespace Chap2 {
    public class Personne {
        // atributos
        private string prenom;
        private string nom;
        private int age;

        // método
        public void Initialise(string P, string N, int age) {
            this.prenom = P;
            this.nom = N;
            this.age = age;
        }

        // método
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

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 clase. 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 establecer 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 método, esté o no definido 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).

Por lo 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 para declarar una clase en C es la siguiente:


public class C{
    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;
}

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

4.1.4. El método Initialise

Volvamos a nuestra clase «Persona», declarada como:


using System;

namespace Chap2 {
    public class Personne {
        // atributos
        private string prenom;
        private string nom;
        private int age;

        // método
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }

        // método
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

¿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:

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:

Personne p1;
p1.Initialise("Jean","Dupont",30);

La sintaxis p1.Initialise es válida, ya que Initialise es de acceso público.

4.1.5. El operador «new»

La secuencia de instrucciones

Personne p1;
p1.Initialise("Jean","Dupont",30);

es incorrecta. La instrucción

    Personne p1;

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:

Personne p1=null;

donde se indica explícitamente, mediante la palabra clave null, que la variable p1 aún no hace referencia a ningún objeto. Cuando a continuación 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:

Personne p1=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 null, y age tendrá el valor 0. Por lo tanto, se produce una inicialización por defecto. Ahora que p1 hace referencia a un objeto, la instrucción de inicialización de dicho objeto

p1.Initialise("Jean","Dupont",30);

es válida.

4.1.6. La palabra clave «this»

Veamos el código del método initialise:


        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
}

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

p1.Initialise("Jean","Dupont",30);

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


        public void Initialise(string p, string n, int age) {
            prenom = p;
            nom = n;
            this.age = age;
}

Cuando un método de un objeto hace referencia a un atributo A de dicho objeto, la notación this.A queda implícita. Debe utilizarse de forma explícita cuando exista un conflicto de identificadores. Este es el caso de la instrucción:


this.age=age;

donde age designa un atributo del objeto actual, así como el parámetro age recibido por el método. En ese caso, hay que resolver la ambigüedad designando el atributo age como this.age.

4.1.7. Un programa de prueba

A continuación se muestra un breve programa de prueba. Está escrito en el archivo fuente [Program.cs]:


using System;

namespace Chap2 {
    class P01 {
        static void Main() {
            Personne p1 = new Personne();
            p1.Initialise("Jean", "Dupont", 30);
            p1.Identifie();
        }
    }
}

Antes de ejecutar el proyecto [01], puede ser necesario especificar el archivo fuente que se va a ejecutar:

En las propiedades del proyecto [01], se indica en [1] la clase que se va a ejecutar.

Los resultados obtenidos tras la ejecución son los siguientes:

[Jean, Dupont, 30]

4.1.8. Otro método: Initialise

Sigamos considerando la clase Personne y añadámosle el siguiente método:


        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
}

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 (this). 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:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            Personne p1 = new Personne();
            p1.Initialise("Jean", "Dupont", 30);
            p1.Identifie();
            Personne p2 = new Personne();
            p2.Initialise(p1);
            p2.Identifie();
        }
    }
}

y sus resultados:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.9. Constructores de la clase Persona

Un constructor es un método que lleva el nombre de la clase y al que se recurre al crear el objeto. Se suele utilizar para inicializarlo. Es un método que puede aceptar argumentos, pero que no devuelve ningún resultado. Ni su prototipo ni su definición van precedidos de ningún tipo (ni siquiera void).

Si una clase C tiene un constructor que admite n argumentos argi, la declaración y la inicialización de un objeto de esta clase se pueden realizar de la siguiente forma:

        C objet =new C(arg1,arg2, ... argn);

o

        C objet;
        objet=new C(arg1,arg2, ... argn);

Cuando una clase C 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, dispone de uno por defecto, que es el constructor sin parámetros: public C(). Los atributos del objeto se inicializan entonces con valores por defecto. Esto es lo que ocurrió en los programas anteriores, en los que se había escrito:

    Personne p1;
    p1=new Personne();

Creemos dos constructores para nuestra clase Personne:


using System;

namespace Chap2 {
    public class Personne {
        // atributos
        private string prenom;
        private string nom;
        private int age;

        // constructores
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Initialise(P);
        }

        // método
        public void Initialise(string p, string n, int age) {
...
        }

        public void Initialise(Personne p) {
...
        }

        // método
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

Nuestros dos constructores se limitan a invocar los métodos Initialise que hemos estudiado anteriormente. Recordemos que, cuando en un constructor aparece, por ejemplo, la notación Initialise(p), el compilador la traduce como this.Initialise(p). Por lo tanto, en el constructor se invoca el método Initialise para trabajar con el objeto al que hace referencia this, es decir, el objeto actual, el que se está construyendo.

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


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            Personne p1 = new Personne("Jean", "Dupont", 30);
            p1.Identifie();
            Personne p2 = new Personne(p1);
            p2.Identifie();
        }
    }
}

y los resultados obtenidos:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.10. Las referencias de objetos

Siempre utilizamos la misma clase Personne. El programa de prueba queda así:


using System;

namespace Chap2 {
    class Program2 {
        static void Main() {
            // p1
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Console.Write("p1="); p1.Identifie();
            // p2 hace referencia al mismo objeto que p1
            Personne p2 = p1;
            Console.Write("p2="); p2.Identifie();
            // p3 hace referencia a un objeto que será una copia del objeto al que hace referencia p1
            Personne p3 = new Personne(p1);
            Console.Write("p3="); p3.Identifie();
            // se cambia el estado del objeto al que hace referencia p1
            p1.Initialise("Micheline", "Benoît", 67);
            Console.Write("p1="); p1.Identifie();
            // como p2 = p1, el objeto al que hace referencia p2 debe haber cambiado de estado
            Console.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.Write("p3="); p3.Identifie();
        }
    }
}

Los resultados obtenidos son los siguientes:

1
2
3
4
5
6
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]

Cuando se declara la variable p1 mediante

Personne p1=new Personne("Jean","Dupont",30);

p1 hace referencia al objeto Personne("Jean","Dupont",30), pero no es el objeto en sí mismo. 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=null;

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:

Personne p2=p1;

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

Cuando se escribe:

Personne p3=new Personne(p1);

se crea un nuevo objeto Personne. Este nuevo objeto será referenciado por p3. Si se modifica el objeto «apuntado» (o al que hace referencia) por p1, no se modifica en absoluto el objeto al que hace referencia p3. Así lo demuestran los resultados obtenidos.

4.1.11. Pase de parámetros de tipo referencia de objeto

En el capítulo anterior, hemos estudiado los modos de paso de los parámetros de una función cuando estos representaban un tipo simple de C# representado por una estructura .NET. Veamos qué ocurre cuando el parámetro es una referencia a un objeto:


using System;
using System.Text;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // ejemplo 4
            StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
            Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
            ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
            Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);

        }

        private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
            Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
            sbf0.Append("*****");
            sbf1 = new StringBuilder("essai1*****");
            sbf2 = new StringBuilder("essai2*****");
            sbf3 = new StringBuilder("essai3*****");
            Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
        }
    }
}
  • línea 8: define 3 objetos de tipo StringBuilder. Un objeto StringBuilder está cerca de un objeto string.. Al manipular un objeto string, se obtiene a cambio un nuevo objeto string. Así, en la secuencia de código:
string s="une chaîne";
s=s.ToUpperCase();

La línea 1 crea un objeto string en memoria y s es su dirección. En la línea 2, s.ToUpperCase() crea otro objeto string en memoria. Así, entre las líneas 1 y 2, s ha cambiado de valor (ahora apunta al nuevo objeto). La clase StringBuilder, por su parte, permite transformar una cadena sin que se cree un segundo objeto. Este es el ejemplo dado anteriormente:

  • línea 8: 4 referencias [sb0, sb1, sb2, sb3] a objetos de tipo StringBuilder
  • línea 10: se pasan al método ChangeStringBuilder con diferentes modos: sb0, sb1 con el modo por defecto, sb2 con la palabra clave «ref», sb3 con la palabra clave «out».
  • líneas 15-22: un método que tiene los parámetros formales [sbf0, sbf1, sbf2, sbf3]. Las relaciones entre los parámetros formales sbfi y los efectivos sbi son las siguientes:
  • sbf0 y sb0 son, al inicio del método, dos referencias distintas que apuntan al mismo objeto (paso por valor de las direcciones)
  • Lo mismo ocurre con sbf1 y sb1
  • sbf2 y sb2 son, al inicio del método, una misma referencia al mismo objeto (palabra clave ref)
  • sbf3 y sb3 son, tras la ejecución del método, una misma referencia al mismo objeto (palabra clave out)

Los resultados obtenidos son los siguientes:

1
2
3
4
Dans fonction appelante avant appel : sb0=essai0, sb1=essai1, sb2=essai2
Début fonction appelée : sbf0=essai0, sbf1=essai1, sbf2=essai2
Fin fonction appelée : sbf0=essai0*****, sbf1=essai1*****, sbf2=essai2*****, sbf3=essai3*****
Dans fonction appelante après appel : sb0=essai0*****, sb1=essai1, sb2=essai2*****, sb3=essai3*****

Explicaciones:

  • sb0 y sbf0 son dos referencias distintas al mismo objeto. Este se ha modificado mediante sbf0 (línea 3). Esta modificación se puede ver mediante sb0 (línea 4).
  • sb1 y sbf1 son dos referencias distintas al mismo objeto. El valor de sbf1 se modifica en el método y ahora apunta a un nuevo objeto (línea 3). Esto no altera en absoluto el valor de sb1, que sigue apuntando al mismo objeto (línea 4).
  • sb2 y sbf2 son la misma referencia al mismo objeto. El valor de sbf2 se modifica en el método y ahora apunta a un nuevo objeto (línea 3). Dado que sbf2 y sb2 son una misma entidad, el valor de sb2 también se ha modificado y sb2 apunta al mismo objeto que sbf2 —líneas 3 y 4—.
  • Antes de llamar al método, sb3 no tenía ningún valor. Tras la llamada al método, sb3 recibe el valor de sbf3. Por lo tanto, tenemos dos referencias al mismo objeto —líneas 3 y 4

4.1.12. Los objetos temporales

En una expresión, se puede invocar explícitamente el constructor de un objeto: este se crea, pero no tenemos acceso a él (para modificarlo, por ejemplo). Este objeto temporal se crea para evaluar la expresión y, a continuación, se descarta. El espacio de memoria que ocupaba será recuperado automáticamente más adelante 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 programa de prueba:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
        }
    }
}

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


        // constructores
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(P);
}

Obtenemos los siguientes resultados:

1
2
3
Constructeur Personne(string, string, int)
Constructeur Personne(Personne)
[Jean, Dupont, 30]

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

4.1.13. 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:


using System;

namespace Chap2 {
    public class Personne {
        // atributos
        private string prenom;
        private string nom;
        private int age;

        // constructores
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(p);
        }

        // método
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }

        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
        }

        // accesores
        public String GetPrenom() {
            return prenom;
        }
        public String GetNom() {
            return nom;
        }
        public int GetAge() {
            return age;
        }

        //modificadores
        public void SetPrenom(String P) {
            this.prenom = P;
        }
        public void SetNom(String N) {
            this.nom = N;
        }
        public void SetAge(int age) {
            this.age = age;
        }

        // método
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

Probamos la nueva clase con el siguiente programa:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p = 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() + ")");
        }
    }
}

y obtenemos los siguientes resultados:

1
2
3
Constructeur Personne(string, string, int)
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)

4.1.14. 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 se han sustituido por propiedades de lectura y escritura:


using System;

namespace Chap2 {
    public class Personne {
        // atributos
        private string prenom;
        private string nom;
        private int age;

        // constructores
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Initialise(p);
        }

        // método
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }

        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
        }

         // propiedades
        public string Prenom {
            get { return prenom; }
            set {
                // ¿Es válido el nombre?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//si
        }//nombre

        public string Nom {
            get { return nom; }
            set {
                // ¿Apellido válido?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
            }//si
        }//apellido


        public int Age {
            get { return age; }
            set {
                // ¿edad válida?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
            }//si
        }//edad

        // método
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

Una propiedad permite leer (get) o establecer (set) el valor de un atributo. Una propiedad se declara de la siguiente manera:

public Type Propriété{
    get {...}
    set {...}
}

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 (aunque 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. Eso es lo que se hace aquí.

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


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p = 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 (Exception ex) {
                Console.Error.WriteLine(ex.Message);
            }//try-catch
        }
    }
}

En la instrucción


    Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");

se pretende 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 este caso, se invoca el método set de dicha propiedad. Este recibirá el valor 56 en su parámetro value.

Una propiedad P de una clase C que solo defina 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:

1
2
3
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. Otra característica de las propiedades es que pueden utilizarse junto con un constructor siguiendo la siguiente sintaxis:

Classe objet=new Classe (...) {Propriété1=val1, Propriété2=val2, ...}

Esta sintaxis es equivalente al siguiente código:

1
2
3
4
Classe objet=new Classe(...);
objet.Propriété1=val1;
objet.Propriété2=val2;
...

El orden de las propiedades no importa. A continuación se muestra un ejemplo.

A la clase Personne se le añade un nuevo constructor sin parámetros:


        public Personne() {
}

El constructor no inicializa los miembros del objeto. Es lo que se denomina «constructor por defecto». Es el que se utiliza cuando la clase no define ningún constructor.

El siguiente código crea e inicializa (línea 6) un nuevo objeto Personne con la sintaxis presentada anteriormente:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
            Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
        }
    }
}

En la línea 6 anterior, se utiliza el constructor sin parámetros Personne(). En este caso concreto, también se podría haber escrito


            Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };

pero los paréntesis del constructor Personne() sin parámetros no son obligatorios en esta sintaxis.

Los resultados de la ejecución son los siguientes:

p2=(Arthur,Martin,7)

En muchos casos, los métodos get y set de una propiedad se limitan a leer y escribir un campo privado sin ningún otro procesamiento. En este escenario, se puede utilizar una propiedad automática declarada de la siguiente manera:

public Type Propriété{ get ; set ; }

El campo privado asociado a la propiedad no se declara. Lo genera automáticamente el compilador. Solo se accede a él a través de su propiedad. Así, en lugar de escribir:


    private string prenom;
...
     // ¿propiedad asociada?
        public string Prenom {
            get { return prenom; }
            set {
                // ¿nombre válido?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//if
        }//nombre

se puede escribir:

public string Prenom {get; set;}

sin declarar el campo privado prenom. La diferencia entre las dos propiedades anteriores es que la primera comprueba la validez del nombre en set, mientras que la segunda no realiza ninguna comprobación.

Utilizar la propiedad automática Prenom equivale a declarar un campo Prenom como público:

public string Prenom;

Cabe preguntarse si existe alguna diferencia entre ambas declaraciones. No se recomienda declarar public como campo de una clase. Esto rompe con el concepto de encapsulación del estado de un objeto, estado que debe ser privado y exponerse mediante métodos públicos.

Si la propiedad automática se declara como virtuelle,, entonces puede redefinirse en una clase hija:


    class Class1 {
        public virtual string Prop { get; set; }
}

    class Class2 : Class1 {
        public override string Prop { get { return base.Prop; } set {... } }
}

En la línea 2 anterior, la clase hija Class2 puede incluir en set, código que compruebe la validez del valor asignado a la propiedad automática base.Prop de la clase madre Class1.

4.1.15. Los 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 a 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 una instancia concreta de dicha clase, se declara de forma diferente con la palabra clave static:


        private static long nbPersonnes;

Para hacer referencia a él, se escribe Personne.nbPersonnes para indicar que es un atributo de la propia clase Personne. En este caso, hemos creado un atributo privado al que no se podrá acceder directamente desde 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 es un atributo de toda una clase. Por lo tanto, se necesita una propiedad declarada también como static:


        public static long NbPersonnes {
            get { return nbPersonnes; }
}

que, desde el exterior, se invocará con la sintaxis Personne.NbPersonnes. A continuación se muestra un ejemplo.

La clase Personne queda así:


using System;

namespace Chap2 {
    public class Personne {

        // atributos de clase
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }

        // atributos de instancia
        private string prenom;
        private string nom;
        private int age;

        // constructores
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
            nbPersonnes++;
        }
        public Personne(Personne p) {
            Initialise(p);
            nbPersonnes++;
        }

...
}

En las líneas 20 y 24, los constructores incrementan el campo estático de la línea 7.

Con el siguiente programa:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Personne p2 = new Personne(p1);
            new Personne(p1);
            Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
        }
    }
}

se obtienen los siguientes resultados:

    Nombre de personnes créées : 3

4.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:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            // una tabla de personas
            Personne[] amis = new Personne[3];
            amis[0] = new Personne("Jean", "Dupont", 30);
            amis[1] = new Personne("Sylvie", "Vartan", 52);
            amis[2] = new Personne("Neil", "Armstrong", 66);
            // visualización
            foreach (Personne ami in amis) {
                ami.Identifie();
            }
        }
    }
}
  • línea 7: crea una matriz de 3 elementos de tipo Personne. Estos 3 elementos se inicializan aquí con los valores null y c.a.d, ya que no hacen referencia a ningún objeto. Una vez más, por un uso incorrecto del lenguaje, se habla 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 (presencia de new), no crea ningún objeto del tipo de sus elementos: esto hay que hacerlo después.
  • líneas 8-10: creación de los 3 objetos de tipo Personne
  • líneas 12-14: visualización del contenido del array amis

Se obtienen los siguientes resultados:

1
2
3
[Jean, Dupont, 30]
[Sylvie, Vartan, 52]
[Neil, Armstrong, 66]

4.2. La herencia a través del ejemplo

4.2.1. Aspectos generales

Aquí abordamos el concepto de herencia. El objetivo de la herencia es «personalizar» una clase existente para que se adapte a 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 propios 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 crear una clase Enseignant desde cero, preferiríamos aprovechar lo ya establecido en la clase Personne y adaptarlo al carácter particular de los profesores. El concepto de herencia es lo que nos permite hacerlo.

Para expresar que la clase Enseignant hereda las propiedades de la clase Personne, escribiremos:

    public class Enseignant : Personne

Personne se denomina clase padre (o madre) y 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:


using System;

namespace Chap2 {
    public class Personne {

        // atributos de clase
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }

        // atributos de instancia
        private string prenom;
        private string nom;
        private int age;

        // constructores
        public Personne(String prenom, String nom, int age) {
            Nom = nom;
            Prenom = prenom;
            Age = age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(string, string, int)");
        }
        public Personne(Personne p) {
            Nom = p.Nom;
            Prenom = p.Prenom;
            Age = p.Age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(Personne)");
        }

        // propiedades
        public string Prenom {
            get { return prenom; }
            set {
                // ¿Es válido el nombre?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//si
        }//nombre

        public string Nom {
            get { return nom; }
            set {
                // ¿Apellido válido?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
            }//si
        }//apellido

        public int Age {
            get { return age; }
            set {
                // ¿Edad válida?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
            }//si
        }//edad

        // propiedad
        public string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
        }
    }

}

El método Identifie ha sido sustituido por la propiedad Identite, de solo lectura, que identifica a la persona. Creamos una clase Enseignant que hereda de la clase Personne:


using System;

namespace Chap2 {
    class Enseignant : Personne {
        // atributos
        private int section;

        // constructor
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            // se almacena la sección mediante la propiedad Section
            Section = section;
            // seguimiento
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
        }//constructor

        // propiedad Section
        public int Section {
            get { return section; }
            set { section = value; }
        }// Sección

    }
}

La clase Enseignant añade a los métodos y atributos de la clase Personne:

  • línea 4: la clase Enseignant deriva de la clase Personne
  • línea 6: 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 asignatura, a grandes rasgos). Se puede acceder a este atributo privado a través de la propiedad pública Section de las líneas 18-21
  • línea 9: un nuevo constructor que permite inicializar todos los atributos de un profesor

4.2.2. Creación de un objeto «Profesor»

Una clase hija no hereda los constructores de su clase padre. Por lo tanto, debe definir sus propios constructores. El constructor de la clase Enseignant es el siguiente:


        // constructor
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            // se guarda la sección
            Section = section;
            // seguimiento
            Console.WriteLine("Construction enseignant(string, string, int, int)");
}//fabricante

La declaración


        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {

indica que el constructor recibe cuatro parámetros: prenom, nom, age, section y pasa tres ((prenom,nom,age)) a su clase base, en este caso la clase Personne. Sabemos que esta clase tiene un constructor Persona(string, string, int) que permitirá crear una persona con los parámetros pasados (prenom,nom,age). Una vez finalizada la creación de la clase base, la creación del objeto Enseignant continúa con la ejecución del cuerpo del constructor:

            // se guarda la sección
            Section = section;

Cabe señalar que, a la izquierda del signo =, no se ha utilizado el atributo section del objeto, sino la propiedad Section asociada a él. Esto permite al constructor aprovechar los posibles controles de validez que pudiera realizar este método. De este modo, se evita tener que colocarlos en dos lugares diferentes: el constructor y la propiedad.

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:


// constructor
  public Enseignant(string prenom, string nom, int age, int section){
    this.prenom=prenom;
        this.nom=nom;
        this.age=age;
      this.section=section;
  }

Es imposible. La clase Personne ha declarado como 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 utilizar métodos públicos para acceder a ellos. La situación 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 crear un objeto hijo, primero se llama al constructor del objeto padre y, a continuación, se completan las inicializaciones propias del objeto hijo (section en nuestro ejemplo).

Probemos con un primer programa de prueba [Program.cs]:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
        }
    }
}

Este programa se limita a crear un objeto Enseignant (new) e identificarlo. La clase Enseignant no tiene un método Identite, pero su clase padre sí tiene uno que, además, es público: por herencia, se convierte en un método público de la clase Enseignant.

El proyecto completo es el siguiente:

Los resultados obtenidos son los siguientes:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Jean, Dupont, 30]

Se observa que:

  • se ha creado un objeto Personne (línea 1) antes que el objeto Enseignant (línea 2)
  • la identidad obtenida es la del objeto Personne

4.2.3. Redefinición 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:


using System;

namespace Chap2 {
    class Enseignant : Personne {
        // atributos
        private int section;

        // constructor
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            // se almacena la sección mediante la propiedad Section
            Section = section;
            // seguimiento
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
        }//constructor

        // propiedad Section
        public int Section {
            get { return section; }
            set { section = value; }
        }// sección

        // propiedad Identidad
        public new string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
        }
    }
}

En las líneas 24-26, la propiedad Identite de la clase Enseignant se basa en la propiedad Identite de su clase madre (base.Identite) (línea 25) para mostrar su parte «Personne» y, a continuación, se completa con el campo section, propio de la clase Enseignant. Cabe destacar la declaración de la propiedad Identite:


    public new string Identite{

Supongamos un objeto enseignant E. Este objeto contiene en su interior un objeto Personne:

La propiedad Identite está definida 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 «new» para indicar que se está redefiniendo una nueva propiedad Identite para la clase Enseignant.


    public new string Identite{

La clase Enseignant dispone ahora de dos propiedades Identite:

  • la heredada de la clase padre Personne
  • y una propia

Si E es un objeto Enseignant, E.Identite hace referencia a la propiedad Identite de la clase Enseignant. Se dice que la propiedad Identite de la clase hija redefine u oculta la propiedad Identite de la clase madre. 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 padre de su clase padre, si existe
  • etc…

La herencia permite, por tanto, redefinir en la clase hija los métodos o propiedades que tengan 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 redefinición de métodos y propiedades es el principal interés de la herencia.

Consideremos el mismo programa de prueba que antes:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
        }
    }
}

Los resultados obtenidos esta vez son los siguientes:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Jean, Dupont, 30],27]

4.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, además de otras. 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, además de 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 también a cualquier objeto derivado de la clase Ci,, se denomina polimorfismo: la capacidad de una variable para hacer referencia a diferentes tipos de objetos.

Veamos un ejemplo y consideremos la siguiente función independiente de cualquier clase (static):

    public static void Affiche(Personne p){
        ….
    }

También podríamos escribir

    Personne p;
    ...
    Affiche(p);

como

    Enseignant e;
    ...
    Affiche(e);

En este último caso, el parámetro formal p, de tipo Personne, del método estático Affiche recibirá un valor de tipo Enseignant. Dado que el tipo Enseignant deriva del tipo Personne, esto es válido.

4.2.5. Redefinición y polimorfismo

Completemos nuestro método Affiche:


        public static void Affiche(Personne p) {
            // muestra la identidad de p
            Console.WriteLine(p.Identite);
}//muestra

La propiedad p.Identite devuelve una cadena de caracteres que identifica al objeto Persona p. ¿Qué ocurre en el ejemplo anterior si el parámetro pasado al método Affiche es un objeto de tipo Enseignant?:


            Enseignant e = new Enseignant(...);
            Affiche(e);

Veamos el siguiente ejemplo:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // un profesor
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
            // una persona
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }

        // cartel
        public static void Affiche(Personne p) {
            // muestra la identidad de p
            Console.WriteLine(p.Identite);
        }//muestra
    }
}

Los resultados obtenidos son los siguientes:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Lucile, Dumas, 56]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

La ejecución muestra que la instrucción p.Identite (línea 17) ha ejecutado en cada ocasión la propiedad Identite de una Personne, primero (línea 7) la persona contenida en la Enseignant e, y luego (línea 10) la propia Personne p. No se ha adaptado al objeto realmente pasado como parámetro a Affiche. Hubiéramos preferido disponer de la identidad completa del e de Enseignant. 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 virtual en la clase base Personne:


public virtual string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
        }

La palabra clave «virtual» convierte a Identite en una propiedad virtual. Esta palabra clave también se puede aplicar a los métodos. Las clases derivadas que redefinan una propiedad o un método virtual deben utilizar la palabra clave override en lugar de new para calificar su propiedad o método redefinido. Así, en la clase Enseignant, la propiedad Identite se redefine de la siguiente manera:


        public override string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}

El programa anterior genera entonces los siguientes resultados:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

Esta vez, en la línea 3, se ha obtenido la identidad completa del profesor. Redefinamos ahora un método en lugar de una propiedad. La clase object (alias en C# de System.Object) es la clase «madre» de todas las clases de C#. Así, cuando se escribe:

    public class Personne

se escribe implícitamente:

    public class Personne : System.Object

La clase System.Object define un método virtual ToString:

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:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // un profesor
            Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
            // una persona
            Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
        }
    }
}

Los resultados obtenidos son los siguientes:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Chap2.Enseignant
Constructeur Personne(string, string, int)
Chap2.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 ha sido capaz de mostrar el nombre real de la clase del objeto.

Redefinamos el método ToString en las clases Personne y Enseignant:


        // método ToString
        public override string ToString() {
            return Identite;
}

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


using System;
namespace Chap2 {
    class Program3 {
        public static void Main() {
            // un profesor
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
            // una persona
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
        // cartel
        public static void Affiche(Personne p) {
            // cartel de identidad de p
            Console.WriteLine(p);
        }//Cartel
    }
}

Centrémonos en el método Affiche, que admite como parámetro una persona p. En la línea 15, el método WriteLine de la clase Console no tiene ninguna variante que admita un parámetro de tipo Personne. Entre las diferentes variantes de Writeline, hay una que admite como parámetro un tipo Object. El compilador utilizará este método, WriteLine(Object o), porque esta firma significa que el parámetro o puede ser de tipo Object o derivado. Dado que Object es la clase padre de todas las clases, cualquier objeto puede pasarse como parámetro a WriteLine y, por lo tanto, un objeto de tipo Personne o Enseignant. El método WriteLine(Object o) escribe o.ToString() en el flujo de escritura Out. Dado que el método ToString es virtual, si el objeto o (de tipo Object o derivado) ha redefinido el método ToString, se utilizará este último. Este es el caso de las clases Personne y Enseignant.

Así lo demuestran los resultados de la ejecución:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

4.3. Redefinir el significado de un operador para una clase

4.3.1. Introducción

Consideremos la instrucción

op1 + op2

donde op1 y op2 son dos operandos. Es posible redefinir el significado del operador +. Si el operando op1 es un objeto de la clase C1, hay que definir un método estático en la clase C1 con la siguiente firma:

public static [type] operator +(C1 opérande1, C2 opérande2);

Cuando el compilador encuentra la instrucción

op1 + op2

, la traduce a C1.operator+(op1,op2). El tipo devuelto por el método operator es importante. De hecho, consideremos la operación op1+op2+op3. El compilador la traduce como (op1+op2)+op3. Supongamos que res12 es el resultado de op1+op2. La operación que se realiza a continuación es res12+op3. Si res12 es de tipo C1, también se traducirá como C1.operator+(res12,op3). Esto permite encadenar las operaciones.

También se pueden redefinir los operadores unarios que solo tienen un operando. Así, si op1 es un objeto de tipo C1, la operación op1++ puede redefinirse mediante un método estático de la clase C1:

public static [type] operator ++(C1 opérande1);

Lo expuesto aquí es válido para la mayoría de los operadores, aunque con algunas excepciones:

  • los operadores == y != deben redefinirse al mismo tiempo
  • los operadores &&, ||, [], (), +=, -=, ... no se pueden redefinir

4.3.2. Un ejemplo

Creamos una clase ListeDePersonnes derivada de la clase ArrayList. Esta clase implementa una lista dinámica y se presenta en el capítulo siguiente. De esta clase, solo utilizamos los siguientes elementos:

  • el método L.Add(Object o), que permite añadir a la lista L un objeto o. En este caso, el objeto o será un objeto Personne.
  • la propiedad L.Count, que indica el número de elementos de la lista L
  • la notación L[i], que devuelve el elemento i de la lista L

La clase ListeDePersonnes heredará todos los atributos, métodos y propiedades de la clase ArrayList. Su definición es la siguiente:


using System;
using System.Collections;
using System.Text;

namespace Chap2 {
    class ListeDePersonnes : ArrayList{
        // redefinición del operador +, para añadir una persona a la lista
        public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
            // se añade la persona p a la ListeDePersonnes l
            l.Add(p);
            // se convierte la ListeDePersonnes l
            return l;
        }// operador +

        // ToString
        public override string ToString() {
            // devuelve (elemento1, elemento2, ..., elemento n)
            // paréntesis de apertura
            StringBuilder listeToString = new StringBuilder("(");
            // se recorre la lista de personas (this)
            for (int i = 0; i < Count - 1; i++) {
                listeToString.Append(this[i]).Append(",");
            }//for
            // último elemento
            if (Count != 0) {
                listeToString.Append(this[Count-1]);
            }
            // paréntesis de cierre
            listeToString.Append(")");
            // hay que devolver una cadena
            return listeToString.ToString();
        }//ToString
    }
}
  • línea 6: la clase ListeDePersonnes deriva de la clase ArrayList
  • líneas 8-13: definición del operador + para la operación l + p, donde l es de tipo ListeDePersonnes y p es de tipo Personne o derivado.
  • línea 10: la persona p se añade a la lista l. Aquí se utiliza el método Add de la clase padre ArrayList.
  • línea 12: se devuelve la referencia a la lista l para poder encadenar los operadores +, como en l + p1 + p2. La operación l+p1+p2 se interpretará (por prioridad de los operadores) como (l+p1)+p2. La operación l+p1 devolverá la referencia l. La operación (l+p1)+p2 se convierte entonces en l+p2, que añade a la persona p2 a la lista de personas l.
  • línea 16: redefinimos el método ToString para mostrar una lista de personas en el formato (persona1, persona2, ...) donde personnei es, a su vez, el resultado del método ToString de la clase Personne.
  • Línea 19: utilizamos un objeto de tipo StringBuilder. Esta clase resulta más adecuada que la clase string cuando es necesario realizar numerosas operaciones sobre la cadena de caracteres, en este caso, adiciones. De hecho, cada operación sobre un objeto string devuelve un nuevo objeto string, mientras que las mismas operaciones sobre un objeto StringBuilder modifican el objeto sin crear uno nuevo. Utilizamos el método Append para concatenar las cadenas de caracteres.
  • línea 21: se recorren los elementos de la lista de personas. Esta lista se designa aquí como «this». Es el objeto actual sobre el que se ejecuta el método ToString. La propiedad Count es una propiedad de la clase padre ArrayList.
  • línea 22: se puede acceder al elemento n.º i de la lista actual this mediante la notación this[i]. Una vez más, se trata de una propiedad de la clase ArrayList. Como se trata de añadir cadenas, se utilizará el método this[i].ToString(). Dado que este método es virtual, se utilizará el método ToString del objeto this, de tipo Personne o derivado.
  • Línea 31: debemos devolver un objeto de tipo string (línea 16). La clase StringBuilder tiene un método ToString que permite pasar de un tipo StringBuilder a un tipo string.

Cabe señalar que la clase ListeDePersonnes no tiene constructor. En este caso, sabemos que el constructor

public ListeDePersonnes(){
}

será el utilizado. Este constructor no hace nada más que llamar al constructor sin parámetros de su clase padre:

public ArrayList(){
...
}

Una clase de prueba podría ser la siguiente:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // una lista de personas
            ListeDePersonnes l = new ListeDePersonnes();
            // Añadir personas
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
            // visualización
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
        }
    }
}
  • línea 7: creación de una lista de personas en la
  • línea 9: añadir 2 personas con el operador +
  • línea 12: añadir un profesor
  • líneas 11 y 13: uso del método redefinido ListeDePersonnes.ToString().

Resultados:

l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])

4.4. Definir un indexador para una clase

Aquí seguimos utilizando la clase ListeDePersonnes. Si l es un objeto ListeDePersonnes, queremos poder utilizar la notación l[i] para designar a la persona n.º i de la lista l tanto en lectura (Persona p=l[i]) como en escritura (l[i]=new Persona(...)).

Para poder escribir l[i], donde l[i] designa un objeto Personne, debemos definir en la clase ListeDePersonnes el siguiente método this:


        public Personne this[int i] {
            get { ... }
            set { ... }
}

Al método this[int i] se le denomina «indexador», ya que da sentido a la expresión obj[i], que recuerda la notación de las matrices, aunque obj no sea una matriz, sino un objeto. El método get del método this delobjeto obj se invoca cuando se escribe variable=obj[i], y el método set cuando se escribe obj[i]=valor.

La clase ListeDePersonnes deriva de la clase ArrayList, que a su vez tiene un indexador:

    public object this[int i] { ... }

Existe un conflicto entre el método this de la clase ListeDePersonnes:


 public Personne this[int i] 

y el método this de la clase ArrayList


 public object this[int i] 

porque tienen el mismo nombre y admiten el mismo tipo de parámetro (int). Para indicar que el método this de la clase ListeDePersonnes «oculta» el método del mismo nombre de la clase ArrayList, es necesario añadir la palabra clave new a la declaración del indexador de ListeDePersonnes. Por lo tanto, se escribirá:


    public new Personne this[int i]{
        get { ... }
        set { ... }
    }

Completemos este método. El método this.get se invoca cuando se escribe, por ejemplo, variable=l[i], donde l es de tipo ListeDePersonnes. En ese caso, hay que devolver la persona n.º i de la lista l. Esto se hace con la notación base[i], que devuelve el objeto n.º i de la clase ArrayList, subyacente a la clase ListeDePersonnes. Dado que el objeto devuelto es de tipo Object, es necesario realizar una conversión de tipo a la clase Personne.


    public new Personne this[int i]{
        get { return (Personne) base[i]; }
        set { ... }
    }

El método set se invoca cuando se escribe l[i]=p, donde p es un Personne. En este caso, se trata de asignar la persona p al elemento i de la lista l.


    public new Personne this[int i]{
        get { ... }
        set { base[i]=value; }
    }

En este caso, la persona p, representada por la palabra clave value, se asigna al elemento n.º i de la clase base ArrayList.

Por lo tanto, el indexador de la clase ListeDePersonnes será el siguiente:


    public new Personne this[int i]{
        get { return (Personne) base[i]; }
        set { base[i]=value; }
    }

Ahora queremos poder escribir también «Persona p=l["nom"]», c.a.d, e indexar la lista l no por un número de elemento, sino por el nombre de una persona. Para ello, definimos un nuevo indexador:


        // búsqueda por nombre
        public int this[string nom] {
            get {
                // se busca a la persona
                for (int i = 0; i < Count; i++) {
                    if (((Personne)base[i]).Nom == nom)
                        return i;
                }//for
                return -1;
            }//obtener
}

La primera línea


public int this[string nom]

indica que se indexa la clase ListeDePersonnes mediante una cadena de caracteres nom y que el resultado de l[nom] es un entero. Este entero será la posición en la lista de la persona con el nombre nom o -1 si dicha persona no figura en la lista. Solo se define la propiedad get,, lo que impide la asignación l["nom"]=valor, que habría requerido la definición de la propiedad set. La palabra clave new no es necesaria en la declaración del indexador, ya que la clase base ArrayList no define ningún indexador this[string].

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

El programa de prueba anterior se completa de la siguiente manera:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // una lista de personas
            ListeDePersonnes l = new ListeDePersonnes();
            // Añadir personas
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
            // visualización
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
            // modificación del elemento 1
            l[1] = new Personne("franck", "gallon",5);
            // visualización del elemento 1
            Console.WriteLine("l[1]=" + l[1]);
            // visualización de la lista l
            Console.WriteLine("l=" + l);
            // búsqueda de personas
            string[] noms = { "martin", "germain", "xx" };
            for (int i = 0; i < noms.Length; i++) {
                int inom = l[noms[i]];
                if (inom != -1)
                    Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
                else
                    Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
            }//para
        }
    }
}

Al ejecutarlo, se obtienen los siguientes resultados:

1
2
3
4
5
6
7
l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])
l[1]=[franck, gallon, 5]
l=([jean, martin, 10],[franck, gallon, 5],Enseignant[[camille, germain, 27],60])
Personne(martin)=[jean, martin, 10]
Personne(germain)=Enseignant[[camille, germain, 27],60]
Personne(xx) n'existe pas

4.5. Las estructuras

La estructura en C# es análoga a la del lenguaje C y se asemeja mucho al concepto de clase. Una estructura se define de la siguiente manera:

struct NomStructure{
// atributos
    ...
// propiedades
...
// constructores
...
// métodos
...
}

A pesar de que su declaración es similar, 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:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // una estructura sp1
            SPersonne sp1;
            sp1.Nom = "paul";
            sp1.Age = 10;
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            // una estructura sp2
            SPersonne sp2 = sp1;
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
            // se modifica sp2
            sp2.Nom = "nicole";
            sp2.Age = 30;
            // verificación de sp1 y sp2
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");

            // un objeto op1
            CPersonne op1=new CPersonne();
            op1.Nom = "paul";
            op1.Age = 10;
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            // un objeto op2
            CPersonne op2=op1;
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
            // se modifica op2
            op2.Nom = "nicole";
            op2.Age = 30;
            // verificación de op1 y op2
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
        }
    }
    // estructura SPersonne
    struct SPersonne {
        public string Nom;
        public int Age;
    }

    // clase CPersonne
    class CPersonne {
        public string Nom;
        public int Age;
    }

}
  • líneas 38-41: una estructura con dos campos públicos: Nom, Age
  • líneas 44-47: una clase con dos campos públicos: Nom, Age

Si se ejecuta este programa, se obtienen los siguientes resultados:

1
2
3
4
5
6
7
8
sp1=SPersonne(paul,10)
sp2=SPersonne(paul,10)
sp1=SPersonne(paul,10)
sp2=SPersonne(nicole,30)
op1=CPersonne(paul,10)
op2=CPersonne(paul,10)
op1=CPersonne(nicole,30)
op2=CPersonne(nicole,30)

Donde antes se utilizaba una clase Personne, ahora utilizamos una estructura SPersonne:


    struct SPersonne {
        public string Nom;
        public int Age;
}

La estructura no tiene aquí ningún constructor. Podría tener uno, como veremos más adelante. Por defecto, siempre dispone del constructor sin parámetros, en este caso SPersonne().

  • Línea 7 del código: la declaración

    SPersonne sp1;

es equivalente a la instrucción:


    SPersonne sp1=new Spersonne();

Se crea una estructura (Nombre,Edad) y el valor de sp1 es la propia estructura. En el caso de la clase, la creación del objeto (Nombre,Edad) debe realizarse explícitamente mediante el operador new (línea 22):


CPersonne op1=new CPersonne();

La instrucción anterior crea un objeto CPersonne (más o menos el equivalente a nuestra estructura) y el valor de p1 es, por tanto, la dirección (la referencia) de dicho 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 op1 es la dirección del objeto creado

Cuando en el programa se escribe la línea 12:


            SPersonne sp2 = sp1;

se crea una nueva estructura sp2(Nombre,Edad) y se inicializa con el valor de sp1,, es decir, la propia estructura.

La estructura de sp1 se duplica en sp2 [1]. Se trata de una copia de valores. Consideremos ahora la instrucción de la línea 27:


CPersonne op2=op1;

En el caso de las clases, el valor de op1 se copia en op2, pero como este valor es, en realidad, la dirección del objeto, este no se duplica: [2].

En el caso de la estructura [1], 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 [2], si se modifica el objeto al que apunta op2, se modifica también el objeto al que apunta op1, ya que es el mismo. Esto es lo que muestran también los resultados del programa.

De estas explicaciones se desprende, 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, tal y como muestra el siguiente ejemplo:


using System;

namespace Chap2 {

    // estructura SPersonne
    struct SPersonne {
        // atributos privados
        private string nom;
        private int age;

        // propiedades
        public string Nom {
            get { return nom; }
            set { nom = value; }
        }//nombre

        public int Age {
            get { return age; }
            set { age = value; }
        }//edad

        // Fabricante
        public SPersonne(string nom, int age) {
            this.nom = nom;
            this.age = age;
        }//fabricante

        // ToString
        public override string ToString() {
            return "SPersonne(" + Nom + "," + Age + ")";
        }//ToString
    }//estructura
}//espacio de nombres
  • líneas 8-9: dos campos privados
  • líneas 12-20: las propiedades públicas asociadas
  • líneas 23-26: se define un constructor. Cabe señalar que el constructor sin parámetros SPersonne() siempre está presente y no es necesario declararlo. El compilador rechaza su declaración. En el constructor de las líneas 23-26, podría resultar tentador inicializar los campos privados nom y age a través de sus propiedades públicas Nom y Age. El compilador lo rechaza. Los métodos de la estructura no se pueden utilizar durante su construcción.
  • Líneas 29-31: redefinición del método ToString.

Un programa de prueba podría ser el siguiente:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // una persona p1
            SPersonne p1=new SPersonne();
            p1.Nom="paul";
            p1.Age= 10;
            Console.WriteLine("p1={0}",p1);
            // una persona p2
            SPersonne p2 = p1;
            Console.WriteLine("p2=" + p2);
            // se modifica p2
            p2.Nom = "nicole";
            p2.Age = 30;
            // verificación de p1 y p2
            Console.WriteLine("p1=" + p1);
            Console.WriteLine("p2=" + p2);
            // una persona p3
            SPersonne p3 = new SPersonne("amandin", 18);
            Console.WriteLine("p3=" + p3);
            // una persona p4
            SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
            Console.WriteLine("p4=" + p4);
        }
    }
}
  • línea 7: nos vemos obligados a utilizar explícitamente el constructor sin parámetros, ya que existe otro constructor en la estructura. Si la estructura no tuviera ningún constructor, la instrucción

            SPersonne p1;

habría bastado para crear una estructura vacía.

  • líneas 8-9: la estructura se inicializa a través de sus propiedades públicas
  • línea 10: el método p1.ToString se va a utilizar en el WriteLine.
  • línea 21: creación de una estructura con el constructor SPersonne(string, int)
  • línea 24: creación de una estructura con el constructor sin parámetros SPersonne(), con la inicialización de los campos privados entre llaves mediante sus propiedades públicas.

Se obtienen los siguientes resultados de ejecución:

1
2
3
4
5
6
p1=SPersonne(paul,10)
p2=SPersonne(paul,10)
p1=SPersonne(paul,10)
p2=SPersonne(nicole,30)
p3=SPersonne(amandin,18)
p4=SPersonne(x,10)

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

4.6. Las interfaces

Una interfaz es un conjunto de prototipos de métodos o propiedades que conforman 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 el que verifica dicha implementación.

A continuación se muestra, por ejemplo, la definición de la interfaz System.Collections.IEnumerator:

public interface System.Collections.IEnumerator 
{

    // Propiedades
    Object Current { get; }

    // Métodos
    bool MoveNext();
    void Reset();
}

Las propiedades y los métodos de la interfaz solo se definen mediante sus firmas. No están implementados (no tienen código). Son las clases que implementan la interfaz las que proporcionan código a los métodos y propiedades de la interfaz.

1
2
3
4
5
6
public class C : IEnumerator{
    ...
    Object Current{ get {...}}
    bool MoveNext{...}
    void Reset(){...}
}
  • línea 1: la clase C implementa la clase IEnumerator. Cabe señalar que el signo : utilizado para la implementación de una interfaz es el mismo que el utilizado para la derivación de una clase.
  • Líneas 3-5: la implementación de los métodos y propiedades de la interfaz IEnumerator.

Consideremos la siguiente interfaz:


namespace Chap2 {
    public interface IStats {
        double Moyenne { get; }
        double EcartType();
    }
}

La interfaz IStats presenta:

  • una propiedad de solo lectura Moyenne: para calcular la media de una serie de valores
  • un método EcartType: para calcular su desviación típica

Cabe señalar que en ningún sitio se especifica de qué serie de valores se trata. Puede tratarse de la media de las notas de una clase, de la media mensual de las ventas de un producto concreto, de la temperatura media en un lugar determinado, etc. Este es el principio de las interfaces: se supone la existencia de métodos en el objeto, pero no la de datos concretos.

Una primera clase de implementación de la interfaz IStats podría ser una clase que sirva para almacenar las notas de los alumnos de una clase en una asignatura determinada. Un alumno se caracterizaría por la siguiente estructura Elève:


    public struct Elève {
        public string Nom { get; set; }
        public string Prénom { get; set; }
}//Alumno

El alumno se identificaría por su nombre y apellidos. En las líneas 2 y 3 se encuentran las propiedades automáticas para estos dos atributos.

Una nota se caracterizaría por la siguiente estructura: Note:


    public struct Note {
        public Elève Elève { get; set; }
        public double Valeur { get; set; }
}//Nota

La nota se identificaría mediante el alumno calificado y la propia nota. En las líneas 2 y 3 se encuentran las propiedades automáticas para estos dos atributos.

Las notas de todos los alumnos en una asignatura determinada se recogen en la clase TableauDeNotes siguiente:


using System;
using System.Text;

namespace Chap2 {

    public class TableauDeNotes : IStats {
        // atributos
        public string Matière { get; set; }
        public Note[] Notes { get; set; }
        public double Moyenne { get; private set; }
        private double ecartType;

        // constructor
        public TableauDeNotes(string matière, Note[] notes) {
            // almacenamiento mediante propiedades públicas
            Matière = matière;
            Notes = notes;
            // cálculo de la media de las notas
            double somme = 0;
            for (int i = 0; i < Notes.Length; i++) {
                somme += Notes[i].Valeur;
            }
            if (Notes.Length != 0) Moyenne = somme / Notes.Length;
            else Moyenne = -1;
            // desviación estándar
            double carrés = 0;
            for (int i = 0; i < Notes.Length; i++) {
                carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
            }//for
            if (Notes.Length != 0)
                ecartType = Math.Sqrt(carrés / Notes.Length);
            else ecartType = -1;
        }//constructor

        public double EcartType() {
            return ecartType;
        }

        // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
            // se concatenan todas las notas
            for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
            //última nota
            if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
            // fin
            return valeur.ToString();
        }//ToString

    }//clase
}
  • Línea 6: la clase TableauDeNotes implementa la interfaz IStats. Por lo tanto, debe implementar la propiedad Moyenne y el método EcartType. Estas se implementan en las líneas 10 (Moyenne) y 35-37 (EcartType)
  • líneas 8-10: tres propiedades automáticas
  • línea 8: la asignatura cuyas notas almacena el objeto
  • línea 9: la tabla de notas de los alumnos (Alumno, Nota)
  • línea 10: la media de las notas —propiedad que implementa la propiedad Moyenne de la interfaz IStats.
  • línea 11: campo que almacena la desviación estándar de las notas - el método get asociado a EcartType de las líneas 35-37 implementa el método EcartType de la interfaz IStats.
  • línea 9: las notas se almacenan en un array. Este se pasa, durante la construcción de la clase TableauDeNotes, al constructor de las líneas 14-33.
  • Líneas 14-33: el constructor. Se supone aquí que las notas transmitidas al constructor no volverán a modificarse posteriormente. Por ello, se utiliza el constructor para calcular inmediatamente la media y la desviación típica de dichas notas y almacenarlas en los campos de las líneas 10-11. La media se almacena en el campo privado subyacente a la propiedad automática Moyenne de la línea 10 y la desviación típica en el campo privado de la línea 11.
  • línea 10: el método get de la propiedad automática Moyenne devolverá el campo privado subyacente.
  • Líneas 35-37: el método EcartType devuelve el valor del campo privado de la línea 11.

Hay algunas sutilezas en este código:

  • línea 23: el método set de la propiedad Moyenne se utiliza para realizar la asignación. Este método se ha declarado privado en la línea 10 para que la asignación de un valor a la propiedad Moyenne solo sea posible dentro de la clase.
  • Líneas 40-54: utilizan un objeto StringBuilder para construir la cadena que representa el objeto TableauDeNotes con el fin de mejorar el rendimiento. Cabe señalar que la legibilidad del código se ve muy afectada. Esa es la otra cara de la moneda.

En la clase anterior, las notas se almacenaban en un array. No era posible añadir una nueva nota tras la creación del objeto TableauDeNotes. Ahora proponemos una segunda implementación de la interfaz IStats, denominada ListeDeNotes, en la que, en esta ocasión, las notas se guardarían en una lista, con la posibilidad de añadir notas tras la creación inicial del objeto ListeDeNotes.

El código de la clase ListeDeNotes es el siguiente:


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

namespace Chap2 {

    public class ListeDeNotes : IStats {
        // atributos
        public string Matière { get; set; }
        public List<Note> Notes { get; set; }
        public double moyenne = -1;
        public double ecartType = -1;

        // constructor
        public ListeDeNotes(string matière, List<Note> notes) {
            // almacenamiento mediante propiedades públicas
            Matière = matière;
            Notes = notes;
        }//constructor

        // Añadir una nota
        public void Ajouter(Note note) {
            // Añadir la nota
            Notes.Add(note);
            // media y desviación estándar reiniciadas
            moyenne = -1;
            ecartType = -1;
        }

        // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
            // se concatenan todas las notas
            for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
            //última nota
            if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
            // fin
            return valeur.ToString();
        }//ToString

        // media de las notas
        public double Moyenne {
            get {
                if (moyenne != -1) return moyenne;
                // cálculo de la media de las notas
                double somme = 0;
                for (int i = 0; i < Notes.Count; i++) {
                    somme += Notes[i].Valeur;
                }
                // se calcula la media
                if (Notes.Count != 0) moyenne = somme / Notes.Count;
                return moyenne;
            }
        }

        public double EcartType() {
            // desviación estándar
            if (ecartType != -1) return ecartType;
            // media
            double moyenne = Moyenne;
            double carrés = 0;
            for (int i = 0; i < Notes.Count; i++) {
                carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
            }//for
            // se muestra la desviación típica
            if (Notes.Count != 0)
                ecartType = Math.Sqrt(carrés / Notes.Count);
            return ecartType;
        }
    }//clase
}
  • línea 7: la clase ListeDeNotes implementa la interfaz IStats
  • línea 10: las notas se almacenan ahora en una lista en lugar de en un array
  • línea 11: la propiedad automática Moyenne de la clase TableauDeNotes se ha sustituido aquí por un campo privado moyenne, línea 11, asociado a la propiedad pública de solo lectura Moyenne de las líneas 48-60
  • líneas 22-28: ahora se puede añadir una nota a las ya almacenadas, algo que antes no se podía hacer.
  • líneas 15-19: por lo tanto, la media y la desviación estándar ya no se calculan en el constructor, sino en los propios métodos de la interfaz: Moyenne (líneas 48-60) y EcartType (62-76). Sin embargo, el recálculo solo se vuelve a ejecutar si la media y la desviación estándar son distintas de -1 (líneas 50 y 64).

Una clase de prueba podría ser la siguiente:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // algunos alumnos y notas de inglés
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
            // que se guardan en un objeto TableauDeNotes
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
            // visualización de la media y la desviación estándar
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
            // se introducen los alumnos y la asignatura en un objeto ListeDeNotes
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
            // visualización de la media y la desviación típica
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
            // se añade una nota
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
            // Visualización de la media y la desviación típica
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
        }
    }
}
  • línea 8: creación de una matriz de alumnos utilizando el constructor sin parámetros e inicialización mediante las propiedades públicas
  • línea 9: creación de un array de notas siguiendo la misma técnica
  • línea 11: un objeto TableauDeNotes del que se calculan la media y la desviación estándar; línea 13
  • Línea 15: un objeto ListeDeNotes del que se calculan la media y la desviación típica en la línea 17. La clase List<Note> tiene un constructor que admite un objeto que implementa la interfaz IEnumerable<Note>. La matriz notes1 implementa esta interfaz y puede utilizarse para construir el objeto List<Note>.
  • línea 19: se añade una nueva nota
  • línea 21: recálculo de la media y la desviación típica

Los resultados de la ejecución son los siguientes:

1
2
3
matière=anglais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18],[Jérôme,Jaric,10]), Moyenne=14,5, Ecart-type=2,95803989154981

En el ejemplo anterior, dos clases implementan la interfaz IStats. Dicho esto, el ejemplo no pone de manifiesto la utilidad de la interfaz IStats. Reescribamos el programa de prueba de la siguiente manera:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // algunos alumnos y notas de inglés
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
            // que se guardan en un objeto TableauDeNotes
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
            // visualización de la media y la desviación estándar
            AfficheStats(anglais);
            // se introducen los alumnos y la asignatura en un objeto ListeDeNotes
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
            // visualización de la media y la desviación típica
            AfficheStats(français);
            // se añade una nota
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
            // Visualización de la media y la desviación típica
            AfficheStats(français);
        }

        // Visualización de la media y la desviación típica de un tipo IStats
        static void AfficheStats(IStats valeurs) {
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
        }
    }
}
  • líneas 25-27: el método estático AfficheStats recibe como parámetro un tipo IStats, es decir, un tipo «Interfaz». Esto significa que el parámetro efectivo puede ser cualquier objeto que implemente la interfaz IStats. Cuando se utiliza un dato que tiene el tipo de una interfaz, esto significa que solo se utilizarán los métodos de la interfaz implementados por el dato. Se hace abstracción del resto. Se trata de una propiedad similar al polimorfismo que hemos visto en el caso de 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 que implementarían todas las clases en cuestión. Las instancias de estas clases Ci pueden utilizarse entonces como parámetros efectivos de funciones que admiten un parámetro formal de tipo I, c.a.d, es decir, funciones que solo utilizan los métodos de los objetos Ci definidos en la interfaz I y no los atributos ni los métodos específicos de las diferentes clases Ci.
  • línea 13: se invoca el método AfficheStats con un tipo TableauDeNotes que implementa la interfaz IStats
  • línea 17: lo mismo con un tipo ListeDeNotes

Los resultados de la ejecución son idénticos a los de la anterior.

Una variable puede ser de tipo interfaz. Así, se puede escribir:

1
2
3
IStats stats1=new TableauDeNotes(...);
...
stats1=new ListeDeNotes(...);

La declaración de la línea 1 indica que stats1 es la instancia de una clase que implementa la interfaz IStats. Esta declaración implica que el compilador solo permitirá el acceso en stats1 a los métodos de la interfaz: la propiedad Moyenne y el método EcartType.

Por último, cabe señalar que la implementación de interfaces puede ser múltiple, como en el caso de c.a.d, que se puede escribir

public class ClasseDérivée:ClasseDeBase,I1,I2,..,In{
...
}

donde los Ij son interfaces.

4.7. Las clases abstractas

Una clase abstracta es una clase de la que no se pueden crear instancias. Es necesario crear clases derivadas que sí puedan instanciarse.

Se pueden utilizar clases abstractas para factorizar el código de una línea de clases. Veamos el siguiente caso:


using System;

namespace Chap2 {
    abstract class Utilisateur {
        // campos
        private string login;
        private string motDePasse;
        private string role;

        // fabricante
        public Utilisateur(string login, string motDePasse) {
            // se registra la información
            this.login = login;
            this.motDePasse = motDePasse;
            // se identifica al usuario
            role=identifie();
            // ¿Identificado?
            if (role == null) {
                throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
            }
        }

        // toString
        public override string ToString() {
            return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
        }

        // identifica
        abstract public string identifie();
    }
}
  • líneas 11-21: el constructor de la clase Utilisateur. Esta clase almacena información sobre el usuario de una aplicación web. Dicha aplicación cuenta con diversos tipos de usuarios autenticados mediante nombre de usuario y contraseña (líneas 6-7). Estos dos datos se verifican mediante un servicio LDAP para algunos usuarios, mediante un SGBD para otros, etc.
  • líneas 13-14: se almacenan los datos de autenticación
  • línea 16: se comprueba mediante un método «identifie». Dado que se desconoce el método de identificación, se declara como abstracto en la línea 29 con la palabra clave «abstract». El método identifie devuelve una cadena de caracteres que especifica el rol del usuario (en resumen, lo que tiene permiso para hacer). Si esta cadena es el puntero null, se lanza una excepción en la línea 19.
  • Línea 4: dado que tiene un método abstracto, la propia clase se declara abstracta con la palabra clave «abstract».
  • Línea 29: el método abstracto «identify» no tiene definición. Serán las clases derivadas las que le proporcionen una.
  • líneas 24-26: el método ToString, que identifica una instancia de la clase.

Se supone aquí que el desarrollador quiere tener el control sobre la creación de instancias de la clase Utilisateur y de las clases derivadas, quizá porque quiere asegurarse de que se lance una excepción de un tipo determinado si no se reconoce al usuario (línea 19). Las clases derivadas podrán basarse en este constructor. Para ello, deberán proporcionar el método «identifica».

La clase ExceptionUtilisateurInconnu es la siguiente:


using System;

namespace Chap2 {
    class ExceptionUtilisateurInconnu : Exception {
        public ExceptionUtilisateurInconnu(string message) : base(message){
        }
    }
}
  • línea 3: deriva de la clase Exception
  • líneas 4-6: solo tiene un único constructor que admite como parámetro un mensaje de error. Este se pasa a la clase padre (línea 5), que tiene ese mismo constructor.

Ahora derivamos la clase Utilisateur de la clase hija Administrateur:


namespace Chap2 {
    class Administrateur : Utilisateur {
        // fabricante
        public Administrateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        // identifica
        public override string identifie() {
            // identificación LDAP
            // ...
            return "admin";
        }
    }
}
  • líneas 4-6: el constructor se limita a pasar a su clase padre los parámetros que recibe
  • líneas 9-12: el método «identifica» de la clase Administrateur. Supongamos que un administrador se identifica mediante un sistema LDAP. Este método redefine el método «identifica» de su clase padre. Dado que redefine un método abstracto, no es necesario incluir la palabra clave override.

Ahora derivamos la clase Utilisateur de la clase hija Observateur:


namespace Chap2 {
    class Observateur : Utilisateur{
        // fabricante
        public Observateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        //identifica
        public override string identifie() {
            // identificación SGBD
            // ...
            return "observateur";
        }
        
    }
}
  • líneas 4-6: el constructor se limita a pasar a su clase padre los parámetros que recibe
  • líneas 9-13: el método de identificación de la clase Observateur. Se supone que se identifica a un observador mediante la verificación de sus datos de identificación en una base de datos.

Finalmente, los objetos Administrateur y Observateur son instanciados por el mismo constructor, el de la clase padre Utilisateur.. Este constructor utilizará el método «identifica» que proporcionan estas clases.

Una tercera clase, Inconnu, también deriva de la clase Utilisateur:


namespace Chap2 {
    class Inconnu : Utilisateur{

        // fabricante
        public Inconnu(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        //identifica
        public override string identifie() {
            // usuario desconocido
            // ...
            return null;
        }
        
    }
}
  • línea 13: el método identifie devuelve el puntero null para indicar que no se ha reconocido al usuario.

Un programa de prueba podría ser el siguiente:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Observateur("observer","mdp1"));
            Console.WriteLine(new Administrateur("admin", "mdp2"));
            try {
                Console.WriteLine(new Inconnu("xx", "yy"));
            } catch (ExceptionUtilisateurInconnu e) {
                Console.WriteLine("Utilisateur non connu : "+ e.Message);
            }
        }
    }
}

Cabe señalar que en las líneas 6, 7 y 9, el método [Utilisateur].ToString() es el que utilizará el método WriteLine.

Los resultados de la ejecución son los siguientes:

1
2
3
Utilisateur[observer,mdp1,observateur]
Utilisateur[admin,mdp2,admin]
Utilisateur non connu : [xx,yy]

4.8. Las clases, interfaces y métodos genéricos

Supongamos que queremos escribir un método que intercambie dos números enteros. Este método podría ser el siguiente:


        public static void Echanger1(ref int value1, ref int value2){
            // se intercambian las referencias value1 y value2
            int temp = value2;
            value2 = value1;
            value1 = temp;
}

Ahora bien, si quisiéramos intercambiar dos referencias a objetos Personne, escribiríamos:


        public static void Echanger2(ref Personne value1, ref Personne value2){
            // se intercambian las referencias «value1» y «value2»
            Personne temp = value2;
            value2 = value1;
            value1 = temp;
}

Lo que diferencia a ambos métodos es el tipo T de los parámetros: int en lugar de Echanger1, y Personne en lugar de Echanger2. Las clases e interfaces genéricas responden a la necesidad de disponer de métodos que solo difieran en el tipo de algunos de sus parámetros.

Con una clase genérica, el método Echanger podría reescribirse de la siguiente manera:


namespace Chap2 {
    class Generic1<T> {
        public static void Echanger(ref T value1, ref T value2){
            // se intercambian las referencias «value1» y «value2»
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • línea 2: la clase Generic1 se parametriza con un tipo denominado T. Se le puede dar el nombre que se desee. Este tipo T se reutiliza posteriormente en la clase en las líneas 3 y 5. Se dice que la clase Generic1 es una clase genérica.
  • línea 3: define las dos referencias a un tipo T que se van a intercambiar
  • línea 5: la variable temporal temp es de tipo T.

Un programa de prueba de la clase podría ser el siguiente:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            // int
            int i1 = 1, i2 = 2;
            Generic1<int>.Echanger(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // cadena
            string s1 = "s1", s2 = "s2";
            Generic1<string>.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // Persona
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic1<Personne>.Echanger(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);

        }
    }
}
  • Línea 8: cuando se utiliza una clase genérica parametrizada por los tipos T1, T2, ... estos deben estar «instanciados». En la línea 8, se utiliza el método estático Echanger del tipo Generic1<int> para indicar que las referencias pasadas al método Echanger son de tipo int.
  • Línea 12: se utiliza el método estático Echanger del tipo Generic1<string> para indicar que las referencias pasadas al método Echanger son de tipo string.
  • línea 16: se utiliza el método estático Echanger del tipo Generic1<Personne> para indicar que las referencias pasadas al método Echanger son de tipo Personne.

Los resultados de la ejecución son los siguientes:

1
2
3
i1=2,i2=1
s1=s2,s2=s1
p1=[pauline, dard, 55],p2=[jean, clu, 20]

El método Echanger también se podría haber escrito de la siguiente manera:


namespace Chap2 {
    class Generic2 {
        public static void Echanger<T>(ref T value1, ref T value2){
            // se intercambian las referencias value1 y value2
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • línea 2: la clase Generic2 ya no es genérica
  • línea 3: el método estático Echanger es genérico

El programa de prueba queda entonces así:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // int
            int i1 = 1, i2 = 2;
            Generic2.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // cadena
            string s1 = "s1", s2 = "s2";
            Generic2.Echanger<string>(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // Persona
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic2.Echanger<Personne>(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
        }
    }
}
  • líneas 8, 12 y 16: se llama al método Echanger especificando entre <> el tipo de los parámetros. De hecho, el compilador es capaz de deducir, a partir del tipo de los parámetros efectivos, la variante del método Echanger que debe utilizarse. Por lo tanto, la siguiente sintaxis es válida:

            Generic2.Echanger(ref i1, ref i2);
...
            Generic2.Echanger(ref s1, ref s2);
...
            Generic2.Echanger(ref p1, ref p2);

Líneas 1, 3 y 5: ya no se especifica la variante del método Echanger que se invoca. El compilador es capaz de deducirla a partir de la naturaleza de los parámetros efectivos utilizados.

Se pueden establecer restricciones en los parámetros genéricos:

Image

Consideremos el siguiente nuevo método genérico Echanger:


namespace Chap2 {
    class Generic3 {
        public static void Echanger<T>(ref T value1, ref T value2) where T : class {
            // Se intercambian las referencias «value1» y «value2»
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • línea 3: se exige que el tipo T sea una referencia (clase, interfaz)

Consideremos el siguiente programa de prueba:


using System;

namespace Chap2 {
    class Program4 {
        static void Main(string[] args) {
            // int
            int i1 = 1, i2 = 2;
            Generic3.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // cadena
            string s1 = "s1", s2 = "s2";
            Generic3.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // Persona
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic3.Echanger(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);

        }
    }
}

El compilador da un error en la línea 8 porque el tipo int no es una clase ni una interfaz, sino una estructura:

Image

Consideremos el siguiente método genérico nuevo Echanger:


namespace Chap2 {
    class Generic4 {
        public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
            // se recuperan los valores de los dos elementos
            int value1 = element1.Value();
            int value2 = element2.Value();
            // si el primer elemento es mayor que el segundo, se intercambian los elementos
            if (value1 > value2) {
                T temp = element2;
                element2 = element1;
                element1 = temp;
            }
        }
    }
}
  • línea 3: el tipo T debe implementar la interfaz Interface1. Esta tiene un método Value, utilizado en las líneas 5 y 6, que devuelve el valor del objeto de tipo T.
  • líneas 8-12: las dos referencias element1 y element2 solo se intercambian si el valor de element1 es mayor que el valor de element2.

La interfaz Interface1 es la siguiente:


namespace Chap2 {
    interface Interface1 {
        int Value();
    }
}

Se implementa mediante la siguiente clase Class1:


using System;
using System.Threading;

namespace Chap2 {
    class Class1 : Interface1 {
        // valor del objeto
        private int value;

        // constructor
        public Class1() {
            // espera 1 ms
            Thread.Sleep(1);
            // valor aleatorio entre 0 y 99
            value = new Random(DateTime.Now.Millisecond).Next(100);
        }

        // accesor del campo privado «value»
        public int Value() {
            return value;
        }

        // estado de la instancia
        public override string ToString() {
            return value.ToString();
        }
    }
}
  • línea 5: Class1 implementa la interfaz Interface1
  • línea 7: el valor de una instancia de Class1
  • líneas 10-14: el campo value se inicializa con un valor aleatorio entre 0 y 99
  • líneas 18-20: el método Value de la interfaz Interface1
  • líneas 23-25: el método ToString de la clase

La interfaz Interface1 también está implementada por la clase Class2:


using System;

namespace Chap2 {
    class Class2 : Interface1 {
        // valores del objeto
        private int value;
        private String s;

        // constructor
        public Class2(String s) {
            this.s = s;
            value = s.Length;
        }

        // accesor del valor del campo privado
        public int Value() {
            return value;
        }

        // estado de la instancia
        public override string ToString() {
            return s;
        }
    }
}
  • línea 4: Class2 implementa la interfaz Interface1
  • línea 6: el valor de una instancia de Class2
  • líneas 10-13: el campo value se inicializa con la longitud de la cadena de caracteres pasada al constructor
  • líneas 16-18: el método Value de la interfaz Interface1
  • líneas 21-22: el método ToString de la clase

Un programa de prueba podría ser el siguiente:


using System;

namespace Chap2 {
    class Program5 {
        static void Main(string[] args) {
            // intercambiar instancias del tipo Class1
            Class1 c1, c2;
            for (int i = 0; i < 5; i++) {
                c1 = new Class1();
                c2 = new Class1();
                Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
                Generic4.Echanger(ref c1, ref c2);
                Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
            }
            // intercambiar instancias del tipo Class2
            Class2 c3, c4;
            c3 = new Class2("xxxxxxxxxxxxxx");
            c4 = new Class2("xx");
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
            Generic4.Echanger(ref c3, ref c4);
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
        }
    }
}
  • líneas 8-14: se intercambian instancias de tipo Class1
  • líneas 16-22: se intercambian instancias de tipo Class2

Los resultados de la ejecución son los siguientes:

Avant échange --> c1=43,c2=79
Après échange --> c1=43,c2=79
Avant échange --> c1=72,c2=56
Après échange --> c1=56,c2=72
Avant échange --> c1=92,c2=75
Après échange --> c1=75,c2=92
Avant échange --> c1=11,c2=47
Après échange --> c1=11,c2=47
Avant échange --> c1=31,c2=67
Après échange --> c1=31,c2=67
Avant échange --> c3=xxxxxxxxxxxxxx,c4=xx
Après échange --> c3=xx,c4=xxxxxxxxxxxxxx

Para ilustrar el concepto de interfaz genérica, vamos a ordenar una matriz de personas primero por sus nombres y luego por sus edades. El método que nos permite ordenar una matriz es el método estático Sort de la clase Array:

Image

Recordemos que un método estático se utiliza anteponiendo al método el nombre de la clase y no el de una instancia de la clase. El método Sort tiene diferentes firmas (está sobrecargado). Utilizaremos la siguiente firma:

public static void Sort<T>(T[] tableau, IComparer<T> comparateur)

Sort es un método genérico en el que T designa cualquier tipo. El método recibe dos parámetros:

  • T[] matriz: la matriz de elementos de tipo T que se va a ordenar
  • IComparer<T> comparador: una referencia a un objeto que implementa la interfaz IComparer<T>.

IComparer<T> es una interfaz genérica definida de la siguiente manera:

1
2
3
public interface IComparer<T>{
    int Compare(T t1, T t2);
}

La interfaz IComparer<T> solo tiene un único método. El método Compare:

  • recibe como parámetros dos elementos t1 y t2 de tipo T
  • devuelve 1 si t1 > t2, 0 si t1 == t2 y -1 si t1 < t2. Es el desarrollador quien debe asignar un significado a los operadores <, == y >. Por ejemplo, si p1 y p2 son dos objetos Personne, se podrá decir que p1 > p2 si el nombre de p1 precede al de p2 en orden alfabético. De este modo, se obtendrá una ordenación ascendente según el nombre de las personas. Si se desea una ordenación según la edad, se dirá que p1 > p2 si la edad de p1 es mayor que la de p2.
  • Para obtener una ordenación en orden decreciente, basta con invertir los resultados +1 y -1

Ya sabemos lo suficiente para ordenar una tabla de personas. El programa es el siguiente:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program6 {
        static void Main(string[] args) {
            // una tabla de personas
            Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
            // visualización
            Affiche("Tableau à trier", personnes1);
            // ordenar por nombre
            Array.Sort(personnes1, new CompareNoms());
            // visualización
            Affiche("Tableau après le tri selon les nom et prénom", personnes1);
            // ordenar por edad
            Array.Sort(personnes1, new CompareAges());
            // visualización
            Affiche("Tableau après le tri selon l'âge", personnes1);
        }

        static void Affiche(string texte, Personne[] personnes) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (Personne p in personnes) {
                Console.WriteLine(p);
            }
        }
    }

    // clase de comparación de nombres y apellidos de las personas
    class CompareNoms : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
            // se comparan los apellidos
            int i = p1.Nom.CompareTo(p2.Nom);
            if (i != 0)
                return i;
            // igualdad de apellidos: se comparan los nombres
            return p1.Prenom.CompareTo(p2.Prenom);
        }
    }

    // clase de comparación de edades de las personas
    class CompareAges : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
            // se comparan las edades
            if (p1.Age > p2.Age)
                return 1;
            else if (p1.Age == p2.Age)
                return 0;
            else
                return -1;
        }
    }

}
  • línea 8: la matriz de personas
  • línea 12: la ordenación de la matriz de personas según el nombre y los apellidos. El segundo parámetro del método genérico Sort es una instancia de la clase CompareNoms que implementa la interfaz genérica IComparer<Persona>.
  • líneas 30-39: la clase CompareNoms que implementa la interfaz genérica IComparer<Persona>.
  • líneas 31-38: implementación del método genérico int CompareTo(T,T) de la interfaz IComparer<T>. El método utiliza el método String.CompareTo, presentado en el apartado 3.3.5.4, para comparar dos cadenas de caracteres.
  • línea 16: ordenación de la tabla de personas por edades. El segundo parámetro del método genérico Sort es una instancia de la clase CompareAges, que implementa la interfaz genérica IComparer<Persona> y se define en las líneas 42-51.

Los resultados de la ejecución son los siguientes:

Tableau à trier-----------------------------------
[claude, pollon, 25]
[valentine, germain, 35]
[paul, germain, 32]
Tableau après le tri selon les nom et prénom------
[paul, germain, 32]
[valentine, germain, 35]
[claude, pollon, 25]
Tableau après le tri selon l'âge------------------
[claude, pollon, 25]
[paul, germain, 32]
[valentine, germain, 35]

4.9. Los espacios de nombres

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

Console.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.WriteLine(...)

Esto se evita utilizando una cláusula using:

using System;
...
Console.WriteLine(...)

Se dice que se importa el espacio de nombres System con la cláusula using. Cuando el compilador encuentre el nombre de una clase (en este caso, Console), buscará dicha clase en los distintos espacios de nombres importados mediante las cláusulas using. En este caso, encontrará la clase Console en el espacio de nombres System. Fijémonos ahora en 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 y hay que proporcionar las referencias de los distintos dll que contienen las clases que se van a utilizar, esta información puede resultar útil. Para hacer referencia a los dll necesarios para compilar una clase, se escribe:

csc /r:fic1.dll /r:fic2.dll ... prog.cs

donde csc es el compilador de C#. Al crear una clase, se puede hacer dentro de un espacio de nombres. El objetivo de estos espacios de nombres es evitar conflictos de nombres entre clases cuando, por ejemplo, estas se comercializan. 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 denominada Personne. El cliente C compila un programa de la siguiente manera:

csc /r:e1.dll /r:e2.dll prog.cs

Si el código fuente prog.cs utiliza la clase Personne, el compilador no sabrá si debe tomar la clase Personne de e1.dll o la de e2.dll. Mostrará 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 o bien E1.Personne, o bien 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 EspaceDeNoms{
     // definición de la categoría
}

4.10. Ejemplo de aplicación: V2

Retomamos el cálculo del impuesto ya estudiado en el capítulo anterior, apartado 3.6, y lo abordamos ahora utilizando clases e interfaces. Recordemos el problema:

Nos proponemos escribir un programa que permita calcular el impuesto de un contribuyente. Consideramos el caso simplificado de un contribuyente que solo tiene que declarar su salario (cifras de 2004 correspondientes a los ingresos de 2003):

  • Se calcula el número de participaciones del empleado: nbParts = nbEnfants/2 + 1 si no está casado, nbEnfants/2 + 2 si está casado, donde nbEnfants es el número de hijos que tiene.
  • si tiene al menos tres hijos, tiene media parte más
  • se calcula su base 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:
4262
0
0
8382
0,0683
291,09
14 753
0,1914
1322,92
23 888
0,2826
2668,39
38 868
0,3738
4846,98
47 932
0,4262
6883,66
0
0,4809
9505,54

Cada línea tiene 3 campos. Para calcular el impuesto I, se busca la primera línea en la que QF sea menor o igual que el campo 1. Por ejemplo, si QF = 5000, se encontrará la línea

    8382        0.0683        291.09

El impuesto I es entonces igual a 0,0683*R - 291,09*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.4809    9505.54

lo que da como resultado el impuesto I = 0,4809 * R - 9505,54 * nbParts.

En primer lugar, definimos una estructura capaz de encapsular una línea de la tabla anterior:


namespace Chap2 {
    // un tramo impositivo
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

A continuación, definimos una interfaz IImpot capaz de calcular el impuesto:


namespace Chap2 {
    interface IImpot {
        int calculer(bool marié, int nbEnfants, int salaire);
    }
}
  • línea 3: el método de cálculo del impuesto a partir de tres datos: el estado civil del contribuyente (casado o soltero), el número de hijos y su salario

A continuación, definimos una clase abstracta que implementa esta interfaz:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {

        // los tramos impositivos necesarios para el cálculo del impuesto
        // proceden de una fuente externa

        protected TrancheImpot[] tranchesImpot;

        // cálculo del impuesto
        public int calculer(bool marié, int nbEnfants, int salaire) {
            // cálculo del número de participaciones
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
            // cálculo de la base imponible y del coeficiente familiar
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // cálculo del impuesto
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
            // resultado
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//calcular
    }//Clase

}
  • línea 2: la clase AbstractImpot implementa la interfaz IImpot.
  • línea 7: los datos anuales del cálculo del impuesto en forma de un campo protegido. La clase AbstractImpot no sabe cómo se inicializará este campo. Deja esta tarea en manos de las clases derivadas. Por eso se declara abstracta (línea 2), para impedir cualquier instanciación.
  • líneas 10-25: la implementación del método calculer de la interfaz IImpot. Las clases derivadas no tendrán que reescribir este método. La clase AbstractImpot sirve, por tanto, como clase de factorización de las clases derivadas. En ella se incluye lo que es común a todas las clases derivadas.

Una clase que implemente la interfaz IImpot puede crearse derivando de la clase AbstractImpot. Eso es lo que vamos a hacer ahora:


using System;

namespace Chap2 {
    class HardwiredImpot : AbstractImpot {

        // tablas de datos necesarias para el cálculo del impuesto
        decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };

        public HardwiredImpot() {
                // creación de la tabla de tramos impositivos
            tranchesImpot = new TrancheImpot[limites.Length];
                // cumplimentación
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
    }// clase
}// espacio de nombres

La clase HardwiredImpot define, en las líneas 7-9, de forma fija los datos necesarios para el cálculo del impuesto. Su constructor (líneas 11-18) utiliza estos datos para inicializar el campo protegido tranchesImpot de la clase madre AbstractImpot.

Un programa de prueba podría ser el siguiente:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            // programa interactivo de cálculo de impuestos
            // El usuario introduce tres datos mediante el teclado: casado nbEnfants salario
            // A continuación, el programa muestra el impuesto a pagar

            const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
                            + "Marié : o pour marié, n pour non marié\n"
                            + "NbEnfants : nombre d'enfants\n"
                            + "Salaire : salaire annuel en F";

            // creación de un objeto IImpot
            IImpot impot = new HardwiredImpot();

            // bucle infinito
            while (true) {
                // se solicitan los parámetros para el cálculo del impuesto
                Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
                string paramètres = Console.ReadLine().Trim();
                // ¿Hay que hacer algo?
                if (paramètres == null || paramètres == "") break;
                // Comprobación del número de argumentos en la línea introducida
                string[] args = paramètres.Split(null);
                int nbParamètres = args.Length;
                if (nbParamètres != 3) {
                    Console.WriteLine(syntaxe);
                    continue;
                }//if
                // Comprobación de la validez de los parámetros
                // casado
                string marié = args[0].ToLower();
                if (marié != "o" && marié != "n") {
                    Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
                    continue;
                }//if
                // nbEnfants
                int nbEnfants = 0;
                bool dataOk = false;
                try {
                    nbEnfants = int.Parse(args[1]);
                    dataOk = nbEnfants >= 0;
                } catch {
                }//if
                // ¿Datos correctos?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
                    continue;
                }
                // salario
                int salaire = 0;
                dataOk = false;
                try {
                    salaire = int.Parse(args[2]);
                    dataOk = salaire >= 0;
                } catch {
                }//try-catch
                // ¿Datos correctos?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
                    continue;
                }
                // los parámetros son correctos: se calcula el impuesto
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                // siguiente contribuyente
            }//while
        }
    }
}

El programa anterior permite al usuario realizar simulaciones repetidas del cálculo de impuestos.

  • línea 16: creación de un objeto impot que implementa la interfaz IImpot. Este objeto se obtiene mediante la instanciación de un tipo HardwiredImpot, un tipo que implementa la interfaz IImpot. Cabe señalar que a la variable impot no se le ha asignado el tipo HardwiredImpot, sino el tipo IImpot. Al escribir esto, se indica que solo nos interesa el método calculer del objeto impot y no el resto.
  • líneas 19-68: el bucle de simulaciones para el cálculo del impuesto
  • línea 22: los tres parámetros necesarios para el método calculer se solicitan en una sola línea introducida con el teclado.
  • línea 26: el método [chaine].Split(null) permite descomponer [chaine] en palabras. Estas se almacenan en un array args.
  • línea 66: llamada al método calculer del objeto impot, que implementa la interfaz IImpot.

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

Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument marié incorrect : tapez o ou n
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument salaire incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d f
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros