Professional Documents
Culture Documents
Vamos a implementar (en C#) una clase que permita realizar y controlar todas las
operaciones que habitualmente se realizan sobre números en coma flotante, y ésto, sin que
debamos realizar ningún tipo de esfuerzo mientras implementamos los algoritmos sobre los
que usamos este tipo de números.
Una de las cosas que se suelen hacer cuando se programa en C++ es abstraer el tipo de
representación de coma flotante que se va a usar para los cálculos numéricos. Cada
representación (principalmente double o float aunque pueden ser otros) tiene sus ventajas y
sus inconvenientes, esto habitualmente se hace de tres formas:
Preprocesador:
#define REAL double
Definición de tipo:
typedef Real double;
Clase:
class Real {
double y;
}
En C++, cualquiera de las dos últimas soluciones permitirían definir mediante sobrecarga
de operadores lo que pretendemos (pues en la sobrecarga de operadores es independiente de
que se usen clases o no), no obstante la más adecuada como veremos es la última.
Vale, ¿y en C#?
En C# sólo podemos utilizar el último método, pues ninguno de los dos anteriores están
disponibles, sin embargo, para nuestros propósitos no pasa nada, pues igualmente
pensábamos definir una clase que encapsule toda nuestra sobrecarga.
Vamos a crear un nuevo tipo que encapsule todas las operaciones usuales que se realizan
sobre los reales permitíendonos modificar fácilmente el tipo de real a usar en diferentes
implementaciones y registrar estadísticas de las operaciones que se realizan sin que
debamos modificar ni realizar tareas adicionales en las aplicaciones en las que usemos este
tipo.
Acerca de la implementación
Si estás interesado en conocer los detalles de las herramientas de programación que se usan
en esta implementación te sugiero amplíes en otras fuentes (por ejemplo en MSDN) pues
sólo las expondré superficialmente.
Creación de la clase
Puesto que pretendemos realizar una representación de los números reales, llamaremos a la
clase Real y como no podemos heredar (es realmente una pena) del tipo básico double (ni
float) debemos establecer un miembro (privado, por supuesto) que almacene el valor en
cuestión. Este es el aspecto que tomará nuestra clase.
Registro de estadísticas
Las estadísticas deberán basarse lógicamente en las operaciones que se han realizado y
como lo que más nos va a importar es el número de operaciones realizadas de cada tipo
(podría ser interesante también la distribución de éstas en el tiempo, el código, según los
datos, etc... pero esto es algo mucho más ambicioso) tendremos que mantener un contador
para cada una de ellas. Además, estos contadores deben ser globales a toda la aplicación
por tanto dichos miembros deberán ser estáticos.
Como querremos que se pueda acceder a estos datos desde fuera de la clase, sin
modificarlos, estableceremos dichos valores como propiedades de sólo lectura.
Algo bastante habitual será que nos interese un contador general que aglutine una familia
de operaciones, obviamente ese contador no existe (se calcula como la suma de otros), pero
será interesante tener una propiedad (de sólo lectura) que calcule dicho valor y no sea
preciso hacerlo cada vez.
Ya puestos a publicar, nada cuesta crear un método que devuelva formateados los
resúmenes de los contadores, así poco o nada tendremos que hacer para tener una
información aproximada.
Ni que decir tiene que tendremos que poder resetear todos los contadores (por ejemplo
cuando pasamos de testear una implementación a otra).
Constructores de la clase
Como veremos, la forma más habitual en la que se construirán los objetos de tipo Real en
nuestros programas será mediante la conversión implícita, mucho más cómoda; sin
embargo, no cuesta nada y parece más correcto crear algunos. Puesto que los vamos a
definir, aquí incrementaremos los contadores de conversión.
Como vemos, utilizamos la definición del compilador DEBUG que nos indica cuando se
está compilando para depuración, no obstante si quisiéramos contabilizar también las
operaciones en una compilación release no cuesta nada cambiar el nombre por cualquier
otro, lo que pasa es que será habitual querer sólo las estadísticas en un entorno de desarrollo
en el que se prueban los algoritmos.
Conversiones de tipo
Algunas conversiones de tipo son implementadas mediante métodos en la clase, como los
constructores de antes, no son necesarios, pero dejan más claro y aglutinan otras
conversiones menos claras. Tamibién éstas serán las encargadas de incrementar los
contadores de conversión.
Unas conversiones mucho más interesantes son las siguientes:
Que las tres primeras conversiones sean implícitas tiene sentido mientras la dirección en la
que va la conversión sea sin pérdida (y/o sin un coste computacional elevado), en otro caso,
como en la conversión desde una cadena (en la que se puede producir un problema
importante de rendimiento y claridad) las conversiones es mejor que sean explícitas, es
decir, el programador si desea realizar ese tipo de conversiones debe indicar
explícitamente que desea realizar ese tipo de conversión.
Similar ocurre con las conversiones en la otra dirección, en éste caso, sólo permitimos la
conversión implícita del tipo en el que se almacena la información, pues en todos los casos
existe pérdida de información o de rendimiento.
La clase también es un buen sitio para publicar las constantes más importantes relacionadas
con los Reales (de hecho así lo hace la clase estática del Framework .NET).
Propiedades
Para cada instancia, publicamos algunas propiedades interesantes que de las que también
deberemos realizar alguna contabilidad como el valor absoluto, si es "casi" cero, si es
positivo, etc.
Funciones
Algo poco habitual pero que me parece interesante es que, ya que definimos la clase para
cada instancia, las funciones más comunes que la transforma estén en la misma clase (lo
contrario que en el Framework .NET). Así definimos dentro de la misma clase funciones
como coseno, seno, tangente, etc.
Operadores
Pero sin duda, la parte más importante de nuestra clase tiene que ver con la sobrecarga de
los operadores que utilizamos normalmente para realizar las operaciones matemáticas
(suma, resta, multiplicación, etc.). Y lógicamente las dejaremos contabilizadas en sus
respectivos contadores.
Por un lado tenemos los operadores unarios, aquellos en los que sólo interviene una
instancia.
Y por el otro, tenemos los operadores binarios, aquellos en los que intervienen dos
instancias.
¡Y ya tenemos nuestra clase lista!.
Usar la clase
Utilizar nuestra clase dentro de cualquier programa es trivial, podemos usarla exactamente
de la misma forma que utilizamos los tipos básicos double o float. Este es un ejemplo
trivial y su salida:
Algunos operadores pueden ser sobrecargados. Aunque la finalidad es la misma, la sintaxis
para declarar la sobrecarga de operadores difiere en su sintaxis con respecto a la de los
métodos.
No todos los operadores pueden sobrecargarse y, de los que se pueden, algunos tienen
restricciones.
Operadores unarios.
+ , - , ! , � , ++ , -- , true , false
Operadores binarios.
Los operadores relacionales deben sobrecargarse por parejas; esto es, si se sobrecarga = = ,
también debe sobrecargarse != . Lo mismo se aplica a < y > , y a <= y >= .
Operadores lógicos.
&& , ||
// Cuerpo
}
Ejemplo 1
using System;
class Fraccion
{
num = n;
den = d;
public Fraccion( )
{
class Principal
{
r = x + y;
Console.WriteLine("{0:F3}",(double)(x+y));
Console.WriteLine("{0:F3}",(double)(x*y));
Ejemplo 2
using System;
class Complejo
{
real = r;
imag = im;
public Complejo()
{
}
Console.WriteLine("{0:F0},{1:F0}i",w.real, w.imag);
}
class Principal
{
herencia simple y
herencia múltiple.
En la herencia simple, una clase sucesora hereda de una sola clase antecesora directa
(llamada clase base, en C#) , como se muestra en la Figura 5.2.1.
Figura 5.2.1.- Ejemplo de herencia simple
Las flechas en la Figura 5.2.1 representan la relación "es un" o "es una" , por lo que deben
dirigirse desde la clase sucesora hacia la antecesora.
En la herencia múltiple, al menos una clase sucesora hereda de más de una clase
antecesora directa , como se muestra en la Figura 5.3.1.
Figura 5.3.1.- Ejemplo de herencia múltiple
§1 Sinopsis
class hotel {
char * nombre;
int capacidad;
public:
void put(char*);
void put(long);
void put(int);
}
La función put, definida tres veces con el mismo nombre está sobrecargada; el
compilador sabe en cada caso qué definición usar por la naturaleza de los
argumentos (puntero-a-char, long o int).
Es evidente que en L.4 y L.5 los argumentos formales y los actuales concuerdan,
bien porque se trata del mismo tipo, bien porque se ha realizado una promoción
explícita ( 4.9.9).
Dentro del ámbito de visibilidad existe una función fun (L.6) que acepta un float y
devuelve un objeto del tipo necesitado por getx. Aunque el argumento x disponible
no es precisamente un float, puede ser promovido fácilmente a dicho tipo. En
consecuencia el compilador utiliza en L.15 la siguiente invocación:
Este tipo de adecuaciones automáticas son realizadas por el compilador tanto con
funciones-miembro [2] como con funciones normales. Considere cuidadosamente el
ejemplo que sigue, e intente justificar el proceso seguido para obtener cada uno de
sus resultados.
#include <iostream>
using namespace std;
int x = 10;
long lg = 10.0;
class Entero {
public:
int x;
int getx (int i) { return x * i; }
int getx () { return x; }
int getx (Entero e1, Entero e2) { return e1.x + e2.x; }
Entero(float f= 1.0) { x = f; }
};
int getx (int i) { return x * i; }
int getx () { return x; }
int getx (Entero e1, Entero e2) { return e1.getx() + e2.getx(); }
Entero fun (float f= 1.0) {
Entero e1 = { f }; return e1;
}
E1 = 0 F1 = 0
E2 = 10 F2 = 10
E3 = 970 F3 = 970
E4 = 100 F4 = 100
E5 = 20 F5 = 20
E6 = 107 F6 = 107
Una Sobrecarga de metodos simplemente es tener dos metodos distintos pero con el
mismo nombre y con diferente tipo de parametro o con diferente número de
parametros de entrada.
aqui va un ejemplo..
using System;
namespace Saludo
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
System.Console.Write("Escribe el Nombre ");
string Nombre=System.Console.ReadLine();
Class1 Clase=new Class1();//Instancio la clase
Clase.Saludo();//Metodo 1
Clase.Saludo(Nombre);//Metodo sobrecargado
System.Console.ReadLine();
public void Saludo(string Nombre)//si puede observar tiene el mismo nombre del
{ //del metodo anterior pero posee un parametro de entrada
//Se dice que estàn sobrecargados
System.Console.WriteLine("Hola "+Nombre);
}
}
}
Concepto de método
La ejecución de las instrucciones de un método puede depender del valor de unas
variables especiales denominadas parámetros del método, de manera que en
función del valor que se dé a estas variables en cada llamada la ejecución del
método se pueda realizar de una u otra forma y podrá producir uno u otro valor de
retorno.
Definición de métodos
Para definir un método hay que indicar tanto cuáles son las instrucciones que
forman su cuerpo como cuál es el nombre que se le dará, cuál es el tipo de objeto
que puede devolver y cuáles son los parámetros que puede tomar. Esto se indica
definiéndolo así:
<tipoRetorno> <nombreMétodo>(<parámetros>)
<cuerpo>
En <tipoRetorno> se indica cuál es el tipo de dato del objeto que el método devuelve, y si
no devuelve ninguno se ha de escribir void en su lugar.
Aunque es posible escribir métodos que no tomen parámetros, si un método los
toma se ha de indicar en <parámetros> cuál es el nombre y tipo de cada uno,
separándolos con comas si son más de uno y siguiendo la sintaxis que más adelante
se explica.
El <cuerpo> del método también es opcional, pero si el método retorna algún tipo
de objeto entonces ha de incluir al menos una instrucción return que indique cuál
objeto.
int Saluda()
Console.WriteLine("Hola Mundo");
return 1;
Llamada a métodos
La forma en que se puede llamar a un método depende del tipo de método del que
se trate. Si es un método de objeto (método no estático) se ha de usar la notación:
<objeto>.<nombreMétodo>(<valoresParámetros>)
El <objeto> indicado puede ser directamente una variable del tipo de datos al que
pertenezca el método o puede ser una expresión que produzca como resultado una
variable de ese tipo (recordemos que, debido a la herencia, el tipo del <objeto>
puede ser un subtipo del tipo donde realmente se haya definido el método); pero si
desde código de algún método de un objeto se desea llamar a otro método de ese
mismo objeto, entonces se ha de dar el valor this a <objeto>.
En caso de que sea un método de tipo (método estático), entones se ha de usar:
<tipo>.<nombreMétodo>(<valoresParámetros>)
Ahora en <tipo> ha de indicarse el tipo donde se haya definido el método o algún
subtipo suyo. Sin embargo, si el método pertenece al mismo tipo que el código que
lo llama entonces se puede usar la notación abreviada:
<nombreMétodo>(<valoresParámetros>)
El formato en que se pasen los valores a cada parámetro en <valoresParámetros> a
aquellos métodos que tomen parámetros depende del tipo de parámetro que sea. Esto
se explica en el siguiente apartado.
La forma en que se define cada parámetro de un método depende del tipo de
parámetro del que se trate. En C# se admiten cuatro tipos de parámetros: parámetros
de entrada, parámetros de salida, parámetros por referencia y parámetros de número
indefinido.
Parámetros de entrada
Un parámetro de entrada recibe una copia del valor que almacenaría una
variable del tipo del objeto que se le pase. Por tanto, si el objeto es de un tipo valor
se le pasará una copia del objeto y cualquier modificación que se haga al parámetro
dentro del cuerpo del método no afectará al objeto original sino a su copia; mientras
que si el objeto es de un tipo referencia entonces se le pasará una copia de la
referencia al mismo y cualquier modificación que se haga al parámetro dentro del
método también afectará al objeto original ya que en realidad el parámetro referencia
a ese mismo objeto original.
Para definir un parámetro de entrada basta indicar cuál el nombre que se le desea
dar y el cuál es tipo de dato que podrá almacenar. Para ello se sigue la siguiente
sintaxis:
<tipoParámetro> <nombreParámetro>
Por ejemplo, el siguiente código define un método llamado Suma que toma dos
parámetros de entrada de tipo int llamados par1 y par2 y devuelve un int con su
suma:
return par1+par2;
18. Tiene que sobrescribir todos los métodos abstractos de la clase de base. Agregue el
siguiente método a la clase SavingsAccount:
19. public override double CalculateBankCharge()
20. {
21. if (balance < minBalance)
22. return 5.00;
23. else
24. return 0.00;
}
Volver al principio
9. Genere la aplicación.
10. Establezca un punto de interrupción al principio del método Main e inicie la
aplicación en el depurador. Deténgase en cada instrucción y observe qué métodos
son llamados durante la ejecución de la aplicación. La aplicación muestra la
siguiente información en la consola:
11. Name=Laura Villa, balance=100
12. recargo de $5 si el saldo es inferior a $25
13. Name=Laura Barrios, balance=20
14. recargo de $5 si el saldo es inferior a $25
Gastos bancarios: $5
Una clase que herede de otra, puede usar los métodos, las propiedades o las variables de la
clase padre. También se puede redefinir o modificar los métodos y propiedades, usando
algunas palabras reservadas para esto, creando así un nuevo comportamiento del objeto.
Una condición necesaria para que una clase herede de otra, es que la clase hija (la que
hereda), debe poder usarse donde se use la clase padre (de la cual va a heredar). Si hay al
menos un caso donde se puede usar la clase padre, y no se puede usar la clase hija, la
herencia no tiene sentido.
En C#, una clase solo puede heredar de otra (por algunas razones difíciles de explicar). O
sea, una clase no puede heredar de varias clases a la vez. Para que una clase herede de otra
nada más hay que ponerle ‘:‘ después de su declaración, y seguido poner el nombre de la
clase de la que se quiere heredar.
Ejemplo:
Si una clase hija tiene un constructor que recibe los mismos parámetros que un constructor
de la clase padre, y este constructor cumple las mismas funciones, que en la clase padre, el
de la clase padre puede ser llamado desde la clase hija poniendo después de la declaración
del constructor en la clase hija : y después la palabra reservada base. Dentro de los
paréntesis van los parámetros del constructor base, (también veremos un ejemplo).
También es importante saber que si una clase hereda de otra, que a su vez hereda de otra,
esta clase hereda también de la clase más arriba.
Para que una clase padre permita que uno de sus métodos sea redefinido por sus clases
herederas (las que heredan de ella) este método debe ser declarado usando la palabra
reservada virtual, y cuando la clase hija vaya a redefinirlo, debe declararlo usando la
palabra reservada override.
Ejemplo:
Si una clase hijo quiere redefinir un método de una clase padre, y este no fue declarado
virtual en la clase padre, entonces hay que poner la palabra reservada new en la
declaración del método.
Ejemplo:
Ejemplo de herencia:
El constructor de la clase Hijo que recibe como parámetros un entero llama al constructor
de la clase Padre que recibe un entero pasándoselo como parámetro.
Ahora veremos un ejemplo real donde se vea la importancia de este importante mecanismo.
Imaginen, que queremos tener algunas figuras geométricas, poder calcular el area,
perimetro, moverlas en un plano, etc. Pero nos damos cuenta enseguida, que todas las
figuras tienen area y perímetro, solo que se calculan de diferentes formas. Todas se pueden
ocultar, mostrar y mover de igual forma, no? Veamos ya el código, para que sigan esta
idea:
Antes de pasar al código, hay que definir que una jerarquía de clases es un grupo de clases,
que heredan unas de otras, donde existe una clase base de la cual heredan todos los
integrantes de la jerarquía, en este caso, la clase base es la clase figura.
El mecanismo de herencia es uno de los pilares fundamentales en los que se basa la
programación orientada a objetos. Es un mecanismo que permite definir nuevas clases a
partir de otras ya definidas de modo que si en la definición de una clase indicamos que ésta
deriva de otra, entonces la primera -a la que se le suele llamar clase hija- será tratada por el
compilador automáticamente como si su definición incluyese la definición de la segunda –a
la que se le suele llamar clase padre o clase base. Las clases que derivan de otras se
definen usando la siguiente sintaxis:
class <nombreHija>:<nombrePadre>
{
<miembrosHija>
}
class Trabajador:Persona
{
public int Sueldo;
public Trabajador (string nombre, int edad, string nif, int sueldo)
:base(nombre, edad, nif)
{
Sueldo = sueldo;
}
}
Los objetos de esta clase Trabajador contarán con los mismos miembros que los objetos
Persona y además incorporarán un nuevo campo llamado Sueldo que almacenará el dinero
que cada trabajador gane. Nótese además que a la hora de escribir el constructor de esta
clase ha sido necesario escribirlo con una sintaxis especial consistente en preceder la llave
de apertura del cuerpo del método de una estructura de la forma:
: base(<parametrosBase>)
A esta estructura se le llama inicializador base y se utiliza para indicar cómo deseamos
inicializar los campos heredados de la clase padre. No es más que una llamada al
constructor de la misma con los parámetros adecuados, y si no se incluye el compilador
consideraría por defecto que vale :base(), lo que sería incorrecto en este ejemplo debido a
que Persona carece de constructor sin parámetros.
using System;
class Persona
{
// Campo de cada objeto Persona que almacena su nombre
Nótese que ha sido necesario prefijar la definición de los miembros de Persona del
palabra reservada public. Esto se debe a que por defecto los miembros de una tipo sólo son
accesibles desde código incluido dentro de la definición de dicho tipo, e incluyendo public
conseguimos que sean accesibles desde cualquier código, como el método Main() definido
en Trabajador. public es lo que se denomina un modificador de acceso, concepto que se
tratará más adelante en este mismo tema bajo el epígrafe titulado Modificadores de acceso.
Si en la definición del constructor de alguna clase que derive de otra no incluimos
inicializador base el compilador considerará que éste es :base() Por ello hay que estar
seguros de que si no se incluye base en la definición de algún constructor, el tipo padre del
tipo al que pertenezca disponga de constructor sin parámetros.
<nombreClase>(): base()
{}
Es decir, este constructor siempre llama al constructor sin parámetros del padre del tipo que
estemos definiendo, y si ése no dispone de alguno se producirá un error al compilar.
Métodos virtuales
Ya hemos visto que es posible definir tipos cuyos métodos se hereden de definiciones de
otros tipos. Lo que ahora vamos a ver es que además es posible cambiar dichar definición
en la clase hija, para lo que habría que haber precedido con la palabra reservada virtual la
definición de dicho método en la clase padre. A este tipo de métodos se les llama métodos
virtuales, y la sintaxis que se usa para definirlos es la siguiente:
El lenguaje C# impone la restricción de que toda redefinición de método que queramos
realizar incorpore la partícula override para forzar a que el programador esté seguro de que
verdaderamente lo que quiere hacer es cambiar el significado de un método heredado. Así
se evita que por accidente defina un método del que ya exista una definición en una clase
padre. Además, C# no permite definir un método como override y virtual a la vez, ya que
ello tendría un significado absurdo: estaríamos dando una redefinición de un método que
vamos a definir.
Por otro lado, cuando definamos un método como override ha de cumplirse que en
alguna clase antecesora (su clase padre, su clase abuela, etc.) de la clase en la que se ha
realizado la definición del mismo exista un método virtual con el mismo nombre que el
redefinido. Si no, el compilador informará de error por intento de redefinición de método
no existente o no virtual. Así se evita que por accidente un programador crea que está
redefiniendo un método del que no exista definición previa o que redefina un método que el
creador de la clase base no desee que se pueda redefinir.
Para aclarar mejor el concepto de método virtual, vamos a mostrar un ejemplo en el que
cambiaremos la definición del método Cumpleaños() en los objetos Persona por una nueva
versión en la que se muestre un mensaje cada vez que se ejecute, y redefiniremos dicha
nueva versión para los objetos Trabajador de modo que el mensaje mostrado sea otro. El
código de este ejemplo es el que se muestra a continuación:
using System;
class Persona
{
// Campo de cada objeto Persona que almacena su nombre
public string Nombre;
// Campo de cada objeto Persona que almacena su edad
public int Edad;
// Campo de cada objeto Persona que almacena su NIF
public string NIF;
// Incrementa en uno de la edad del objeto Persona
También es importante señalar que para que la redefinición sea válida ha sido necesario
añadir la partícula public a la definición del método original, pues si no se incluyese se
consideraría que el método sólo es accesible desde dentro de la clase donde se ha definido,
lo que no tiene sentido en métodos virtuales ya que entonces nunca podría ser redefinido.
De hecho, si se excluyese el modificador public el compilador informaría de un error ante
este absurdo. Además, este modificador también se ha mantenido en la redefinición de
Cumpleaños() porque toda redefinición de un método virtual ha de mantener los mismos
modificadores de acceso que el método original para ser válida.
Clases abstractas
Una clase abstracta es aquella que forzosamente se ha de derivar si se desea que se
puedan crear objetos de la misma o acceder a sus miembros estáticos (esto último se verá
más adelante en este mismo tema) Para definir una clase abstracta se antepone abstract a
su definición, como se muestra en el siguiente ejemplo:
Las clases A y B del ejemplo son abstractas, y como puede verse es posible combinar en
cualquier orden el modificador abstract con modificadores de acceso.
La utilidad de las clases abstractas es que pueden contener métodos para los que no se dé
directamente una implementación sino que se deje en manos de sus clases hijas darla. No es
obligatorio que las clases abstractas contengan métodos de este tipo, pero sí lo es marcar
como abstracta a toda la que tenga alguno. Estos métodos se definen precediendo su
definición del modificador abstract y sustituyendo su código por un punto y coma (;),
como se muestra en el método F() de la clase A del ejemplo (nótese que B también ha de
definirse como abstracta porque tampoco implementa el método F() que hereda de A)
Obviamente, como un método abstracto no tiene código no es posible llamarlo. Hay que
tener especial cuidado con esto a la hora de utilizar this para llamar a otros métodos de un
mismo objeto, ya que llamar a los abstractos provoca un error al compilar.
Véase que todo método definido como abstracto es implícitamente virtual, pues si no
sería imposible redefinirlo para darle una implementación en las clases hijas de la clase
abstracta donde esté definido. Por ello es necesario incluir el modificador override a la
hora de darle implementación y es redundante marcar un método como abstract y virtual a
la vez (de hecho, hacerlo provoca un error al compilar)
Es posible marcar un método como abstract y override a la vez, lo que convertiría al
método en abstracto para sus clases hijas y forzaría a que éstas lo tuviesen que
reimplementar si no se quisiese que fuesen clases abstractas.
La clase primegenia: System.Object
Ahora que sabemos lo que es la herencia es el momento apropiado para explicar que
en .NET todos los tipos que se definan heredan implícitamente de la clase System.Object
predefinida en la BCL, por lo que dispondrán de todos los miembros de ésta. Por esta
razón se dice que System.Object es la raíz de la jerarquía de objetos de .NET.
A continuación vamos a explicar cuáles son estos métodos comunes a todos los objetos:
public virtual bool Equals(object o): Se usa para comparar el objeto sobre el que se aplica
con cualquier otro que se le pase como parámetro. Devuelve true si ambos objetos son
iguales y false en caso contrario.
La implementación que por defecto se ha dado a este método consiste en usar igualdad por
referencia para los tipos por referencia e igualdad por valor para los tipos por valor. Es
decir, si los objetos a comparar son de tipos por referencia sólo se devuelve true si ambos
objetos apuntan a la misma referencia en memoria dinámica, y si los tipos a comparar son
tipos por valor sólo se devuelve true si todos los bits de ambos objetos son iguales, aunque
se almacenen en posiciones diferentes de memoria.
Como se ve, el método ha sido definido como virtual, lo que permite que los
programadores puedan redefinirlo para indicar cuándo ha de considerarse que son iguales
dos objetos de tipos definidos por ellos. De hecho, muchos de los tipos incluidos en la BCL
cuentan con redefiniciones de este tipo, como es el caso de string, quien aún siendo un tipo
por referencia, sus objetos se consideran iguales si apuntan a cadenas que sean iguales
carácter a carácter (aunque referencien a distintas direcciones de memoria dinámica)
El siguiente ejemplo muestra cómo hacer una redefinición de Equals() de manera que
aunque los objetos Persona sean de tipos por referencia, se considere que dos Personas son
iguales si tienen el mismo NIF:
Hay que tener en cuenta que es conveniente que toda redefinición del método Equals()
que hagamos cumpla con una serie de propiedades que muchos de los métodos
incluidos en las distintas clases de la BCL esperan que se cumplan. Estas propiedades son:
Hay que recalcar que el hecho de que redefinir Equals() no implica que el operador de
igualdad (==) quede también redefinido. Ello habría que hacerlo de independientemente
como se indica en el Tema 11: Redefinición de operadores.
public virtual int GetHashCode(): Devuelve un código de dispersión (hash) que representa
de forma numérica al objeto sobre el que el método es aplicado. GetHashCode() suele
usarse para trabajar con tablas de dispersión, y se cumple que si dos objetos son iguales
sus códigos de dispersión serán iguales, mientras que si son distintos la probabilidad de
que sean iguales es ínfima.
public virtual string ToString(): Devuelve una representación en forma de cadena del
objeto sobre el que se el método es aplicado, lo que es muy útil para depurar aplicaciones
ya que permite mostrar con facilidad el estado de los objetos.
La implementación por defecto de este método simplemente devuelve una cadena de texto
con el nombre de la clase a la que pertenece el objeto sobre el que es aplicado. Sin
embargo, como lo habitual suele ser implementar ToString() en cada nueva clase que es
defina, a continuación mostraremos un ejemplo de cómo redefinirlo en la clase Persona
para que muestre los valores de todos los campos de los objetos Persona:
Es de reseñar el hecho de que en realidad los que hace el operador de concatenación de
cadenas (+) para concatenar una cadena con un objeto cualquiera es convertirlo primero en
cadena llamando a su método ToString() y luego realizar la concatenación de ambas
cadenas.
Del mismo modo, cuando a Console.WriteLine() y Console.Write() se les pasa como
parámetro un objeto lo que hacen es mostrar por la salida estándar el resultado de
convertirlo en cadena llamando a su método ToString(); y si se les pasa como parámetros
una cadena seguida de varios objetos lo muestran por la salida estándar esa cadena pero
sustituyendo en ella toda subcadena de la forma {<número>} por el resultado de convertir
en cadena el parámetro que ocupe la posición <número>+2 en la lista de valores de llamada
al método.
protected object MemberWiseClone(): Devuelve una copia shallow copy del objeto sobre
el que se aplica. Esta copia es una copia bit a bit del mismo, por lo que el objeto resultante
de la copia mantendrá las mismas referencias a otros que tuviese el objeto copiado y toda
modificación que se haga a estos objetos a través de la copia afectará al objeto copiado y
viceversa.
Si lo que interesa es disponer de una copia más normal, en la que por cada objeto
referenciado se crease una copia del mismo a la que referenciase el objeto clonado,
entonces el programador ha de escribir su propio método clonador pero puede servirse de
MemberwiseClone() como base con la que copiar los campos que no sean de tipos
referencia.
protected virtual void Finalize(): Contiene el código que se ejecutará siempre que vaya ha
ser destruido algún objeto del tipo del que sea miembro. La implementación dada por
defecto a Finalize() consiste en no hacer nada.
Aparte de los métodos ya comentados que todos los objetos heredan, la clase
System.Object también incluye en su definición los siguientes métodos de tipo:
public static bool Equals(object objeto1, object objeto2) à Versión estática del método
Equals() ya visto. Indica si los objetos que se le pasan como parámetros son iguales, y para
compararlos lo que hace es devolver el resultado de calcular objeto1.Equals(objeto2)
comprobando antes si alguno de los objetos vale null (sólo se devolvería true sólo si el
otro también lo es)
True
True
False
False
En los primeros casos se devuelve true porque según la redefinición de Equals() dos
personas son iguales si tienen el mismo DNI, como pasa con los objetos p y q. Sin
embargo, en los últimos casos se devuelve false porque aunque ambos objetos tienen el
mismo DNI cada uno se almacena en la memoria dinámica en una posición distinta, que es
lo que comparan ReferenceEquals() y el operador == (éste último sólo por defecto)
A través de la herencia, una clase puede utilizarse como más de un tipo; puede utilizarse
como su propio tipo, cualquier tipo base o cualquier tipo de interfaz si implementa
interfaces. Esto se denomina polimorfismo. En C#, todos los tipos son polimórficos. Los
tipos se pueden utilizar como su propio tipo o como una instancia de Object, porque
cualquier tipo trata automáticamente a Object como tipo base.
El polimorfismo no sólo es importante para las clases derivadas, sino también para las
clases base. Cualquiera que utilice la clase base podría, de hecho, estar utilizando un objeto
de la clase derivada que se haya convertido en el tipo de clase base. Los diseñadores de una
clase base pueden anticipar qué aspectos de su clase base cambiarán probablemente para un
tipo derivado. Por ejemplo, es posible que una clase base para automóviles contenga un
comportamiento sujeto a cambios si el automóvil en cuestión es un vehículo familiar o uno
descapotable. Una clase base puede marcar esos miembros de clase como virtuales, lo cual
permite que las clases derivadas que representan automóviles descapotables y vehículos
familiares reemplacen ese comportamiento.
Para reemplazar un miembro de una clase base por un nuevo miembro derivado, se requiere
la palabra clave new. Si una clase base define un método, campo o propiedad, la palabra
clave new se utiliza para crear una nueva definición de ese método, campo o propiedad en
una clase derivada. La palabra clave new se coloca antes del tipo de valor devuelto de un
miembro de clase que se reemplaza. Por ejemplo:
VB
C#
C++
F#
JScript
Copiar
public class BaseClass
{
public void DoWork() { }
public int WorkField;
public int WorkProperty
{
get { return 0; }
}
}
Cuando se utiliza la palabra clave new, se llama a los nuevos miembros de clase en lugar de
los miembros de clase base que se han reemplazado. Esos miembros de clase base se
denominan miembros ocultos. Aún es posible llamar a los miembros de clase ocultos si una
instancia de la clase derivada se convierte en una instancia de la clase base. Por ejemplo:
VB
C#
C++
F#
JScript
Copiar
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.
BaseClass A = (BaseClass)B;
A.DoWork(); // Calls the old method.
Para que una instancia de una clase derivada controle por completo un miembro de clase de
una clase base, la clase base debe declarar ese miembro como virtual. Esto se consigue
agregando la palabra clave virtual antes del tipo de valor devuelto del miembro. Una clase
derivada tiene entonces la opción de utilizar la palabra clave override, en lugar de new,
para reemplazar la implementación de la clase base por la propia. Por ejemplo:
VB
C#
C++
F#
JScript
Copiar
public class BaseClass
{
public virtual void DoWork() { }
public virtual int WorkProperty
{
get { return 0; }
}
}
public class DerivedClass : BaseClass
{
public override void DoWork() { }
public override int WorkProperty
{
get { return 0; }
}
}
Los campos no pueden ser virtuales. Sólo los métodos, propiedades, eventos e indizadores
pueden serlo. Cuando una clase derivada reemplaza un miembro virtual, se llama a ese
miembro aunque se tenga acceso a una instancia de esa clase como instancia de la clase
base. Por ejemplo:
VB
C#
C++
F#
JScript
Copiar
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.
BaseClass A = (BaseClass)B;
A.DoWork(); // Also calls the new method.
Los métodos y propiedades virtuales permiten hacer planes para una expansión futura. El
hecho de llamar a un miembro virtual sin importar cuál es el tipo que el llamador utiliza
proporciona a las clases derivadas la opción de cambiar completamente el comportamiento
aparente de la clase base.
VB
C#
C++
F#
JScript
Copiar
public class A
{
public virtual void DoWork() { }
}
public class B : A
{
public override void DoWork() { }
}
VB
C#
C++
F#
JScript
Copiar
public class C : B
{
public override void DoWork() { }
}
Una clase derivada puede detener la herencia virtual si la sustitución se declara como
sellada. Para ello es necesario colocar la palabra clave sealed antes de la palabra clave
override en la declaración del miembro de clase. Por ejemplo:
VB
C#
C++
F#
JScript
Copiar
public class C : B
{
public sealed override void DoWork() { }
}
VB
C#
C++
F#
JScript
Copiar
public class D : C
{
public new void DoWork() { }
}
En este caso, si se llama a DoWork en D mediante una variable de tipo D, se llama al nuevo
DoWork. Si se utiliza una variable de tipo C, B o A para tener acceso a una instancia de D,
una llamada a DoWork seguirá las reglas de la herencia virtual y enrutará esas llamadas a la
implementación de DoWork en la clase C.
Una clase derivada que ha reemplazado un método o propiedad todavía puede tener acceso
al método o propiedad de la clase base utilizando la palabra clave base. Por ejemplo:
VB
C#
C++
F#
JScript
Copiar
public class A
{
public virtual void DoWork() { }
}
public class B : A
{
public override void DoWork() { }
}
VB
C#
C++
F#
JScript
Copiar
public class C : B
{
public override void DoWork()
{
// Call DoWork on B to get B's behavior:
base.DoWork();
Polimorfismo
Polimorfismo
A través de la herencia, una clase puede utilizarse como más de un tipo; puede utilizarse como su
propio tipo, cualquier tipo base o cualquier tipo de interfaz si implementa interfaces. Esto se
denomina polimorfismo. En C#, todos los tipos son polimórficos. Los tipos se pueden utilizar como
su propio tipo o como una instancia de Object, porque cualquier tipo trata automáticamente a
Object como tipo base.
El polimorfismo no sólo es importante para las clases derivadas, sino también para las clases base.
Cualquiera que utilice la clase base podría, de hecho, estar utilizando un objeto de la clase
derivada que se haya convertido en el tipo de clase base. Los diseñadores de una clase base
pueden anticipar qué aspectos de su clase base cambiarán probablemente para un tipo derivado.
Por ejemplo, es posible que una clase base para automóviles contenga un comportamiento sujeto
a cambios si el automóvil en cuestión es un vehículo familiar o uno descapotable. Una clase base
puede marcar esos miembros de clase como virtuales, lo cual permite que las clases derivadas que
representan automóviles descapotables y vehículos familiares reemplacen ese comportamiento.
Para reemplazar un miembro de una clase base por un nuevo miembro derivado, se requiere la
palabra clave new. Si una clase base define un método, campo o propiedad, la palabra clave new
se utiliza para crear una nueva definición de ese método, campo o propiedad en una clase
derivada. La palabra clave new se coloca antes del tipo de valor devuelto de un miembro de clase
que se reemplaza. Por ejemplo:
Cuando se utiliza la palabra clave new, se llama a los nuevos miembros de clase en lugar de los
miembros de clase base que se han reemplazado. Esos miembros de clase base se denominan
miembros ocultos. Aún es posible llamar a los miembros de clase ocultos si una instancia de la
clase derivada se convierte en una instancia de la clase base. Por ejemplo:
ClaseBase A = (ClaseBase)B;
A.DoWork(); // Llama el metodo viejo.
Para que una instancia de una clase derivada controle por completo un miembro de clase de una
clase base, la clase base debe declarar ese miembro como virtual. Esto se consigue agregando la
palabra clave virtual antes del tipo de valor devuelto del miembro. Una clase derivada tiene
entonces la opción de utilizar la palabra clave override, en lugar de new, para reemplazar la
implementación de la clase base por la propia. Por ejemplo:
Los campos no pueden ser virtuales. Sólo los métodos, propiedades, eventos e indizadores pueden
serlo. Cuando una clase derivada reemplaza un miembro virtual, se llama a ese miembro aunque
se tenga acceso a una instancia de esa clase como instancia de la clase base. Por ejemplo:
ClaseBase A = (ClaseBase)B;
A.DoWork(); // Tambien llama el metodo nuevo.
Los métodos y propiedades virtuales permiten hacer planes para una expansión futura. El hecho
de llamar a un miembro virtual sin importar cuál es el tipo que el llamador utiliza proporciona a las
clases derivadas la opción de cambiar completamente el comportamiento aparente de la clase
base.
public class A
{
public virtual void DoWork() { }
}
public class B : A
{
public override void DoWork() { }
}
public class C : B
{
public override void DoWork() { }
}
Una clase derivada puede detener la herencia virtual si la sustitución se declara como sellada. Para
ello es necesario colocar la palabra clave sealed antes de la palabra clave override en la
declaración del miembro de clase. Por ejemplo:
public class C : B
{
public sealed override void DoWork() { }
}
public class D : C
{
public new void DoWork() { }
}
En este caso, si se llama a DoWork en D mediante una variable de tipo D, se llama al nuevo
DoWork. Si se utiliza una variable de tipo C, B o A para tener acceso a una instancia de D, una
llamada a DoWork seguirá las reglas de la herencia virtual y enrutará esas llamadas a la
implementación de DoWork en la clase C.
Una clase derivada que ha reemplazado un método o propiedad todavía puede tener acceso al
método o propiedad de la clase base utilizando la palabra clave base. Por ejemplo:
public class A
{
public virtual void DoWork() { }
}
public class B : A
{
public override void DoWork() { }
}
public class C : B
{
public override void DoWork()
{
// Llama a DoWork en B para obtener el comportamiento de B:
base.DoWork();