Biblioteca Online : ¡La Suscripción ENI por 9,90 € el primer mes!, con el código PRIMER9. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí

Creación de clases

Introducción

Recordemos que una clase es un modelo que el sistema utiliza para instanciar en memoria el objeto correspondiente. Son los modelos que el desarrollador declara lo que comúnmente denomina archivos de código fuente (archivos de extensión .cs) de su proyecto. Es lo que hemos hecho con la clase Program.

Los espacios de nombres

El framework .NET ha organizado sus clases agrupándolas por su finalidad, en espacios de nombres (namespace), apareciendo estos en los ensamblados que se instalan en el GAC.

Como ellos, las clases que va a desarrollar deberán pertenecer a un espacio de nombre. Si omite su declaración, el sistema la creará por defecto. Es muy aconsejable hacerlo usted mismo, porque esto le va a permitir estructurar sus proyectos y controlar el "alcance" de los nombres, clases y métodos.

Elija un nombre de espacio de nombres que resuma el papel de la familia de clases que agrupa. Generalmente, el análisis UML le ayudará a su nomenclatura.

Este nombre debe empezar por una letra o por un guión bajo (_). A continuación puede contener letras, cifras y los guiones bajos. Evite utilizar caracteres acentuados y, si su definición contiene varias palabras, opte por la notación "tipo camello" (CamelCase). Por ejemplo, si escribe un espacio de nombres para una serie de herramientas que gestionan las facturas de compra, el nombre en formato "CamelCase" podría ser GestionFacturasCompras. Las primeras letras de las palabras relacionadas están en mayúscula.

Por defecto, Visual Studio utiliza el nombre del proyecto como nombre por defecto del espacio de nombres y también como nombre del ensamblado. Por supuesto, se puede modificar esta configuración....

Declaración de una clase

Una vez que se ha declarado el espacio de nombres, se puede definir la clase. Aunque esto generalmente es desaconsejable, es posible declarar varias clases del mismo nivel en un mismo archivo de código fuente. Una clase se declara con la palabra clave class seguida del nombre que ha elegido. Como para el namespace, este nombre debe empezar por una letra o por un guión bajo (_). A continuación puede contener letras, cifras y guiones bajos. Evite utilizar caracteres acentuados y opte por el formato "de tipo camello" (CamelCase). Por ejemplo, si escribe una clase que emula un lector digital, el nombre en formato "CamelCase" sería LectorDigital. Las primeras letras de las palabras relacionadas se escriben en mayúscula.

Aunque se explica en detalle más adelante, a continuación se realiza la declaración de una herencia. De hecho, si la clase extiende una clase de base y/o implementa una o varias interfaces, el nombre de la clase está seguido del signo ’:’ y a continuación la enumeración de sus componentes padres.

Los miembros de la clase (atributos, propiedades y métodos) se definen a continuación entre llaves.

Sintaxis de declaración:


visibilidad class NombreClase  [:[ClaseMadre], [Lista 
interfaces de base]]  
{  
   // cuerpo de la clase  
}
 

Ejemplo


namespace MiPrimerNameSpace  
{  
   class MiPrimeraClase: SuClaseMadre, Interfaz1, Interfaz2  
   {  
      // Cuerpo de la clase  
   }  
}
 

El atributo de visibilidad de una clase definido en un namespace puede ser de tipo:

  • public: la clase se podrá utilizar por todos.

  • internal: la clase solo se podrá utilizar por los componentes del assembly que la contengan.

Los demás atributos (private, protected y protected internal) solo tienen sentido para las clases anidadas. Una clase anidada es una clase definida en otra. Volveremos más tarde sobre el interés de este tipo de declaración.

La clase anidada precedida por el atributo private solo se podrá utilizar en su clase anfitriona.

La clase anidada precedida por el atributo protected solo se podrá utilizar por las clases heredadas de su clase anfitriona.

La clase anidada precedida por el atributo...

Las interfaces

1. Introducción

