Menu

Herencia en C#: base, virtual, override, sealed y polimorfismo

Cómo funciona la herencia de clases en C#: derivar con dos puntos, qué se hereda y qué no, llamar a la clase base con base, virtual y override, ocultar con new (y la salida que sorprende a todo el mundo), sealed, el polimorfismo y las conversiones hacia arriba y hacia abajo en una jerarquía.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

La herencia permite que una clase se construya sobre otra. La clase derivada recibe los miembros de la clase base, añade los suyos y puede sustituir el comportamiento que la clase base le permite sustituir. Combinada con los métodos virtual, te da polimorfismo: el código escrito contra la clase base ejecuta el comportamiento derivado correcto sin saber qué clase derivada tiene.

Derivar una clase

Pon la clase base después de dos puntos. La clase derivada tiene todo lo que tiene la clase base, más lo que declara ella:

Salida:

TX-19: 20 km, 2 fares
True

Lo que una clase derivada recibe y lo que no:

  • Se hereda: campos, propiedades, métodos, eventos y tipos anidados. Todo está físicamente en el objeto.
  • Es accesible: solo lo que la base permite: los miembros public, protected e internal. Un miembro private de Vehicle existe dentro de cada Taxi, pero el código de Taxi no puede nombrarlo. Por eso Km tiene un private set: Taxi puede leerlo, y solo lo cambia mediante Drive.
  • No se hereda: los constructores. Taxi debe declarar los suyos y encadenarlos con uno de los de Vehicle mediante : base(plate). La página de constructores muestra el orden en que se ejecutan los dos.

Toda clase deriva en última instancia de object, y por eso todo objeto tiene ToString(), Equals() y GetHashCode().

virtual y override

Una clase base marca un método como virtual para decir "las clases derivadas pueden aportar su propia versión". Una clase derivada lo sustituye con override. Dentro de la sobrescritura, base.Method() llama a la versión de la clase base.

Salida:

