1. Introducción
Cuando un principiante escucha la palabra herencia, puede parecerle que todo es fácil: solo tienes que coger una clase que ya existe, ampliarla o cambiarla un poco — ¡y listo! Pero en realidad hay un montón de matices. Los errores al heredar pueden aparecer de muchas formas: signaturas incorrectas, palabras clave olvidadas, mal diseño de la jerarquía — todo esto lleva a bugs complicados.
A veces los errores aparecen enseguida: el código ni siquiera compila. Pero otras veces, los bugs solo se dejan ver en tiempo de ejecución, cuando tu nuevo SuperMegaLogger escribe cualquier cosa menos lo que esperan los usuarios, o directamente no escribe nada. Tristeza, decepción, horas y horas de debug — ¿te suena?
Vamos a repasar juntos los errores más comunes relacionados con la herencia y la sobreescritura de métodos, y a ir "sanando" nuestro código por el camino.
2. Olvidar el virtual: ¿por qué no se puede sobreescribir un método?
El problema
En C#, solo puedes sobreescribir (override) los métodos que en la clase base están marcados con la palabra clave virtual, o como abstract, o como override de sí mismos (en la cadena de herencia). Si el método no tiene esa palabra, intentar poner override en la clase derivada te dará un error de compilación.
class Animal
{
public void Speak()
{
Console.WriteLine("El animal dice algo.");
}
}
class Cat : Animal
{
// ¡Error de compilación! El método en la clase base no es virtual, abstract ni override.
public override void Speak()
{
Console.WriteLine("¡Miau!");
}
}
El error será algo así: "'Cat.Speak()': cannot override inherited member 'Animal.Speak()' because it is not marked virtual, abstract, or override".
¿Cómo no caer en esta trampa?
Para poder sobreescribir métodos, tienes que declararlos en la clase base como virtual. Por cierto, si diseñas clases para que otros las amplíen, piensa siempre qué métodos pueden ser útiles para sobreescribir.
class Animal
{
public virtual void Speak()
{
Console.WriteLine("El animal hace un sonido.");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("¡Miau!");
}
}
¿Para qué sirve esto?
Al declarar un método como virtual, permites explícitamente que los hijos cambien su comportamiento, y el compilador se convierte en tu aliado — no dejará que sobreescribas "el método equivocado" por accidente.
3. Error con las palabras clave override y new
A veces pasa lo contrario: el autor de la clase derivada quiere "sobreescribir" un método, pero en la clase base no es virtual. Aquí el compilador no te deja usar override, pero sí te deja usar new. ¡Pero esto es un comportamiento totalmente distinto!
class Dog : Animal
{
// Esto no es override, sino ocultar (hiding) el método de la clase base.
public new void Speak()
{
Console.WriteLine("¡Guau!");
}
}
Si llamas a este método usando una referencia de tipo Dog — todo bien:
Dog dog = new Dog();
dog.Speak(); // "¡Guau!"
Pero si accedes al objeto usando una referencia del tipo base, se llamará al método base:
Animal dog2 = new Dog();
dog2.Speak(); // "El animal hace un sonido."
Explicación:
La palabra clave new NO sobreescribe el método, sino que esconde el del padre. Esto se llama "hiding". Este enfoque puede confundir, porque el comportamiento polimórfico no funciona como espera el programador.
¿Cómo evitar las trampas de new vs override?
Si quieres una sobreescritura clásica con polimorfismo — usa virtual/override. Si añades funcionalidad totalmente nueva (o quieres ocultar a propósito el comportamiento del método base, ¡ojo!), entonces usa new.
| Situación | Palabra clave | ¿Funciona el polimorfismo? | Comportamiento al acceder por el tipo base |
|---|---|---|---|
| Cambiar el comportamiento | override | Sí | Se llama al método de la clase derivada |
| Ocultar/reemplazar método | new | No | Se llama al método de la clase base |
4. Signaturas de métodos que no coinciden
Los principiantes, y a veces incluso los experimentados, se equivocan cambiando los tipos de los parámetros o el valor de retorno en el método hijo. Por ejemplo, si el método base está declarado como public virtual void Print(string message), y en la clase derivada "sobreescriben" public override void Print(object message), esto ya NO es override, sino una definición nueva de método.
class Printer
{
public virtual void Print(string msg)
{
Console.WriteLine("Impresora base: " + msg);
}
}
class SmartPrinter : Printer
{
// ¡Error de compilación! La signatura no coincide con el método base.
public override void Print(object msg)
{
Console.WriteLine("Impresora inteligente: " + msg);
}
}
Pista:
Tienen que coincidir el nombre del método, el tipo de retorno y los parámetros (por tipo, cantidad y orden).
Si cambiaste por accidente el tipo de un parámetro o te equivocaste en el nombre — el compilador te avisará.
5. Incompatibilidad de modificadores de acceso
Otra piedra con la que tropiezan muchos novatos — los modificadores de acceso. El método derivado no puede ser más privado que el de la base. Ejemplo de mal código:
public class Vehicle
{
public virtual void StartEngine() { /* ... */ }
}
public class Car : Vehicle
{
// ¡Error! El modificador 'private' es más restrictivo que 'public' en el método base.
private override void StartEngine() { /* ... */ }
}
¿Qué hacer?
El modificador de acceso en el método hijo debe ser igual o más abierto que el del método base. En la mayoría de los casos public o protected.
6. Método abstracto olvidado/de más
Si en la clase base el método está declarado como abstract, entonces en la clase hija es obligatorio hacer su override, si no la clase también se vuelve abstracta (y no se puede crear).
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
// ¡Error! No se ha implementado el método abstracto Area()
}
Solución:
Hay que implementar ese método:
class Circle : Shape
{
public override double Area()
{
return 3.14 * 2 * 2; // Más o menos...
}
}
7. Llamar a la implementación base: base.Method()
A veces no quieres reemplazar por completo la implementación de un método, sino añadirle algo. En este caso, muchos se olvidan (o no saben) que en el método hijo puedes llamar a la implementación de la clase base usando la palabra clave base.
class Logger
{
public virtual void Log(string msg)
{
Console.WriteLine("Log base: " + msg);
}
}
class FancyLogger : Logger
{
public override void Log(string msg)
{
// Puedes añadir "cositas" y llamar al método base:
Console.WriteLine("[FANCY] " + msg);
base.Log(msg);
}
}
Explicación:
Si no llamas a base.Log(msg), la lógica que estaba en la clase base se pierde por completo.
8. Llamar al constructor base (base)
Si la clase base necesita parámetros obligatorios en su constructor, la clase hija debe llamar explícitamente al constructor correspondiente usando la palabra clave base.
class Engine
{
public Engine(int cylinders)
{
Console.WriteLine("Motor con cilindros: " + cylinders);
}
}
class RaceEngine : Engine
{
// ¡Error de compilación! No hay constructor por defecto en Engine.
public RaceEngine() { }
}
// Versión corregida:
class RaceEngine2 : Engine
{
public RaceEngine2() : base(8) // Llamamos al constructor base explícitamente
{
Console.WriteLine("¡Motor de carreras listo!");
}
}
9. Olvidar marcar métodos como sealed
A veces necesitas prohibir que se siga sobreescribiendo un método en las clases hijas — para eso en C# existe la palabra clave sealed junto con override. Si no la usas, cualquiera puede sobreescribir tu método, y a lo mejor rompe tu lógica o tus expectativas.
class Hero
{
public virtual void Attack() => Console.WriteLine("¡El héroe ataca!");
}
class Warrior : Hero
{
public sealed override void Attack() => Console.WriteLine("¡El guerrero golpea!");
}
class Mutant : Warrior
{
// ¡Error! El método Attack está marcado como sealed arriba.
// public override void Attack() { ... }
}
Explicación:
Así "sellas" la implementación en ese nivel y las clases inferiores no podrán cambiarla.
10. Sobrecargas innecesarias o incorrectas en vez de sobreescritura
A veces los programadores confunden la sobrecarga de un método (overloading) con la sobreescritura (overriding). Sobrecargar es definir un método con el mismo nombre pero distinta signatura (por ejemplo, diferente número de parámetros), y no tiene nada que ver con el polimorfismo.
class Animal
{
public virtual void Eat()
{
Console.WriteLine("El animal come.");
}
}
class Panda : Animal
{
// ¡Esto NO es override! Solo es un método sobrecargado nuevo.
public void Eat(string what)
{
Console.WriteLine("La panda come: " + what);
}
}
...
Animal a = new Panda();
a.Eat(); // Si el método está sobreescrito — se llamará la implementación de la clase hija. Si no — se usa la base
// a.Eat("bambú"); // Error de compilación: ese método no existe en Animal
Para añadir comportamiento polimórfico, tienes que sobreescribir, no solo sobrecargar.
11. Errores de diseño formales e informales
Cuando las clases están mal diseñadas, las listas de herencia se vuelven un lío:
- una "calesita" interna de overrides sin usar bien el base.,
- uso inconsistente de modificadores,
- jerarquías demasiado profundas o poco claras,
- métodos cuya lógica está "repartida" entre varios niveles de herencia,
- falta de comentarios en métodos virtuales/abstract,
- efectos secundarios poco claros (por ejemplo, llamar a un método virtual en el constructor de la clase base, cuando la hija aún no está bien inicializada).
Consejo:
Si ves que te estás liando — no te cortes, dibuja en un papel (¡el old school funciona!) la jerarquía y apunta qué método está en cada sitio, quién lo sobreescribe, qué llama a qué, y dónde debería ir el base..
En proyectos reales es vital también dejar en la documentación qué se espera que haga cada método que se puede sobreescribir.
GO TO FULL VERSION