Explicar las interfaces y su interés siempre es mejor con un ejemplo concreto que lo apoye. Por tanto, imagine un programa que permita controlar un sistema domótico desde un teléfono móvil (con nuestros smartphones siempre conectados, el interés por este tipo de aplicaciones va a sufrir una verdadera explosión). Este programa gráfico permitirá controlar las persianas eléctricas, leer la temperatura, encender el horno, en resumen leer y escribir los estados lógicos (verdadero o falso) y leer y escribir valores analógicos (00 a ff, por ejemplo).

En este tipo de aplicaciones hay que controlar que no se depende de un hardware concreto. Un cambio en la tarjeta de entrada/salida, es decir, la tarjeta que va a leer los sensores y controlar los relés bajo demanda debe afectar lo menos posible al código existente.

En esto nos van a ayudar las interfaces de programación.

2. El contrato

Para tener éxito en nuestra independencia respecto al hardware, hay que limitar sus relaciones a su forma más sencilla y "firmar un contrato".

Por analogía, se puede decir que gracias al "formato estándar del Conector Jack 3.5mm estéreo, cualquier par de auriculares se puede conectar a cualquier reproductor digital. Esta famosa toma juega el papel de interfaz entre dos hardwares que no son necesariamente del mismo fabricante.

Limitar las relaciones a su expresión más sencilla implica enumerar las funcionalidades mínimas esperadas por la tarjeta de entrada/salida. Esta lista es un tipo de contrato que el hardware deberá respetar obligatoriamente. Para volver a la analogía anterior, los fabricantes de auriculares ofrecen productos con conectores que tienen diámetros estandarizados y la conexión es posible gracias a esta "interfaz".

Entonces, para este proyecto, ¿cuáles son nuestras necesidades?

Hay que poder:

  • Leer los estados binarios de las entradas referenciadas - interruptores, pulsadores y sensores de presencia.

  • Leer de los valores analógicos de las entradas referenciadas - sensores de temperatura, sensores de luminosidad y sensores de sonido.

  • Solicitar las salidas binarias referenciadas - persianas, horno, portal, etc.

  • Solicitar las salidas analógicas referenciadas - adaptadores de luminosidad, etc.

A continuación...

Asociación, composición y agregación

En todo programa, el desarrollador tiene que diseñar clases que utilizan o contienen otras clases que, a su vez, pueden utilizar o contener otras clases. Por ejemplo, un formulario (cuadro de diálogo con el usuario) muestra diferentes controles, como botones de selección, casillas de selección, campos de introducción de texto o listas desplegables. El formulario y cada uno de sus controles se "encapsulan" en clases que el desarrollador va a asociar para conseguir el formulario final.

Las asociaciones son más o menos fuertes. En nuestro ejemplo, la asociación es fuerte porque es el formulario el que instancia estos controles. Estos mismos controles se destruirán cuando se cierre. Hablamos de agregación "compuesta".

Mientras dura esta asociación, el objeto continente accede libremente a los miembros de tipo public de cada uno de los objetos contenidos. De esta manera, durante su carga, nuestro formulario podrá inicializar los contenidos por defecto de las cajas de texto, las selecciones de los botones de selección y, durante la validación, recuperar las selecciones del usuario, preguntando a cada uno de los controles.

¿Cómo C# permite gestionar estas diferentes formas de colaboración?

Independientemente de cuál sea el grado de asociación, la clase host necesitará almacenar referencias a las clases contenidas.

Puede tener varios objetos del mismo tipo referenciado en la clase host. Esta pluralidad se expresa en UML con un índice en el extremo de la relación indicando una cantidad fija o un rango posible.

images/RI05_19.png

En este ejemplo, un autor ha escrito uno o un número indefinido de libros y un libro pertenece a un solo autor.

C# va a tener varias formas de codificación para estos diferentes tipos de asociación.

  • La clase "continente" tiene una sencilla referencia a un objeto de tipo "contenido".


class Usuaria  
{  
   Contenido contenido  = null;  
}
 