[email] to lea@example.com: Your order 1042 has shipped today
  (unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today

La variable del bucle es de tipo Notification y, aun así, cada objeto se representa a su manera. Eso es el polimorfismo: la llamada n.Render(...) se resuelve en tiempo de ejecución a partir del tipo real del objeto. Fíjate también en que base.Render en Email usa Channel, que a su vez es virtual, así que el método base imprime email, no generic. Una llamada virtual dentro de la clase base sigue llegando a la sobrescritura.

Las propiedades también pueden ser virtuales, como muestra Channel. Los campos no.

El compilador te obliga a usar las dos palabras clave. Escribir override en un método que no es virtual es el error CS0506 ("cannot override inherited member ... because it is not marked virtual, abstract, or override"). Omitir override cuando el método base es virtual es solo un aviso, y cambia por completo el significado, como muestra la siguiente sección.

new frente a override: ocultar en lugar de sobrescribir

Si una clase derivada declara un método con la misma firma que un método base sin escribir override, oculta el método base. El compilador avisa (CS0114 con un método base virtual, CS0108 en otro caso) y sugiere la palabra clave new, que silencia el aviso pero mantiene el comportamiento de ocultación:

Salida:

Sales report
Report
Draft report

b y c son el mismo tipo de objeto y, aun así, imprimen títulos distintos. Con new, el método elegido depende del tipo de la variable, decidido en tiempo de compilación. El código que maneja los informes como Report (una lista, un parámetro de un método, un callback de un framework) nunca ve DraftReport.Title. Eso casi nunca es lo que quieres. Usa override para el polimorfismo; new existe sobre todo para el caso en que una clase base que no controlas añade un miembro cuyo nombre choca con uno que ya tenías.

sealed

sealed en una clase prohíbe derivar de ella:

sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'

string es sealed, igual que muchos tipos del framework. En una sobrescritura, sealed detiene la cadena en ese nivel:

class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed

Diseñar una clase para la herencia lleva trabajo: decidir qué es virtual, en qué pueden confiar las clases derivadas, en qué orden ocurren las cosas. Una clase que no se diseñó así es más segura sellada, y el sellado puede deshacerse más adelante sin romper nada, mientras que quitar el sellado no tiene vuelta atrás una vez que otros derivan de ti. Las llamadas a miembros de clases sealed también pueden ser algo más rápidas, porque el runtime sabe que no existe ninguna sobrescritura.

Una clase base, muchas interfaces

Una clase de C# tiene exactamente una clase base. class Admin : User, Employee es el error CS1721 ("cannot have multiple base classes"). Una clase sí puede, en cambio, implementar cualquier número de interfaces junto a su clase base:

class Admin : User, IAuditable, IComparable<Admin>
{
    // base class first, then interfaces, in any order
}

Usa una clase base para "es un tipo de, y comparte implementación con", y las interfaces para "sabe hacer". Cuando la clase base solo existe para obligar a las clases derivadas a rellenar algunos métodos, la herramienta es una clase abstracta.

Conversiones hacia arriba y hacia abajo en la jerarquía

Un objeto derivado siempre puede usarse donde se espera su tipo base. Esa conversión hacia arriba (upcast) es implícita y no puede fallar. Ir en el otro sentido, una conversión hacia abajo (downcast), necesita un cast explícito y falla en tiempo de ejecución si el objeto no es de ese tipo:

Salida:

Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball

Escribir Dog d = pet; sin el cast es un error de compilación (CS0266: "An explicit conversion exists (are you missing a cast?)"), porque el compilador solo sabe que pet es algún Animal. Prefiere is con una variable cuando el objeto pueda ser de otro tipo, y un cast normal solo cuando cualquier otra cosa sería un bug. Hacer muchas conversiones hacia abajo es señal de un problema de diseño: normalmente significa que el comportamiento debería estar en un método virtual de la clase base. La página de pattern matching explica todas las formas de is.

Herencia y colecciones

El polimorfismo funciona elemento a elemento, pero las colecciones genéricas de un tipo derivado no son colecciones del tipo base: List<Animal> animals = new List<Dog>(); no compila, porque entonces la lista aceptaría un Cat. Las vistas de solo lectura son covariantes, así que IEnumerable<Animal> animals = new List<Dog>(); no da problemas. La página de genéricos explica por qué.

Errores comunes

  • Olvidar override. El método compila con un aviso y oculta en silencio en lugar de sobrescribir. Trata CS0114 como un error.
  • Hacerlo todo virtual. Cada miembro virtual es una promesa a las clases derivadas sobre cuándo se llama y qué puede suponer. Marca solo los puntos de extensión que pretendes ofrecer.
  • Jerarquías profundas. Tres o cuatro niveles de herencia hacen difícil saber qué versión de un método se ejecuta. La composición, una clase que contiene otro objeto y lo llama, suele ser más sencilla que un nivel nuevo.
  • Llamar a métodos virtuales desde un constructor. La sobrescritura se ejecuta antes del cuerpo del constructor derivado, así que cualquier campo que asigne ese cuerpo sigue teniendo su valor por defecto.
  • Usar la herencia solo para reutilizar código. Si un Stack hereda de List, quien lo usa puede hacer Insert en mitad de tu pila. Guarda una List en un campo privado.

Preguntas frecuentes

¿Cómo funciona la herencia en C#?

Una clase indica una clase base después de dos puntos: class Dog : Animal. La clase derivada recibe todos los miembros de la clase base (campos, propiedades, métodos, eventos), puede añadir los suyos y puede sobrescribir los que la base marcó como virtual. Los constructores no se heredan, y los miembros private, aunque están presentes en el objeto, no son accesibles desde la clase derivada.

¿Qué diferencia hay entre virtual y override en C#?

virtual va en el método de la clase base y dice "las clases derivadas pueden sustituir esto". override va en el método de la clase derivada y hace la sustitución. Hacen falta los dos: sobrescribir un método que no es virtual, abstract ni ya override es el error CS0506. Cuando se llama a un método virtual, el runtime ejecuta la versión del tipo real del objeto, no la del tipo de la variable.

¿Qué diferencia hay entre new y override en C#?

override sustituye el método base para todos los que llaman, incluido el código que tiene el objeto a través de una variable de la clase base. new solo lo oculta: el código que ve el objeto como el tipo derivado llama al método nuevo, mientras que el código que lo ve como el tipo base sigue llamando al método base. Así, Base b = new Derived(); b.M(); ejecuta Derived.M con override y Base.M con new.

¿Admite C# la herencia múltiple?

No para las clases: una clase tiene exactamente una clase base, e indicar dos es el error CS1721. Una clase puede implementar cualquier número de interfaces, que es como C# modela "este tipo sabe hacer varias cosas". Desde C# 8, las interfaces también pueden llevar implementaciones de métodos por defecto.

¿Qué significa sealed en C#?

Una clase sealed no puede usarse como clase base; derivar de ella es el error CS0509. string es sealed, por ejemplo. En un método, sealed override impide que las clases que están más abajo en la jerarquía vuelvan a sobrescribirlo (CS0239). Sellar las clases que no se diseñaron para la herencia es una opción por defecto razonable.

¿Cómo llamo al método de la clase base en C#?

Usa base.MethodName(...) dentro de la clase derivada, normalmente dentro de la sobrescritura: public override string Describe() => base.Describe() + " with GPS";. En los constructores, usa : base(...) después de la lista de parámetros para elegir qué constructor base se ejecuta.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR