You are on page 1of 63

Sobrecarga de operadores

La sobrecarga de operadores es una poderosa herramienta de programación. Con ella


podemos por ejemplo, obtener estadísticas de todas las operaciones que se realizan en un
pograma de forma muy sencilla, de manera que conozcamos que coste en sumas,
multiplicaciones, divisiones, etc... tienen determinados algoritmos en la práctica (no en
todos los algoritmos es sencillo calcular analíticamente su coste concreto en operaciones
por lo que se deben usar análisis empíricos).

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.

Definición del tipo a usar

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.

¿Qué vamos a hacer exactamente?

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.

El registrar estadísticas de todas las operaciones (matemáticas) que realiza un programa


conlleva un coste adicional (el que supone realizar ese registro), lógicamente podremos
configurar la clase para que las registre o no y, en tal caso, no afecte en absoluto a las
versiones release.

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.

Otras propiedades estáticas

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!.

Nota importante sobre los operadores sobrecargados

Desgraciadamente, no es habitual que se pueda especificar la precedencia de los operadores


sobrecargados. Concretamente en C# no es posible establecer la precedencia de operadores
y no es posible por tanto sobrecargar el operador ^ de forma que tenga mayor prioridad que
el producto y la división (sí se puede sobrecargar, pero no forzar una prioridad de
precedencia mayor). En C# la tabla de precedencia es la siguiente (al menos en Visual C#
2005 versión 8.0):
Aunque podríamos usar otros operadores con mayor prioridad (como []) pierde el sentido
de claridad y por tanto es mejor usar un método como x.Pow( y ).

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.

C# permite la sobrecarga de operadores definiendo métodos estáticos que usan la palabra


operator.

No todos los operadores pueden sobrecargarse y, de los que se pueden, algunos tienen
restricciones.

A continuación se listan los operadores que pueden sobrecargarse y las restricciones


existentes para algunos de ellos.

Operadores unarios.

+ , - , ! , � , ++ , -- , true , false

Operadores binarios.

+ , - , * , / , % , & , | , ^ , << , >>

Operadores relacionales ( condicionales ).

= = , != , < , > , <= , >=

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.

&& , ||

Sintaxis para la sobrecarga de operadores.

[modificadores] <nombreClase> operator <nombreOperador> ( [parámetros] )


{

// Cuerpo

}
Ejemplo 1

//Fraccion.cs . Sobrecarga los operadores + y * para sumar y multiplicar


// números fraccionarios.

using System;

class Fraccion
{

public int num, den;

public Fraccion( int n, int d)


{

num = n;
den = d;

public Fraccion( )
{

public static Fraccion operator + ( Fraccion x, Fraccion y)


{

return new Fraccion(x.num*y.den + y.num*x.den, x.den*y.den);

public static Fraccion operator * ( Fraccion x, Fraccion y)


{

return new Fraccion( x.num *y.num, x.den*y.den);

public static explicit operator double(Fraccion f) //Convierte a double.


{
return (double) f.num / f.den;

class Principal
{

public static void Main( )


{

Fraccion x = new Fraccion(1,4);


Fraccion y = new Fraccion(3,2);
Fraccion r = new Fraccion( );

r = x + y;

Console.WriteLine(r.num + "/" + r.den);

Console.WriteLine("{0:F3}",(double)(x+y));
Console.WriteLine("{0:F3}",(double)(x*y));

Ejemplo 2

// Complejo.cs : Sobrecarga los operadores + y * para la suma y multiplicación


// de números complejos.

using System;

class Complejo
{

double real, imag;


public Complejo(double r , double im)
{

real = r;
imag = im;

public Complejo()
{
}

public static Complejo operator + ( Complejo x, Complejo y)


{

return new Complejo(x.real+y.real, x.imag+y.imag);

public static Complejo operator * ( Complejo x, Complejo y)


{

return new Complejo( x.real*y.real-x.imag*y.imag , x.real*y.imag+x.imag*y.real);

public static void despliega(Complejo w)


{

Console.WriteLine("{0:F0},{1:F0}i",w.real, w.imag);

}
class Principal
{

public static void Main( )


{

Complejo x = new Complejo(3,0);


Complejo y = new Complejo(5,0);
Complejo suma = new Complejo();
Complejo mult = new Complejo();
suma = x+y;
mult = x*y;
Complejo.despliega(suma);
Complejo.despliega(mult);

La herencia es un mecanismo mediante el cual se implementa una jerarquía de clases. Una


clase antecesora (clase base) hereda atributos y comportamientos a una clase sucesora
( clase derivada ), a la que se pueden agregar nuevos atributos y comportamientos. A su
vez, la clase derivada puede heredar sus atributos y comportamientos a otra nueva clase, de
manera tal que se puede establecer una jerarquía similar a la que se utiliza en la
clasificación taxonómica de las especies biológicas.

Existen dos tipos de herencia:

 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

En este caso, la clase C tiene dos clases antecesoras directas ( A y B )

4.11.2a2  Sobrecarga de métodos

§1  Sinopsis

Lo mismo que ocurre con las funciones normales ( 4.4.1), en la definición de


funciones-miembro puede ocurrir que varias funciones compartan el mismo nombre,
pero difieran en el tipo y/o número de argumentos. Ejemplo:

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).

Nota: no confundir estos casos de sobrecarga en métodos con el polimorfismo;


este último se refiere a jerarquías de clases en las que pueden existir diversas
definiciones de métodos en miembros de distintas generaciones (super-clases y
clases derivadas) con los mismos argumentos. Son las denominadas funciones
virtuales ( 4.11.8a).

§2  Adecuació n automá tica de argumentos

Una característica interesante, y a nuestro entender muy desafortunada del lenguaje


(por la posibilidad de errores difíciles de detectar), es que en la invocación de
funciones, sean estas miembros de clase o no, el compilador automáticamente
intenta adecuar el tipo del argumento actual con el formal ( 4.4.5). Es decir,
adecuar el argumento utilizado con el que espera la función según su definición.

Este comportamiento puede esquematizarse como sigue:

int funcion (int i) { /* ... */ }     // L.1:


....
otraFuncion (){
   int x = funcion(5);          // L.4: Ok. concordancia de argumentos
   int y = funcion(int('a'));   // L.5: Ok. promoción explícita
   int z = funcion('a');        // L.6: Ok. promoción automática!!
   ...
}

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).

Lo que no resulta ya tan evidente, ni deseable a veces, es que en L.6 el compilador


autorice la invocación a funcion pasando un tipo distinto del esperado. Esta
promoción del argumento actual realizada de forma automática por el compilador
(sin que medie ningún error o aviso -"Warning"- de compilación) puede ser origen
de errores, y de que el programa use inadvertidamente una función distinta de la
deseada. En este sentido podemos afirmar que el lenguaje C++ es débilmente tipado.

Posiblemente esta característica sea una de las desafortunadas herencias del C


clásico [1], y es origen incluso de ambigüedades que no deberían existir. Por
ejemplo, no es posible definir las siguientes versiones de un constructor (
4.11.2d):
class Entero {
   public: int x;
   Entero(float fl = 1.0) { x = int(fl); }
   Entero(char c) { x = int(c); }
}

A pesar de su evidente disparidad, el compilador interpreta que existe ambigüedad


en los tipos, ya que char puede ser provomido a float.

§2.1  Para complicar la cosa

Pero la conformación de argumentos no acaba con lo expuesto. Si en la invocación


de una función f1 no existe correspondencia entre el tipo de un argumento actual x, y
el formal y, el compilador puede buscar automáticamente en el ámbito si existe una
función f2 que acepte el argumento discordante x y devuelva el tipo y deseado. En
cuyo caso realizará una invocación implícita a dicha función f2(x) y aplicará el
resultado y como argumento a la función primitiva. El mecanismo involucrado
puede ser sintetizado en el siguiente esquema:

class C {                 // L.1


  public: int x;
  ...
};
int getx (C c1, C c2) { return c1.x + c2.x; }   // L.5
C fun (float f= 1.0) {    // L.6:
  C ct = { f };
  return ct;
}
...
unaFuncion () {
  int x = 10;
  C c1;
  ...
  int z = getx(x, c1);    // L.15
  ...
}

La invocación a getx en L.15 es correcta, a pesar de no existir una definición


concordante para esta función cuyos argumentos formales son dos objetos tipo C, y
en este caso el primer argumento x es un int. El mecanismo utilizado por el
compilador es el siguiente:

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:

int z = getx( fun( float(x) ), c1);

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;
}

void main () {    // ==========================


  Entero c1 = Entero(x);
  cout << "E1 = " << c1.getx(0) << endl;
  cout << "E2 = " << c1.getx() << endl;
  cout << "E3 = " << c1.getx('a') << endl;
  cout << "E4 = " << c1.getx(lg) << endl;
  cout << "E5 = " << c1.getx(x, c1) << endl;
  cout << "E6 = " << c1.getx('a', c1) << endl;
  cout << "F1 = " << getx(0) << endl;
  cout << "F2 = " << getx() << endl;
  cout << "F3 = " << getx('a') << endl;
  cout << "F4 = " << getx(lg) << endl;
  cout << "F5 = " << getx(x, c1) << endl;
  cout << "F6 = " << getx('a', c1) << endl;
}

Salida (reformateada en dos columnas):

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&#250;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()


{
System.Console.WriteLine("Hola");
}

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&#224;n sobrecargados
System.Console.WriteLine("Hola "+Nombre);
}
}
}

Saludo=Metodo simplemente pos consola saluda


Para sobrecrgarlo le envio una variable de tipo string y se la concateno al saludo
intentalo pidiendo por consola el nombre de la persona.

Concepto de método

    Un método es un conjunto de instrucciones a las que se les da un determinado


nombre de tal manera que sea posible ejecutarlas en cualquier momento sin tenerlas
que rescribir sino usando sólo su nombre. A estas instrucciones se les denomina
cuerpo del método, y a su ejecución a través de su nombre se le denomina llamada
al método.

    La ejecución de las instrucciones de un método puede producir como resultado un


objeto de cualquier tipo. A este objeto se le llama valor de retorno del método y es
completamente opcional, pudiéndose escribir métodos que no devuelvan ninguno.

    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.

    Al conjunto formado por el nombre de un método y el número y tipo de sus


parámetros se le conoce como signatura del método. La signatura de un método es
lo que verdaderamente lo identifica, de modo que es posible definir en un mismo
tipo varios métodos con idéntico nombre siempre y cuando tengan distintos
parámetros. Cuando esto ocurre se dice que el método que tiene ese nombre está
sobrecargado.

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.

    Como nombre del método se puede poner en <nombreMétodo> cualquier


identificador válido. Como se verá más adelante en el Tema 15: Interfaces, también
es posible incluir en <nombreMétodo> información de explicitación de
implementación de interfaz, pero por ahora podemos considerar que siempre será un
identificador.

    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.

    La sintaxis anteriormente vista no es la que se usa para definir métodos


abstractos. Como ya se vio en el Tema 5: Clases, en esos casos lo que se hace es
sustituir el cuerpo del método y las llaves que lo encierran por un simple punto y
coma (;) Más adelante en este tema veremos que eso es también lo que se hace para
definir métodos externos.

    A continuación se muestra un ejemplo de cómo definir un método de nombre


Saluda cuyo cuerpo consista en escribir en la consola el mensaje “Hola Mundo” y
que devuelva  un objeto int de valor 1:

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.

Tipos de parámetros. Sintaxis de definición

    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:

int Suma(int par1, int par2)

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

Comprobar que funciona

1. Muestre el código de Class1.cs en la ventana Ver código.


2. En el método Main, cree un objeto SavingsAccount y muéstrelo de la manera
siguiente:
3. SavingsAccount sa = new SavingsAccount("Laura Villa", 100.00,
25);
sa.Display();

4. Agregue el código siguiente para llamar a métodos públicos en SavingsAccount o


en Cuenta:
5. sa.Credit(100);
6. sa.Debit(180);
7. sa.ChangeName("Laura Barrios");
8. sa.Display();
Console.WriteLine("Bank charge: ${0}",
sa.CalculateBankCharge());

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

Como todo lenguaje de programación actual, C# es un lenguaje orientado a objetos y


soporta todas las características propias del paradigma de programación orientada a objetos:
encapsulación, herencia y polimorfismo.
En este post trataré de explicar de la forma más sencilla posible, lo que es la herencia en
programación, y pondré algunos ejemplos al final para ver si se llevan la idea, aunque
seguro se quedan con algunas dudas, ya que es un concepto fácil de entender, pero difícil de
aplicar en programación… después hablaremos entonces de lo que es la encapsulación y el
polimorfismo

Por definición, la herencia no es más que la capacidad de un objeto de heredar las


características de otro. Bueno, en programación, la herencia es la capacidad de una clase de
heredar los métodos, las variables y las propiedades de otra (los constructores no se
heredan). Esto permite ahorrar código, ya que varias funcionalidades no tendríamos que
implementarlas nuevamente.

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:

class Circulo: Figura


{
// Implementación de la clase
}

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.

Por ejemplo, si un cuadrilátero es una figura y un paralelogramo es un cuadrilátero,


entonces un paralelogramo es una figura…

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:

public <strong>virtual</strong> void Método()


//declaración del método en la clase Padre
{
//aquí va el codigo
}
 
public override void Método()
//declaración del método en la clase Hijo
{
//nuevas instrucciones
}

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:

public void Método()


//declaración del método en la clase Padre
{
//aquí va el codigo
}
 
public new void Método()
//declaración del método en la clase Hijo
{
//nuevas instrucciones
}

Ejemplo de herencia:

public class Hijo:Padre


{
 
//la clase Hijo hereda de la clase Padre
public Hijo(int n):base(n){}
}

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.

//Declaramos la clase padre, de la que todas las figuras heredarán


//Es abstracta porque tendrá algunos metodos abstractos
 
public abstract class Figura
{
//Usamos <strong>protected,</strong> para que solo tengan acceso
//a estas variables, las clases que heredan de ella
protected int x,y;
protected Control control;
protected Pen myPen;
protected bool visible;
 
//Un constructor por defecto para todas las figuras
public Figura(int x, int y, Color color, Control control)
{
//Validar parametros
this.x = x;
this.y = y;
this.control = control;
myPen = new Pen(color,4);
 
}
public abstract void Muestra();
public abstract int Area();
public virtual void Oculta()
{
Color temp = myPen.Color;
myPen.Color = control.BackColor;
Muestra();
myPen.Color = temp;
visible = false;
}
public virtual void Traslada(int plusX, int plusY)
{
if(visible)
{
Oculta();
x += plusX;
y += plusY;
Muestra();
}
else
{
x += plusX;
y += plusY;
}
Ahora veremos el código de otras dos figuras en concreto que heredarán de la clase figura,
un circulo y un rectángulo.

public class Circulo:Figura


{
//Vemos que en esta clase solo hay que declarar
//una variable
protected int radio;
//Este constructor tendrá todos los parámetros
//por defecto y un radio porque es un circulo
public Circulo(int x, int y, Color miColor, Control miControl,
int radio)
:base(x,y,miColor,miControl)
{
//Validacion
this.radio = radio;
}
//Ahora hay que programar como se mostrará la figura
public override void Muestra()
{
Graphics g = control.CreateGraphics();
g.DrawEllipse(myPen,x-radio,y-radio,radio*2,radio*2);
visible = true;
}
 
//Decimos como se calcula el area en este caso
public override int Area()
{
return Math.PI * Math.PI * radio;
}
 
//Le podemos añadir otro método independiente
public int Perimetro()
{
return 2 * Math.PI * radio;
}
}
 
//Esta clase también heredará de figura
public class Rectangulo:Figura
{
protected int ancho,alto;
 
public Rectangulo(int x, int y, Color miColor, Control miControl,
int ancho, int alto)
:base(x,y,miColor,miControl)
{
//Validar
this.ancho = ancho;
this.alto = alto;
}
//Redefinimos como mostrar la figura
public override void Muestra()
{
Graphics g = control.CreateGraphics();
g.DrawRectangle(myPen,x,y,ancho,alto);
visible = true;
}
//Redefinimos como calcular el area
public override int Area()
{
return alto * ancho;
}
}

Herencia y métodos virtuales


Concepto de herencia

    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>
}

    A los miembros definidos en <miembrosHijas> se le añadirán los que hubiésemos


definido en la clase padre. Por ejemplo, a partir de la clase Persona puede crearse una clase
Trabajador así:

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.

    Un ejemplo que pone de manifiesto cómo funciona la herencia es el siguiente:

 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;    

    void Cumpleaños()   // Incrementa en uno de edad del objeto Persona


    {
       Edad++;
    }
                                   
   // Constructor de Persona
    public Persona (string nombre, int edad, string nif)
    {
       Nombre = nombre;
       Edad = edad;
       NIF = nif;
    }
 }
 
 class Trabajador: Persona
 {
 
// Campo de cada objeto Trabajador que almacena cuánto gana
    public int Sueldo;

 Trabajador(string nombre, int edad, string nif, int sueldo)


: base(nombre, edad, nif)
    {  // Inicializamos cada Trabajador en base al constructor de Persona
       Sueldo = sueldo;
    }
   
    public static void Main()
    {
       Trabajador p = new Trabajador("Josan", 22, "77588260-Z", 100000);   

       Console.WriteLine ("Nombre="+p.Nombre);


       Console.WriteLine ("Edad="+p.Edad);
       Console.WriteLine ("NIF="+p.NIF);
       Console.WriteLine ("Sueldo="+p.Sueldo);
    }
 }

    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.

Llamadas por defecto al constructor base

    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.

    Es especialmente significativo reseñar el caso de que no demos la definición de ningún


constructor en la clase hija, ya que en estos casos la definición del constructor que por
defecto introducirá el compilador será en realidad de la forma:

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

virtual <tipoDevuelto> <nombreMétodo>(<parámetros>)


{
   <código>
}
    Si en alguna clase hija quisiésemos dar una nueva definición del del método, simplemente lo
volveríamos a definir en la misma pero sustituyendo en su definición la palabra reservada virtual
por override. Es decir, usaríamos esta sintaxis:

override <tipoDevuelto> <nombreMétodo>(<parámetros>)


{
      <nuevoCódigo>
}
    Nótese que esta posibilidad de cambiar el código de un método en su clase hija sólo se da si en
la clase padre el método fue definido como virtual. En caso contrario, el compilador considerará
un error intentar redefinirlo.

    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
    

     public virtual void Cumpleaños()


{      
Edad++;
      Console.WriteLine("Incrementada edad de persona");
     }
    
     // Constructor de Persona
     public Persona (string nombre, int edad, string nif)
     {
     Nombre = nombre;  
     Edad = edad;   
      NIF = nif;
     }
  }
 
  class Trabajador: Persona
  {
     // Campo de cada objeto Trabajador que almacena cuánto gana

public int Sueldo;     

     Trabajador(string nombre, int edad, string nif, int sueldo)


         : base(nombre, edad, nif)
     { // Inicializamos cada Trabajador en base al constructor de Persona
       Sueldo = sueldo;
     }
 
     public override void Cumpleaños()
     {
     Edad++;
       Console.WriteLine("Incrementada edad de trabajador");
     }
             
     public static void Main()
     {
        Persona p = new Persona("Carlos", 22, "77588261-Z");
        Trabajador t = new Trabajador("Josan", 22, "77588260-Z", 100000);
        t.Cumpleaños();
        p.Cumpleaños();     
     }
  }

    Nótese cómo se ha añadido el modificador virtual en la definición de Cumpleaños() en


la clase Persona para habilitar la posibilidad de que dicho método puede ser redefinido en
clase hijas de Persona y cómo se ha añado override en la redefinición del mismo dentro de
la clase Trabajador para indicar que la nueva definición del método es una redefinición del
heredado de la clase. La salida de este programa confirma que la implementación de
Cumpleaños() es distinta en cada clase, pues es de la forma:

 ncrementada edad de trabajador

 ncrementada edad de 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:

public abstract class A


{
    public abstract void F();
}
abstract public class B: A
{
    public void G() {}
}
class C: B
{
    public override void F(){}
}

    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:

public override bool Equals(object o)


{
if (o==null)
   return this==null;
else
   return (o is Persona) && (this.NIF == ((Persona) o).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:

 Reflexividad: Todo objeto ha de ser igual a sí mismo. Es decir, x.Equals(x) siempre ha de


devolver true.
 Simetría: Ha de dar igual el orden en que se haga la comparación. Es decir, x.Equals(y) ha
de devolver lo mismo que y.Equals(x) .
 Transitividad: Si dos objetos son iguales y uno de ellos es igual a otro, entonces el primero
también ha de ser igual a ese otro objeto. Es decir, si x.Equals(y) e y.Equals(z) entonces
x.Equals(z) .
 Consistencia: Siempre que el método se aplique sobre los mismos objetos ha de devolver
el mismo resultado.
 Tratamiento de objetos nulos: Si uno de los objetos comparados es nulo (null), sólo se ha
de devolver true si el otro también lo es.

    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.

En tanto que la búsqueda de objetos en tablas de dispersión no se realiza únicamente


usando la igualdad de objetos (método Equals()) sino usando también la igualdad de
códigos de dispersión, suele ser conveniente redefinir GetHashCode() siempre que se
redefina Equals() De hecho, si no se hace el compilador informa de la situación con un
mensaje de aviso.

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

public override string ToString()


{
   string cadena = "";
   cadena += "DNI = " + this.DNI + "\n";
   cadena += "Nombre = " + this.Nombre + "\n";
   cadena += "Edad = " + this.Edad + "\n";
   return cadena;
}

     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.

 public System.Type GetType(): Devuelve un objeto de clase System.Type que representa


al tipo de dato del objeto sobre el que el método es aplicado. A través de los métodos
ofrecidos por este objeto se puede acceder a metadatos sobre  el mismo como su nombre,
su clase padre, sus miembros, etc. La explicación de cómo usar los miembros de este
objeto para obtener dicha información queda fuera del alcance de este documento ya que
es muy larga y puede ser fácilmente consultada en la documentación que acompaña al
.NET SDK.

 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.

Aunque es un método virtual, en C# no se permite que el programador lo redefina


explícitamente dado que hacerlo es peligroso por razones que se explicarán en el Tema 8:
Métodos (otros lenguajes de .NET podrían permitirlo)

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)

Obviamente si se da una redefinición al Equals() no estático, sus efectos también se verán


cuando se llame al estático.
 public static bool ReferenceEquals(object objeto1, object objeto2) à Indica si los dos
objetos que se le pasan como parámetro se almacenan en la misma posición de memoria
dinámica. A través de este método, aunque se hayan redefinido Equals() y el operador de
igualdad (==) para un cierto tipo por referencia, se podrán seguir realizando
comparaciones por referencia entre objetos de ese tipo en tanto que redefinir de Equals()
no afecta a este método. Por  ejemplo, dada la anterior redefinición de Equals() para
objetos Persona:

Persona p = new Persona("José", 22, "83721654-W");


Persona q = new Persona("Antonio", 23, "83721654-W");
Console.WriteLine(p.Equals(q));
Console.WriteLine(Object.Equals(p, q));
Console.WriteLine(Object.ReferenceEquals(p, q));
Console.WriteLine(p == q);

    La salida que por pantalla mostrará el código anterior 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)

Polimorfismo (Guía de programación de


C#)
Visual Studio 2005
Otras versiones

 Visual Studio 2010


 Visual Studio 2008

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 obtener más información, vea Herencia.

Información general sobre el polimorfismo


Cuando una clase derivada hereda de una clase base, obtiene todos los métodos, campos,
propiedades y eventos de la clase base. Para cambiar los datos y el comportamiento de una
clase base, existen dos opciones: se puede reemplazar el miembro base por un nuevo
miembro derivado o se puede reemplazar un miembro base virtual.

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; }
}
}

public class DerivedClass : BaseClass


{
public new void DoWork() { }
public new int WorkField;
public new 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.

Los miembros virtuales siguen siendo virtuales de forma indefinida, independientemente de


cuántas clases se hayan declarado en la clase que originalmente declaró el miembro virtual.
Si la clase A declara un miembro virtual, la clase B deriva de A y la clase C deriva de B, la
clase C hereda el miembro virtual y tiene la opción de reemplazarlo, independientemente de
si la clase B declaró la sustitución de ese miembro. 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() { }
}

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() { }
}

En el ejemplo anterior, el método DoWork ya no es virtual para ninguna clase derivada de C.


Todavía es virtual para instancias de C, aunque se conviertan al tipo B o A. Los métodos
sellados se pueden reemplazar por clases derivadas mediante la palabra clave new, como se
muestra en el ejemplo siguiente:

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();

// DoWork behavior specific to C goes here:


// ...
}
}

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 obtener más información, vea Herencia.

Información general sobre el polimorfismo


Cuando una clase derivada hereda de una clase base, obtiene todos los métodos, campos,
propiedades y eventos de la clase base. Para cambiar los datos y el comportamiento de una clase
base, existen dos opciones: se puede reemplazar el miembro base por un nuevo miembro
derivado o se puede reemplazar un miembro base virtual.

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:

public class ClaseBase


{
public void DoWork() { }
public int WorkField;
public int WorkProperty
{
get { return 0; }
}
}

public class ClaseDerivada : ClaseBase


{
public new void DoWork() { }
public new int WorkField;
public new 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:

ClaseDerivada B = new ClaseDerivada();


B.DoWork(); // Llama el metodo nuevo.

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:

public class ClaseBase


{
public virtual void DoWork() { }
public virtual int WorkProperty
{
get { return 0; }
}
}
public class ClaseDerivada : ClaseBase
{
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:

ClaseDerivada B = new ClaseDerivada();


B.DoWork(); // Llama el metodo nuevo.

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.

Los miembros virtuales siguen siendo virtuales de forma indefinida, independientemente de


cuántas clases se hayan declarado en la clase que originalmente declaró el miembro virtual. Si la
clase A declara un miembro virtual, la clase B deriva de A y la clase C deriva de B, la clase C hereda
el miembro virtual y tiene la opción de reemplazarlo, independientemente de si la clase B declaró
la sustitución de ese miembro. 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() { }
}

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() { }
}

En el ejemplo anterior, el método DoWork ya no es virtual para ninguna clase derivada de C.


Todavía es virtual para instancias de C, aunque se conviertan al tipo B o A. Los métodos sellados se
pueden reemplazar por clases derivadas mediante la palabra clave new, como se muestra en el
ejemplo siguiente:

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();

// El comportamiento de DoWork que especifica a C iria aqui:


// ...
}
}

You might also like