¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
  1. Libros
  2. C# 8 y Visual Studio 2019
  3. Programación orientada a objetos con C#
Extrait - C# 8 y Visual Studio 2019 Los fundamentos del lenguaje
Extractos del libro
C# 8 y Visual Studio 2019 Los fundamentos del lenguaje
1 opinión
Volver a la página de compra del libro

Programación orientada a objetos con C#

Principios de la programación orientada a objetos

La noción de objetos es omnipresente cuando se desarrolla con C#. En primer lugar veremos lo que representa esta noción y a continuación estudiaremos cómo ponerla en práctica.

La programación procedural, tal y como se utiliza en lenguajes como C o Pascal, define los programas como flujos de datos que sufren transformaciones, en el hilo de la ejecución, por procedimientos y funciones. No existe ningún vínculo robusto entre los datos y las acciones.

La programación orientada a objetos (POO) introduce la noción de conjunto coherente de datos y de acciones, trasponiendo al mundo del desarrollo conceptos comunes e intuitivos propios del mundo que nos rodea.

En efecto, utilizamos, de manera cotidiana objetos con todas las propiedades y acciones que tienen asociadas. Pueden también interactuar entre sí o estar compuestos unos por otros, lo que permite formar sistemas complejos.

Existe una infinidad de analogías para materializar este concepto, pero escogeremos como ejemplo para esta introducción un automóvil, que es lo suficientemente sencillo y lo suficientemente complejo para ilustrar perfectamente las nociones asociadas a la POO.

Un coche tiene propiedades que le son propias, como por ejemplo su marca o color, así como acciones asociadas, como el hecho de arrancar, frenar o encender las luces y puede producir...

Clases y estructuras

Para abordar el uso de objetos, corazón de la POO, veremos cómo crearlos, declarando la clase o la estructura que servirá como modelo hasta su instanciación. Veremos, también, cómo agregar propiedades y funcionalidades a nuestros objetos.

1. Clases

La mayoría de tipos definidos en el framework .NET son clases, de modo que resulta muy importante comprender cómo manipularlas. En efecto, cualquier aplicación escrita en C# contendrá, como mínimo, una clase escrita por el desarrollador y utilizará probablemente varias decenas o centenares propias del framework .NET.

a. Declaración

Una clase se define y se utiliza mediante su nombre. Es preciso respetar ciertas reglas en su nomenclatura, de modo que puede resultar imposible de compilar si no se respetan. Las posibilidades de uso de la clase son relativas a las definidas por el modificador de acceso asociado.

Nombre de una clase

El nombre de una clase es válido solamente si se respetan las siguientes reglas:

  • Contiene únicamente caracteres alfanuméricos o el carácter _.

  • No empieza por una cifra.

Por convención, es habitual nombrar las clases respetando el estilo "UpperCamelCase" (también llamado PascalCase), es decir, la primer letra de cada palabra que compone el nombre de la clase se escribe en mayúsculas, mientras que el resto se escribe en minúsculas. Todas las clases del framework .NET respetan esta convención.

MiClase, MuroDeLadrillos o PortaEquipajes son nombres que respetan la regla "UpperCamelCase".

pantallaPlana es un nombre válido pero que no respeta esta convención.

Sintaxis

Una clase se declara utilizando la palabra clave class seguida de un nombre y de un bloque de código delimitado por los caracteres { y }.

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

<modificador de acceso> [partial] class <nombre de la clase> : 
[<Clase base>, <Interfaz1>, <Interfaz2>, ...] 
{  
} 

Las nociones de clase base y de interfaz se estudiarán en este capítulo dentro de las secciones La herencia y Las interfaces.

Las clases son de tipo referencia, lo que significa que las variables cuyo tipo es una clase tienen como valor por defecto null. Este comportamiento es importante y es conveniente conocerlo...

Los espacios de nombres

El código de una aplicación puede contener fácilmente varias decenas, centenares o millares de tipos diferentes. Para organizar el proyecto es posible crear una estructura jerárquica de carpetas que agrupan los tipos por utilidad o en base a su contexto de uso. Además de esta organización física, es posible crear una organización lógica mediante el concepto de espacio de nombres (namespace, en inglés). 

1. Nomenclatura

Un espacio de nombres está compuesto por varios identificadores separados por el operador ., cada uno de estos identificadores forma parte de un contenedor lógico. Veamos algunos ejemplos de espacios de nombres:

System  
System.Windows  
System.Data.SqlClient 

Estos tres espacios de nombres forman parte del framework .NET. System contiene los tipos básicos de.NET, como los tipos primitivos, System.Windows es el contenedor lógico de tipos básicos que permiten crear aplicaciones de ventanas. Por último, System.Data.SqlClient contiene los tipos del bloque ADO.NET específicos de la base de datos SQL Server.