Traducción UML de esta asociación:

images/RI05_20.png
  • La clase "continente" tiene una lista de referencias de tamaño fijo a los tipos de "contenido". En este caso, será preferible un objeto de tipo tabla, como se verá más adelante.


class Continente   
{  ...

Las clases anidadas

Es posible declarar una clase en una clase. Esta funcionalidad ofrece al desarrollador otra manera de organizar su código. Las clases principales se registran en los espacio de nombres; así es posible realizar una nueva clasificación dentro de estas clases principales.

La mayor parte del tiempo, la clase anidada, llamada nested class o inner class, no significa nada fuera de su clase host y su operador de visibilidad es de tipo private. A pesar de todo, es posible modificar este tipo de acceso a public o protected.

Sintaxis de una nested class:


class Host  
{  
   class Anidada  
   {  
   }  
}
 

La clase anidada tiene acceso a todos los miembros de la clase host. La clase host solo tiene acceso a los miembros de tipo public de la clase anidada.

El hecho de declarar una clase en otra es una forma de notación. No tiene ninguna consecuencia inducida en las instanciaciones de las clases host y anidada, que siguen siendo independientes.

Ejemplo de codificación de una clase anidada y su clase host:


using System;  
  
namespace Cap7  
{  
  class Program  
  {  
    static void Main(string[] args)  
    {  
      new Prueba();  
    }  
  
    class ClaseHost  ...

Las estructuras

Las estructuras son una herencia de los lenguajes C y C++. En sus formas C# se parecen mucho a las clases, pero con una diferencia de tamaño: las estructuras se asignan a la pila, a diferencia de las clases, que se asignan a la cola. Esto tiene dos consecuencias:

  • El ciclo de vida de la estructura se limita a las llaves que la encierran.

  • La estructura no necesita gestión de la memoria (asignación, garbage collector),  de modo que tiene un rendimiento muy alto para entidades muy pequeñas.

Las estructuras no existen en Java.

1. Declaración de una estructura

Sintaxis de declaración de una estructura:


[visibilidad] struct nombre [:interfaces] {//implementación } [;]
 

El atributo de visibilidad de una estructura definida en un espacio de nombres, puede ser de tipo:

  • public: la estructura es utilizable por todos.

  • internal: la estructura es utilizable por los componentes del ensamblado que lo alberga. 

Los atributos private, protected y protected internal solo tienen sentido para las estructuras anidadas. Una estructura anidada es una estructura definida en una clase.

Precedida del atributo private, la estructura anidada solo se podrá utilizar en su clase host.

Precedida del atributo protected, la estructura anidada solo se podrá utilizar por las clases heredadas de su clase host.

Precedida del atributo protected internal, la estructura anidada solo se podrá utilizar por las clases...

Las clases parciales

La mayor parte de la veces, una clase se define en un único archivo fuente (de extensión .cs). Si varios desarrolladores deben enriquecer la misma clase al mismo tiempo, la implementación en un archivo único plantea problemas. De la misma manera, Visual Studio ofrece un asistente gráfico que permite diseñar cuadros de diálogo y generar código C# que los encapsula en una clase. Esta clase se "comparte" con el desarrollador. Por ejemplo, Visual Studio escribe todo el código que permite instanciar y situar un botón en el cuadro de diálogo; también prepara el marco del método que se invoca cuando el usuario hace clic en este botón. Le corresponde al desarrollador "rellenar" el cuerpo del método. Compartir el mismo archivo fuente entre el diseñador gráfico y el desarrollador puede ser complicado y C# ofrece una solución elegante que autoriza el fraccionamiento de clases (y también de estructuras y de interfaces) en varios archivos. Durante la compilación, el código se fusiona y la clase se reconstituye.

Se utiliza la palabra clave partial para indicar al compilador que la clase se puede completar por otros archivos fuente.

Ejemplo de contenido del primer archivo fuente MiClaseCompartida.cs


namespace Cap5  
{  
  partial class MiClaseCompartida  ...

Los métodos parciales

Cuando la definición de una clase se fracciona en varios archivos fuente, es posible definir la firma de métodos en un lado para eventualmente implementarlos en otro. Es esta noción de eventualidad la que tiene todo el interés de esta funcionalidad. Si el compilador encuentra una implementación del método en uno los archivos fuente, la integra. En caso contrario, retira la definición del método.

Varias reglas para este modo de codificación:

  • Los métodos de tipo partial siempre deben tener void como tipo de retorno.

  • No definir ni atributo de visibilidad ni modificador a los métodos de tipo partial. Siempre son implícitamente de tipo private.

Ejemplo de contenido del primer archivo fuente MiClaseCompartida.cs:


namespace Cap5  
{  
  partial class MiClaseCompartida  
  {  
    partial void MiMetodoParcial(string param1);  
  }  
}
 

Ejemplo de contenido del segundo archivo fuente MiClaseCompartida2.cs


namespace Cap5  
{  
  partial class MiClaseCompartida: Prueba  
  {  
    partial void MiMetodoParcial(string param1)  
    {  
      System.Console.WriteLine(param1);  
    }  
  }  
}
 

Los indexadores

C# permite construir tipos que ofrecen acceso a sus colecciones internas utilizando el operador de índice ([ ]) como si se tratara de tablas nativas.

Imaginemos por ejemplo una clase Empleados que gestiona una lista de empleados. La clase define un determinado número de métodos para calcular el tiempo de permanencia, los salarios, etc., y contiene una colección de objetos que describen cada empleado. Esta colección no será de tipo public en virtud de la regla de encapsulación, pero el usuario de la clase deberá poder acceder a cualquier objeto Empleado de la manera más sencilla posible...

Proponer una sintaxis basada en el operador [ ] para acceder a una ubicación particular de la colección es una solución muy acertada.

Ejemplo:


Empleado e = MisEmpleados[3];
 

La implementación de la sobrecarga del operador [ ] se realiza como sigue:


visibilidad tipoRetorno this[TipoIndice valorIndice]  
{  
   // descriptores de acceso get y set  
}
 
  • La mayor parte de las veces, el atributo de visibilidad será de tipo public.

  • El tipoRetorno corresponde al tipo contenido en la colección.

  • De nuevo se utiliza this para simbolizar la instancia.

  • [TipoIndice valorIndice] define el tipo y el valor a pasar para identificar el índice al que acceder en la colección. En el caso de una colección de tipo List<T>, se utilizará un int. En el caso de una colección "diccionario", el tipo podrá ser cualquier otro.

  • La sintaxis de los descriptores de acceso set y get es la misma...

Sobrecarga de operadores

Acabamos de ver que el indexador con el operador [ ] era un medio práctico que se podía ofrecer a los usuarios de las clases para acceder a las entradas de sus colecciones.

Es posible sobrecargar otros operadores si esto tiene sentido para sus objetos.

Tomemos ejemplo del operador +. Se usa entre dos enteros y permite realizar su suma. El operador + se "sobrecargó" en la clase System.String para realizar la concatenación de dos cadenas.

Otro ejemplo: el operador ==; por defecto, si los tipos a comparar forman parte de la familia Valor, serán valores que se podrán comparar. En caso contrario lo serán las referencias. En el caso de System.String, que forma parte de la familia referencia, será el contenido de las cadenas lo que se comparará, porque el operador == se sobrecargó para esto.

Sobrecargar los operadores siempre genera controversia entre los desarrolladores. De hecho hay que "navegar" en la documentación de la clase utilizada para saber cómo se comportan los operadores. Además, Java ha rechazado completamente esta funcionalidad.

Sobrecargar el operador ==

Como ya hemos visto, System.Object implementa un método virtual Equals. Este método se puede sobrecargar en nuestras clases para que la comparación, se haga, por defecto, sobre las referencias de las clases, y después se realiza sobre los valores "pertinentes"...