1. Introducción
El tema de hoy puede parecerte un poco abstracto, pero créeme, sin esto no vas a ningún lado. Vamos a hablar de la herencia en programación. Hoy solo la vamos a tocar un poco, y más adelante la veremos en detalle.
Imagina que estás escribiendo un programa para llevar el control de los animales en un zoo. Tienes diferentes tipos de animales: leones, tigres, elefantes, loros. Todos ellos son animales. Pero cada uno tiene sus particularidades:
- El león tiene melena.
- El tigre es rayado.
- El elefante es muy grande y tiene trompa.
- El loro sabe hablar.
Si describiéramos cada animal por separado, el código sería muy parecido:
// León
public class Lion
{
public string Name { get; set; }
public int Age { get; set; }
public string Species { get; set; } = "León"; // Siempre "León"
public void Eat() { Console.WriteLine("León come carne."); }
public void Sleep() { Console.WriteLine("León duerme."); }
public void Roar() { Console.WriteLine("León ruge!"); }
public string ManeColor { get; set; } // Particularidad del león - melena
}
// Tigre
public class Tiger
{
public string Name { get; set; }
public int Age { get; set; }
public string Species { get; set; } = "Tigre"; // Siempre "Tigre"
public void Eat() { Console.WriteLine("Tigre come carne."); }
public void Sleep() { Console.WriteLine("Tigre duerme."); }
public void Stride() { Console.WriteLine("Tigre acecha."); }
public string StripePattern { get; set; } // Particularidad del tigre - rayas
}
¿Ves cuánto código duplicado hay? Name, Age, Eat(), Sleep() — ¡esto es común para todos los animales! ¿Y si tuviéramos 100 tipos de animales? ¡Sería un caos!
Aquí es donde la herencia nos salva.
2. ¿Qué es la herencia de clases en C#?
Herencia — es uno de los principios fundamentales de la programación orientada a objetos (POO). Permite crear nuevas clases basadas en otras ya existentes. La nueva clase (hija, clase hija, clase derivada) recibe (hereda) todas las propiedades (campos) y comportamientos (métodos) de su clase padre (clase base, clase padre).
Es como en la vida real: heredas algunas características de tus padres, pero también tienes tus propias particularidades.
En C#, la herencia se indica con dos puntos : después del nombre de la clase hija:
// Clase padre (base) - Animal
public class Animal
{
public string Name { get; set; } // Nombre del animal
public int Age { get; set; } // Edad del animal
public void Eat()
{
Console.WriteLine($"{Name} come.");
}
public void Sleep()
{
Console.WriteLine($"{Name} duerme.");
}
}
// Clase hija (derivada) - Lion hereda de Animal
public class Lion : Animal // <-- ¡Aquí está la herencia!
{
public string ManeColor { get; set; } // Propiedad única del león
public void Roar() // Comportamiento único del león
{
Console.WriteLine($"{Name} ruge: RRRRRR!");
}
}
// Otra clase hija - Tiger hereda de Animal
public class Tiger : Animal
{
public string StripePattern { get; set; } // Propiedad única del tigre
public void Stride() // Comportamiento único del tigre
{
Console.WriteLine($"{Name} acecha silenciosamente.");
}
}
Ahora, si creamos un objeto Lion o Tiger, automáticamente tendrán las propiedades Name y Age, así como los métodos Eat() y Sleep(), porque los han heredado de Animal.
class Program
{
static void Main(string[] args)
{
Lion simba = new Lion();
simba.Name = "Simba";
simba.Age = 5;
simba.ManeColor = "Dorado";
simba.Eat(); // Método heredado de Animal
simba.Sleep(); // Método heredado de Animal
simba.Roar(); // Método propio de Lion
Console.WriteLine($"{simba.Name} - edad: {simba.Age}, color de melena: {simba.ManeColor}");
Tiger shereKhan = new Tiger();
shereKhan.Name = "Shere Khan";
shereKhan.Age = 7;
shereKhan.StripePattern = "Clásico";
shereKhan.Eat(); // Método heredado de Animal
shereKhan.Sleep(); // Método heredado de Animal
shereKhan.Stride(); // Método propio de Tiger
Console.WriteLine($"{shereKhan.Name} - edad: {shereKhan.Age}, patrón: {shereKhan.StripePattern}");
}
}
Ideas clave de la herencia:
- Reutilización de código (Code Reusability): Evitas duplicar código poniendo la lógica común en la clase base.
- Jerarquía (Hierarchy): Se crea una estructura lógica "general-específico" (Animal → Lion/Tiger).
- Polimorfismo (Polymorphism): (Solo lo mencionamos por ahora, lo veremos más adelante) Puedes trabajar con objetos de clases hijas usando referencias a su clase base. Por ejemplo, tener una lista de Animal y meter ahí leones, tigres, y luego llamar al método Eat() en todos ellos.
3. Llamar a constructores de la clase base (base)
Cuando creas un objeto de una clase hija (por ejemplo, new Lion()), C# llama automáticamente al constructor de la clase base (en nuestro caso Animal) antes de llamar al constructor de la clase hija. Esto asegura que la parte base del objeto se inicialice correctamente.
A veces necesitas pasar parámetros al constructor de la clase base. Para eso se usa la palabra clave base después de la firma del constructor de la clase hija.
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
// Constructor de la clase base
public Animal(string name, int age)
{
Name = name;
Age = age;
Console.WriteLine($"Constructor Animal: Animal creado {Name}, edad {Age}.");
}
public void Eat()
{
Console.WriteLine($"{Name} come.");
}
public void Sleep()
{
Console.WriteLine($"{Name} duerme.");
}
}
public class Lion : Animal
{
public string ManeColor { get; set; }
// Constructor de Lion que llama al constructor de la clase base Animal
public Lion(string name, int age, string maneColor) : base(name, age) // <-- ¡Aquí está 'base'!
{
ManeColor = maneColor;
Console.WriteLine($"Constructor Lion: Color de melena {ManeColor}.");
}
public void Roar()
{
Console.WriteLine($"{Name} ruge: RRRRRR!");
}
}
class Program
{
static void Main(string[] args)
{
// Al crear Lion, primero se llama al constructor de Animal, luego al de Lion
Lion simba = new Lion("Simba", 5, "Dorado");
simba.Roar();
}
}
¿Qué pasa cuando ejecutas new Lion("Simba", 5, "Dorado")?
- Se llama al constructor Lion(string name, int age, string maneColor).
- Gracias a : base(name, age), el control pasa al constructor Animal(string name, int age).
- Se ejecuta el código del constructor Animal y verás Constructor Animal: Animal creado Simba, edad 5.
- Después el control vuelve al constructor de Lion.
- Se ejecuta el código del constructor de Lion y verás Constructor Lion: Color de melena Dorado.
Muy importante: ¡la clase base siempre se inicializa primero!
4. Operadores is y as
En el mundo de la programación en C# te vas a encontrar muchas veces con situaciones donde necesitas comprobar el tipo de un objeto o intentar convertirlo a otro tipo. Aquí es donde entran los operadores is y as. Son especialmente útiles cuando trabajas con herencia y polimorfismo, de los que hablaremos más adelante, pero ya puedes entender lo básico ahora.
Operador is (comprobación de tipo)
El operador is te permite comprobar si un objeto es compatible con un tipo determinado. Devuelve true si el objeto se puede convertir al tipo indicado, y false en caso contrario.
Sintaxis: expresión is Tipo
static void AnalyzeObject(object obj)
{
if (obj is string)
{
Console.WriteLine("¡Esto es una cadena!");
}
else if (obj is int)
{
Console.WriteLine("¡Esto es un número entero!");
}
else
{
Console.WriteLine("Esto es otra cosa.");
}
}
static void Main()
{
AnalyzeObject("¡Hola, mundo!"); // ¡Esto es una cadena!
AnalyzeObject(123); // ¡Esto es un número entero!
AnalyzeObject(3.14); // Esto es otra cosa.
AnalyzeObject(new int[] { 1, 2 }); // Esto es otra cosa.
}
is con pattern matching:
Las versiones modernas de C# permiten usar is no solo para comprobar el tipo, sino también para crear una variable de ese tipo si coincide. Esto se llama "pattern matching" y hace el código mucho más limpio.
static void AnalyzeObjectWithPatternMatching(object obj)
{
if (obj is string mensaje) // Si obj es string, se asigna a la variable mensaje
{
Console.WriteLine($"Esto es una cadena, su longitud: {mensaje.Length}");
}
else if (obj is int numero) // Si obj es int, se asigna a la variable numero
{
Console.WriteLine($"Esto es un número, multiplicado por 2: {numero * 2}");
}
else
{
Console.WriteLine("Tipo no reconocido.");
}
}
static void Main()
{
AnalyzeObjectWithPatternMatching("Ejemplo"); // Esto es una cadena, su longitud: 7
AnalyzeObjectWithPatternMatching(50); // Esto es un número, multiplicado por 2: 100
AnalyzeObjectWithPatternMatching(new object()); // Tipo no reconocido.
}
Este enfoque con is y pattern matching es mucho mejor que el casteo tradicional seguido de comprobación de null, porque es más seguro y legible.
Operador as (conversión segura de tipo)
El operador as intenta convertir un objeto al tipo indicado. Si la conversión es posible, devuelve el objeto convertido a ese tipo. Si no es posible (es decir, el objeto no es compatible con el tipo destino), as devuelve null en vez de lanzar una excepción. Por eso as es un operador de conversión "segura".
Sintaxis: expresión as Tipo
static void ProcessData(object data)
{
string texto = data as string; // Intentamos convertir data a string
if (texto != null) // Comprobamos si la conversión fue exitosa
{
Console.WriteLine($"Procesando cadena: {texto.ToUpper()}");
}
else
{
Console.WriteLine("Los datos no son una cadena.");
}
}
static void Main()
{
ProcessData("hello"); // Procesando cadena: HELLO
ProcessData(123); // Los datos no son una cadena.
ProcessData(null); // Los datos no son una cadena.
}
¿Cuándo usar as en vez de un casteo directo (Tipo)expresión?
- Seguridad: Si no estás seguro de que el objeto sea del tipo correcto, as te permite evitar InvalidCastException y en su lugar obtener null, que puedes manejar.
- Con tipos de referencia: as solo funciona con tipos de referencia (clases, interfaces, delegados, arrays). No se puede usar para convertir tipos de valor (por ejemplo, de int a double).
Importante: Si estás seguro de que el objeto siempre será del tipo correcto, o si necesitas convertir un tipo de valor, usa el casteo directo (Tipo)expresión. Si la conversión no es posible, el casteo directo lanzará un error (InvalidCastException), lo cual puede ser lo que quieres si un tipo incorrecto es un error en la lógica de tu programa.
GO TO FULL VERSION