Además de ayudar a la estructuración, el uso de espacios de nombres permite tener varios tipos cuyo nombre es idéntico: para evitar cualquier ambigüedad entre estos tipos es posible utilizar el nombre plenamente cualificado del tipo que se desea utilizar. Este nombre...

La herencia

La herencia permite representar e implementar una relación de especialización entre una clase de base y una clase derivada. Permite, por tanto, transmitir las características y el comportamiento del tipo de base hacia el tipo derivado, así como su modificación.

1. Implementación

Para declarar una relación de herencia entre dos clases es necesario modificar la declaración de la clase que se quiere definir como clase derivada. Esta modificación consiste en agregar el símbolo ":" seguido del nombre de la clase de base tras la declaración del tipo:

public class ClaseDeBase  
{  
   public int Identificador;   
  
   public void MostrarIdentificador()  
      => Console.WriteLine(Identificador) 
}   
  
public class ClaseDerivada : ClaseDeBase 
{  
} 

A diferencia de otros lenguajes, como C++, C# no autoriza a derivar un tipo a partir de varias clases de base.

Una vez implementada esta relación de herencia es perfectamente posible escribir el siguiente código:

ClaseDerivada miObjeto = new ClaseDerivada();  
miObjeto.Identificador = 42;  
miObjeto.MostrarIdentificador();  
//Muestra 42 en la consola 

En efecto, ClaseDerivada puede acceder a los miembros public, internal y protected de su clase de base, incluidas la variable Identificador y el método MostrarIdentificador.

2. Las palabras clave this y base

La palabra clave this devuelve la instancia en curso de la clase en la que se utiliza. Permite, por ejemplo, pasar una referencia del objeto en curso a otro objeto.

namespace ThisYBase  
{   
   public class Coche  
   {  
         public decimal Longitud;   
  
         //Constructor de la clase Coche  
         public Coche()  
         {  
              //Las 2 líneas siguientes tienen, exactamente, 
              //el mismo rol pues devuelven el objeto en curso 
  
             ...

Las interfaces

Para manipular distintos tipos de objetos que poseen funcionalidades similares es posible utilizar un contrato que define los datos y los comportamientos comunes a estos tipos. En C# se denomina, a este contrato, una interfaz.

Una interfaz es un tipo que posee únicamente miembros públicos. En general, estos miembros no poseen implementación: se respetan los tipos a través del contrato y cada miembro aportará su propia implementación. Veremos pronto que esta regla no es absoluta.

Las interfaces representan una capa de abstracción, particularmente útil para hacer que una aplicación sea modular, pues permiten utilizar cualquier implementación para un mismo contrato.

1. Creación

Las interfaces se declaran de forma similar a las clases, con las siguientes diferencias: 

  • Es preciso utilizar la palabra clave interface en lugar de class.

  • Solamente los miembros que sean visibles públicamente deben definirse en la interfaz y no existe ningún otro modificador de acceso autorizado sobre estos miembros.

  • Generalmente, los miembros de la interfaz no tienen implementación.

La sintaxis general para crear una interfaz es la siguiente:

<modificador de acceso> interface <nombre> [: interfaces de base ] 
{  
  
} 

Por convención, el nombre de las interfaces en C# empieza por una letra I (i mayúscula).

Consideremos el caso de la lectura de archivos de audio. Es posible leer archivos en los formatos MP3, WAV, OGG, MWA, o incluso otros. La lectura de cada uno de estos formatos requiere un decodificador particular, por lo que parece conveniente crear una clase para cada uno de estos tipos de archivo soportados en una aplicación: LectorAudioMp3, LectorAudioOgg, etc.

Es probable que estas clases sean muy similares y que deban utilizarse en las mismas circunstancias, lo que hace de este conjunto un candidato perfecto para la creación de una interfaz común a los distintos tipos.

Una posible interfaz para estas distintas clases podría ser la siguiente:...

Las enumeraciones

Una enumeración es un tipo de datos que representa a un conjunto finito de valores constantes que pueden utilizarse en las mismas circunstancias.

La declaración de una enumeración se realiza utilizando la siguiente sintaxis:

<modificador de acceso> enum <nombre>  
{  
   <nombre de constante 1> [ = <valor numérico>],  
   <nombre de constante 2> [ = <valor numérico>],  
   ...  
} 

El framework .NET define un número importante de tipos enumerados. Entre ellos encontramos el tipo System.Windows.Visibility, que define el estado visual de un control de WPF. Su definición es la siguiente:

public enum Visibility  
{  
   Visible = 0,  
   Hidden = 1,  
   Collapsed = 2  
} 

Se utilizará un valor de la enumeración escribiendo el nombre de la enumeración seguido del operador punto (.) y del nombre de una constante de enumeración. 

Para ocultar un control System.Windows.Grid podríamos escribir, por ejemplo, el siguiente código:

//grid es un objeto de tipo System.Windows.Grid  
grid.Visibility = System.Windows.Visibility.Collapsed; 

Los delegados

Un delegado es un tipo que representa una referencia a un método. Gracias a los delegados es posible especificar que un parámetro de un método debe ser una función que posea una lista de parámetros y un tipo de retorno definido. A continuación, es posible invocar a esta función en el código de nuestro método sin conocerla previamente.

1. Creación

La declaración de un delegado utiliza la palabra clave delegate seguida de un procedimiento o función. El nombre especificado en esta firma es el nombre del tipo delegado creado.

//Creación de un delegado para una función que recibe 
//2 parámetros de tipo double y devuelve un double 
public delegate double OperacionMatematica(double operando1, 
double operando2); 

El código anterior crea un nuevo tipo que puede utilizarse en la aplicación: OperacionMatematica.

2. Uso

El tipo OperacionMatematica puede utilizarlo cualquier variable o cualquier parámetro de un método. Es posible utilizar una variable de este tipo como un método, es decir, es posible pasarle parámetros y recuperar su valor de retorno.

private static double   
EjecutarOperacionMatematica(OperacionMatematica  
operacionARealizar, double operando1, double operando2)  
{  
    return operacionARealizar(operando1, operando2);  
} 

Es preciso...

Los eventos

Los eventos están en el núcleo del desarrollo de aplicaciones con C#. Permiten basar la lógica de la aplicación sobre una serie de procedimientos y de funciones que se ejecutan cuando alguno de sus componentes solicita la ejecución. Es el caso, por ejemplo, de los componentes gráficos: estos pueden desencadenar eventos cuando el usuario realiza alguna acción como, por ejemplo, la selección de un elemento en una lista desplegable o hacer clic sobre un botón.

1. Declaración y producción

Los eventos de C# están basados en el uso de delegados. La idea general es que cada evento puede recibir uno o varios controladores de eventos con una firma definida por un tipo de delegado.

Los eventos generados por las clases del framework utilizan con frecuencia el tipo de delegado EventHandler para definir los controladores de eventos que pueden tener asociados. Este delegado se define de la siguiente manera:

public void delegate EventHandler(object sender, EventArgs e); 

El parámetro sender se corresponde con el objeto que ha generado el evento, mientras que el parámetro de tipo EventArgs, llamado e, se utiliza para proveer información a los métodos que tratan el evento. Si no es necesario pasar ningún valor al controlador de eventos es posible utilizar una instancia de la clase EventArgs, pero en caso contrario es conveniente pasar algún objeto de un tipo que herede de la clase EventArgs.

Existe...

Los genéricos

Los genéricos son elementos de un programa capaces de adaptarse de cara a proveer las mismas funcionalidades para distintos tipos de datos.

Se introducen con la aparición del framework .NET 2.0 con el objetivo de proporcionar servicios adaptados a varios tipos de datos, manteniendo un tipado fuerte.

Los frameworks .NET 1.0 y .NET 1.1 proporcionaban la clase ArrayList para la gestión de listas dinámicas. Este tipo, muy práctico, tenía el inconveniente de que contenía únicamente objetos de tipo System.Object, lo que generaba un gran número de conversiones de tipo que hubiera sido preferible evitar. Los genéricos son la respuesta aportada por Microsoft a este problema: la clase List<T> remplaza ventajosamente (en la mayoría de casos) a la clase ArrayList y permite definir el tipo de los objetos que contiene.

Esto simplifica el código y lo vuelve más seguro, pues el desarrollador elimina gran parte de los problemas derivados de las potenciales conversiones de tipo.

Los elementos genéricos se reconocen mediante los caracteres < y > presentes en sus nombres. Estos símbolos contienen los nombres de los tipos asociados a la instancia de un elemento genérico.

Los genéricos se estudian aquí a través del desarrollo de un tipo que permite gestionar una lista de elementos. Las listas son colecciones que siguen el principio FIFO (First In, First Out): es posible acceder, únicamente, al primer elemento de la colección y cuando se agrega un elemento nuevo, éste se sitúa automáticamente en la última posición de la colección. En el framework .NET existe un tipo similar: System.Collections.Generic.Queue<T>.

1. Clases

Las clases genéricas permiten manipular de una manera unificada varios tipos de datos. Estos tipos están designados por uno o varios alias en el nombre de la clase y tras la instanciación de un objeto genérico se asignan los tipos concretos a estos alias.

a. Definición de una clase genérica

Para crear una clase genérica es preciso definir una clase, agregarle los caracteres < y > y definir, entre ellos, uno o varios alias de tipo separados por comas. Estos alias permiten manipular variables fuertemente tipadas sin conocer previamente el tipo concreto.

public...

Las colecciones

Es habitual que una aplicación tenga que manipular grandes cantidades de datos. Para ello, el framework .NET provee varias estructuras de datos, agrupados bajo el nombre de colecciones. Estas están adaptadas a distintos tipos de situación: un almacenamiento desordenado de datos dispares, el almacenamiento de datos en base a su tipo, el almacenamiento de datos por nombre, etc.

1. Tipos existentes

Las distintas clases que permiten gestionar colecciones se agrupan en dos espacios de nombres:

  • System.Collections

  • System.Collections.Generic

El primero representa a los tipos "clásicos", mientras que el segundo incluye las clases genéricas equivalentes que permiten trabajar con objetos fuertemente tipados.

a. Array

La clase Array no se encuentra en el espacio de nombres System.Collections, pero puede considerarse como una colección. En efecto, implementa varias interfaces propias de las colecciones: IList, ICollection e IEnumerable. Esta clase es la clase de base para todos los arrays utilizados en C#.

No obstante, a menudo se utiliza esta clase directamente: se la prefiere en la mayoría de casos en la sintaxis C#.

La clase Array, abstracta, no permite instanciarla mediante el operador new. Por este motivo, se utilizará alguna de las sobrecargas del método estático Array.CreateInstance.

Array array = Array.CreateInstance(typeof(int), 5); 

Esta declaración de variable es equivalente a la siguiente:

int[] array = new int[5]; 

b. ArrayList y List<T>

Las clases ArrayList y su contraparte genérica List<T> son evoluciones de la clase Array. Aportan ciertas mejoras respecto a los arrays:

  • El tamaño de un objeto ArrayList o List<T> es dinámico y se ajusta en función de las necesidades.

  • Estas clases aportan métodos para agregar, incluir o eliminar varios elementos.

En cambio, las listas tienen una única dimensión, lo que puede complicar ciertos procesamientos.

Las colecciones de tipo ArrayList también pueden presentar problemas de rendimiento, pues manipulan elementos de tipo Object, lo que implica muchas conversiones de tipo. Es preferible utilizar arrays fuertemente tipados o listas genéricas (List<T>) que aseguran, también, un tipado fuerte.

La clase ArrayList puede instanciarse mediante uno de sus tres constructores. El primero no recibe ningún parámetro:...

Programación dinámica

Desde sus inicios, C# es un lenguaje con un tipado fuerte y estático, lo que significa que el compilador es capaz de conocer el tipo de cada variable que se utiliza en una aplicación. Esto le permite saber cómo tratar las llamadas a los métodos y las propiedades de cada objeto y permite generar errores de compilación cuando se detecta que alguna operación es inválida.

Con la aparición de C# 4 y del entorno .NET en versión 4.0 se introduce un nuevo tipo de dato singular: dynamic. Este tipo de dato está destinado a resolver problemáticas muy particulares, pues permite utilizar variables cuyo tipo no se conoce en tiempo de ejecución.

El compilador no realiza ninguna verificación para aquellas operaciones realizadas sobre variables dynamic. Se realizan únicamente en tiempo de ejecución de la aplicación: cualquier operación que se detecte como inválida en este punto genera un error de ejecución.

La siguiente función recibe dos parámetros de tipo dynamic y les aplica el operador +.

private static dynamic Sumar(dynamic operando1, dynamic 
operando2) 
{ 
   var resultado = operando1 + operando2; 
  
   return resultado;  
} 

El tipo de retorno de la función es dynamic, pues no es posible determinarlo en tiempo de compilación....

Programación asíncrona

Cada vez es más frecuente tener que desarrollar aplicaciones reactivas y capaces de realizar varias tareas simultáneamente. El framework .NET aporta soluciones a este problema desde sus inicios mediante las clases Thread o BackgroundWorker, entre otras. Con el framework .NET 4.0 aparecen la clase Task y su equivalente genérico Task<TResult>. Estos tipos simplifican el trabajo del desarrollador permitiéndole gestionar de manera sencilla el procesamiento y la espera de la ejecución de bloques de código, proporcionando también un medio para ejecutar varios procesamientos asíncronos a la vez.

Con la llegada de C# 5 la tarea del desarrollador se simplifica todavía más gracias a la integración del asincronismo directamente en el lenguaje. En efecto, las palabras clave async y await permiten escribir código asíncrono de manera secuencial, como código... ¡síncrono!

1. Los objetos Task

Las clases Task y Task<TResult> permiten ejecutar código asíncrono encapsulando el uso de threads.

Funcionamiento de los threads

Los threads son unidades de ejecución que pueden trabajar, en función de la arquitectura de la máquina, en paralelo (cada thread se ejecuta durante un pequeño espacio de tiempo y a continuación cede su lugar a otro thread, que cederá de nuevo su espacio a otro thread, etc.). Será el sistema operativo el que decidirá el tiempo de procesador dedicado a cada thread. Por este motivo, es imposible prever exactamente en qué momento se ejecutará una instrucción...