Herencia en C# y .NET
En este tutorial se presenta la herencia en C#. La herencia es una característica de lenguajes de programación orientados a objetos que permite definir una clase base que proporciona funcionalidad específica (datos y comportamiento) y definir clases derivadas que heredan o invalidan esa funcionalidad.
Prerrequisitos
- La versión más reciente del SDK de .NET
- Editor de Visual Studio Code
- El DevKit de C#
Instrucciones de instalación
En Windows, utilice este archivo de configuración de WinGet para instalar todos los requisitos previos. Si ya tiene algo instalado, WinGet omitirá ese paso.
- Descargue el archivo y haga doble clic para ejecutarlo.
- Lea el contrato de licencia, escriba yy seleccione Escriba cuando se le pida que acepte.
- Si recibe un mensaje de control de cuentas de usuario (UAC) parpadeante en la barra de tareas, permita que la instalación continúe.
En otras plataformas, debe instalar cada uno de estos componentes por separado.
- Descargue el instalador recomendado en la página de descarga del SDK de .NET de y haga doble clic para ejecutarlo. La página de descarga detecta la plataforma y recomienda el instalador más reciente para la plataforma.
- Descargue el instalador más reciente de la página principal de Visual Studio Code y haga doble clic para ejecutarlo. Esa página también detecta tu plataforma y el vínculo debe ser correcto para tu sistema operativo.
- Haga clic en el botón "Instalar" de la página de extensión C# DevKit. Se abre Visual Studio Code y se pregunta si desea instalar o habilitar la extensión. Seleccione "instalar".
Ejecución de los ejemplos
Para crear y ejecutar los ejemplos de este tutorial, use la utilidad dotnet desde la línea de comandos. Siga estos pasos para cada ejemplo:
Cree un directorio para almacenar el ejemplo.
Escriba el comando dotnet new console en el símbolo del sistema para crear un nuevo proyecto de .NET Core.
Copie y pegue el código del ejemplo en el editor de código.
Escriba el comando dotnet restore desde la línea de comandos para cargar o restaurar las dependencias del proyecto.
No es necesario ejecutar
dotnet restore
porque se ejecuta implícitamente por todos los comandos que requieren que se produzca una restauración, tales comodotnet new
,dotnet build
,dotnet run
,dotnet test
,dotnet publish
ydotnet pack
. Para deshabilitar la restauración implícita, use la opción--no-restore
.El comando
dotnet restore
sigue siendo útil en determinados escenarios en los que la restauración explícitamente tiene sentido, como compilaciones de integración continua en Azure DevOps Services o en sistemas de compilación que necesitan controlar explícitamente cuándo se produce la restauración.Para obtener información sobre cómo administrar fuentes de NuGet, consulte la documentación de
dotnet restore
.Escriba el comando dotnet run para compilar y ejecutar el ejemplo.
Contexto: ¿Qué es la herencia?
La herencia es uno de los atributos fundamentales de la programación orientada a objetos. Permite definir una clase secundaria que reutiliza (hereda), amplía o modifica el comportamiento de una clase primaria. La clase cuyos miembros se heredan se denomina clase base . La clase que hereda los miembros de la clase base se denomina clase derivada .
C# y .NET solo admiten herencia única. Es decir, una clase solo puede heredar de una sola clase. Sin embargo, la herencia es transitiva, lo que permite definir una jerarquía de herencia para un conjunto de tipos. En otras palabras, el tipo D
puede heredar del tipo C
, que hereda del tipo B
, que hereda del tipo de clase base A
. Dado que la herencia es transitiva, los miembros del tipo A
están disponibles para el tipo D
.
No todos los miembros de una clase base son heredados por las clases derivadas. Los siguientes miembros no se heredan:
constructores estáticos, que inicializan los datos estáticos de una clase.
constructores de instancia, a los que llamas para crear una nueva instancia de la clase. Cada clase debe definir sus propios constructores.
Finalizadores, llamados por el recolector de elementos no utilizados en tiempo de ejecución para destruir instancias de una clase.
Aunque todos los demás miembros de una clase base se heredan mediante clases derivadas, tanto si son visibles como si no dependen de su accesibilidad. La accesibilidad de un miembro afecta a su visibilidad para las clases derivadas de la siguiente manera:
Los miembros privados solo son visible en las clases derivadas que están anidadas en su clase base. De lo contrario, no son visibles en las clases derivadas. En el ejemplo siguiente,
A.B
es una clase anidada que deriva deA
yC
deriva deA
. El campoA._value
privado está visible en A.B. Sin embargo, si quita los comentarios del métodoC.GetValue
e intenta compilar el ejemplo, genera el error del compilador CS0122: "'A._value' no es accesible debido a su nivel de protección".public class A { private int _value = 10; public class B : A { public int GetValue() { return _value; } } } public class C : A { // public int GetValue() // { // return _value; // } } public class AccessExample { public static void Main(string[] args) { var b = new A.B(); Console.WriteLine(b.GetValue()); } } // The example displays the following output: // 10
Los miembros protegidos solo son visibles en las clases derivadas.
Los miembros internos son visibles solo en las clases derivadas que se encuentran en el mismo ensamblado que la clase base. No son visibles en las clases derivadas ubicadas en un ensamblado diferente de la clase base.
Los miembros públicos son visibles en las clases derivadas y forman parte de la interfaz pública de la clase derivada. Se puede llamar a los miembros heredados públicos como si estuvieran definidos en la clase derivada. En el ejemplo siguiente, la clase
A
define un método denominadoMethod1
y la claseB
hereda de la claseA
. A continuación, el ejemplo llama aMethod1
como si fuera un método de instancia enB
.public class A { public void Method1() { // Method implementation. } } public class B : A { } public class Example { public static void Main() { B b = new (); b.Method1(); } }
Las clases derivadas pueden también invalidar los miembros heredados al proporcionar una implementación alternativa. Para poder invalidar un miembro, el miembro de la clase base debe marcarse con la palabra clave virtual. De forma predeterminada, los miembros de clase base no están marcados como virtual
y no se pueden invalidar. Al intentar invalidar un miembro no virtual, como en el ejemplo siguiente, se genera el error del compilador CS0506: "<miembro> no puede invalidar el miembro heredado <miembro> porque no está marcado como virtual, abstracto o invalidado".
public class A
{
public void Method1()
{
// Do something.
}
}
public class B : A
{
public override void Method1() // Generates CS0506.
{
// Do something else.
}
}
En algunos casos, una clase derivada debe invalidar la implementación de la clase base. Los miembros de clase base marcados con la palabra clave abstract requieren que las clases derivadas los invaliden. Al intentar compilar el ejemplo siguiente se genera el error del compilador CS0534, "<clase> no implementa el miembro abstracto heredado <miembro>", porque la clase B
no proporciona ninguna implementación para A.Method1
.
public abstract class A
{
public abstract void Method1();
}
public class B : A // Generates CS0534.
{
public void Method3()
{
// Do something.
}
}
La herencia solo se aplica a clases e interfaces. Otras categorías de tipos (estructuras, delegados y enumeraciones) no admiten la herencia. Debido a estas reglas, al intentar compilar código como en el ejemplo siguiente se produce el error del compilador CS0527: "El tipo 'ValueType' en la lista de interfaz no es una interfaz". El mensaje de error indica que, aunque puede definir las interfaces que implementa una estructura, no se admite la herencia.
public struct ValueStructure : ValueType // Generates CS0527.
{
}
Herencia implícita
Además de cualquier tipo que puedan heredar de a través de una sola herencia, todos los tipos del sistema de tipos de .NET heredan implícitamente de Object o de un tipo derivado de él. La funcionalidad común de Object está disponible para cualquier tipo.
Para ver qué significa la herencia implícita, vamos a definir una nueva clase, SimpleClass
, que es simplemente una definición de clase vacía:
public class SimpleClass
{ }
A continuación, puede usar la reflexión (que permite inspeccionar los metadatos de un tipo para obtener información sobre ese tipo) para obtener una lista de los miembros que pertenecen al tipo de SimpleClass
. Aunque no ha definido ningún miembro de la clase SimpleClass
, la salida del ejemplo indica que realmente tiene nueve miembros. Uno de estos miembros es un constructor sin parámetros (o predeterminado) que el compilador de C# proporciona automáticamente para el tipo SimpleClass
. Los ocho restantes son miembros de Object, el tipo del que todas las clases e interfaces del sistema de tipos de .NET heredan implícitamente.
using System.Reflection;
public class SimpleClassExample
{
public static void Main()
{
Type t = typeof(SimpleClass);
BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
MemberInfo[] members = t.GetMembers(flags);
Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
foreach (MemberInfo member in members)
{
string access = "";
string stat = "";
var method = member as MethodBase;
if (method != null)
{
if (method.IsPublic)
access = " Public";
else if (method.IsPrivate)
access = " Private";
else if (method.IsFamily)
access = " Protected";
else if (method.IsAssembly)
access = " Internal";
else if (method.IsFamilyOrAssembly)
access = " Protected Internal ";
if (method.IsStatic)
stat = " Static";
}
string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
Console.WriteLine(output);
}
}
}
// The example displays the following output:
// Type SimpleClass has 9 members:
// ToString (Method): Public, Declared by System.Object
// Equals (Method): Public, Declared by System.Object
// Equals (Method): Public Static, Declared by System.Object
// ReferenceEquals (Method): Public Static, Declared by System.Object
// GetHashCode (Method): Public, Declared by System.Object
// GetType (Method): Public, Declared by System.Object
// Finalize (Method): Internal, Declared by System.Object
// MemberwiseClone (Method): Internal, Declared by System.Object
// .ctor (Constructor): Public, Declared by SimpleClass
La herencia implícita de la clase Object hace que estos métodos estén disponibles para la clase SimpleClass
:
El método público
ToString
, que convierte un objetoSimpleClass
en su representación de cadena, devuelve el nombre de tipo completo. En este caso, el métodoToString
devuelve la cadena "SimpleClass".Tres métodos que prueban la igualdad de dos objetos: el método de instancia pública
Equals(Object)
, el método estático públicoEquals(Object, Object)
y el método estático públicoReferenceEquals(Object, Object)
. De forma predeterminada, estos métodos prueban la igualdad de referencia; es decir, para que sea igual, dos variables de objeto deben hacer referencia al mismo objeto.El método público
GetHashCode
, que calcula un valor que permite usar una instancia del tipo en colecciones hash.El método
GetType
público, que devuelve un objeto Type que representa el tipoSimpleClass
.El método Finalize protegido, que está diseñado para liberar recursos no administrados antes de que el recolector de basura recupere la memoria de un objeto.
El método MemberwiseClone protegido, que crea un clon superficial del objeto actual.
Debido a la herencia implícita, puede llamar a cualquier miembro heredado de un objeto SimpleClass
igual que si realmente fuera un miembro definido en la clase SimpleClass
. Por ejemplo, en el ejemplo siguiente se llama al método SimpleClass.ToString
, que SimpleClass
hereda de Object.
public class EmptyClass
{ }
public class ClassNameExample
{
public static void Main()
{
EmptyClass sc = new();
Console.WriteLine(sc.ToString());
}
}
// The example displays the following output:
// EmptyClass
En la tabla siguiente se enumeran las categorías de tipos que puede crear en C# y los tipos de los que heredan implícitamente. Cada tipo base constituye un conjunto diferente de miembros disponible mediante herencia para los tipos derivados de forma implícita.
Categoría de tipo | Hereda implícitamente de |
---|---|
class | Object |
struct | ValueType, Object |
enum | Enum, ValueType, Object |
delegado | MulticastDelegate, Delegate, Object |
Herencia y una relación "is a"
Normalmente, la herencia se usa para expresar una relación "es una" entre una clase base y una o varias clases derivadas, donde las clases derivadas son versiones especializadas de la clase base; la clase derivada es un tipo de la clase base. Por ejemplo, la clase Publication
representa una publicación de cualquier tipo y las clases Book
y Magazine
representan tipos específicos de publicaciones.
Nota:
Una clase o estructura puede implementar una o varias interfaces. Aunque a menudo la implementación se presenta como una solución alternativa para la herencia única o como una forma de usar la herencia con structs, su finalidad es expresar una relación diferente (una relación "can do") entre una interfaz y su tipo de implementación que la herencia. Una interfaz define un subconjunto de funcionalidades (como la capacidad de verificar la igualdad, comparar o ordenar objetos, o admitir el análisis y formato sensibles a la cultura) que la interfaz pone a disposición de los tipos que la implementan.
Tenga en cuenta que "is a" también expresa la relación entre un tipo y una instancia específica de ese tipo. En el ejemplo siguiente, Automobile
es una clase que tiene tres propiedades únicas de solo lectura: Make
, el fabricante del automóvil; Model
, el tipo de automóvil; y Year
, su año de fabricación. La clase Automobile
también tiene un constructor cuyos argumentos se asignan a los valores de propiedad y invalida el método Object.ToString para generar una cadena que identifica de forma única la instancia de Automobile
en lugar de la clase Automobile
.
public class Automobile
{
public Automobile(string make, string model, int year)
{
if (make == null)
throw new ArgumentNullException(nameof(make), "The make cannot be null.");
else if (string.IsNullOrWhiteSpace(make))
throw new ArgumentException("make cannot be an empty string or have space characters only.");
Make = make;
if (model == null)
throw new ArgumentNullException(nameof(model), "The model cannot be null.");
else if (string.IsNullOrWhiteSpace(model))
throw new ArgumentException("model cannot be an empty string or have space characters only.");
Model = model;
if (year < 1857 || year > DateTime.Now.Year + 2)
throw new ArgumentException("The year is out of range.");
Year = year;
}
public string Make { get; }
public string Model { get; }
public int Year { get; }
public override string ToString() => $"{Year} {Make} {Model}";
}
En este caso, no debe confiar en la herencia para representar marcas y modelos de automóviles específicos. Por ejemplo, no es necesario definir un tipo de Packard
para representar automóviles fabricados por Packard Motor Car Company. En su lugar, puede representarlos creando un objeto Automobile
con los valores adecuados pasados a su constructor de clases, como hace el ejemplo siguiente.
using System;
public class Example
{
public static void Main()
{
var packard = new Automobile("Packard", "Custom Eight", 1948);
Console.WriteLine(packard);
}
}
// The example displays the following output:
// 1948 Packard Custom Eight
La relación is-a basada en la herencia se aplica mejor a una clase base y a clases derivadas que agregan miembros adicionales a la clase base o que requieren funcionalidad adicional no presente en la clase base.
Diseño de la clase base y las clases derivadas
Echemos un vistazo al proceso de diseño de una clase base y sus clases derivadas. En esta sección, definirá una clase base, Publication
, que representa una publicación de cualquier tipo, como un libro, una revista, un periódico, un diario, un artículo, etc. También definirá una clase Book
que deriva de Publication
. Puede ampliar fácilmente el ejemplo para definir otras clases derivadas, como Magazine
, Journal
, Newspaper
y Article
.
Clase base Publication
Al diseñar la clase Publication
, debe tomar varias decisiones de diseño:
Qué miembros incluir en la clase base
Publication
y si los miembros dePublication
proporcionan implementaciones de método o siPublication
es una clase base abstracta que actúa como plantilla para sus clases derivadas.En este caso, la clase
Publication
proporcionará implementaciones de método. El Diseño de clases base abstractas y sus clases derivadas contiene una sección con un ejemplo que usa una clase base abstracta para definir los métodos que las clases derivadas deben sobrescribir. Las clases derivadas son gratuitas para proporcionar cualquier implementación que sea adecuada para el tipo derivado.La capacidad de reutilizar código (es decir, varias clases derivadas comparten la declaración y la implementación de métodos de clase base y no es necesario invalidarlos) es una ventaja de las clases base no abstractas. Por tanto, se deben agregar miembros a
Publication
si es probable que algunos o la mayoría de los tiposPublication
especializados compartan su código. Si no proporciona implementaciones de clase base de forma eficaz, terminará teniendo que proporcionar implementaciones de miembros en gran medida idénticas en clases derivadas, en lugar de una sola implementación en la clase base. La necesidad de mantener el código duplicado en varias ubicaciones es un posible origen de errores.Tanto para maximizar la reutilización de código como para crear una jerarquía de herencia lógica e intuitiva, quiere asegurarse de incluir en la clase
Publication
solo los datos y la funcionalidad que es común a todas o a la mayoría de las publicaciones. A continuación, las clases derivadas implementan miembros que son únicos para los tipos concretos de publicación que representan.Hasta dónde ampliar la jerarquía de clases. ¿Desea desarrollar una jerarquía de tres o más clases, en lugar de simplemente una clase base y una o varias clases derivadas? Por ejemplo,
Publication
podría ser una clase base dePeriodical
, que a su vez es una clase base deMagazine
,Journal
yNewspaper
.En el ejemplo, usará la jerarquía pequeña de una clase
Publication
y una sola clase derivada,Book
. Puede ampliar fácilmente el ejemplo para crear una serie de clases adicionales que derivan dePublication
, comoMagazine
yArticle
.Si tiene sentido crear instancias de la clase base. Si no, se debe aplicar la palabra clave abstract a la clase. De lo contrario, la clase
Publication
puede instanciarse llamando a su constructor de clase. Si se intenta crear una instancia de una clase marcada con la palabra claveabstract
mediante una llamada directa a su constructor de clases, el compilador de C# genera el error CS0144, "No se puede crear una instancia de la clase o interfaz abstractas". Si se intenta crear una instancia de la clase mediante la reflexión, el método de reflexión produce un MemberAccessException.De forma predeterminada, se puede crear una instancia de una clase base llamando a su constructor de clases. No es necesario definir explícitamente un constructor de clase. Si uno no está presente en el código fuente de la clase base, el compilador de C# proporciona automáticamente un constructor predeterminado (sin parámetros).
En el ejemplo, la clase
Publication
se marcará como abstract para que no se puedan crear instancias de ella. Una claseabstract
sin ningún métodoabstract
indica que esta clase representa un concepto abstracto que se comparte entre varias clases concretas (como unBook
,Journal
).Si las clases derivadas deben heredar la implementación de clase base de miembros concretos, si tienen la opción de invalidar la implementación de la clase base o si deben proporcionar una implementación. Use la palabra clave abstract para forzar que las clases derivadas proporcionen una implementación. Use la palabra clave virtual para permitir que las clases derivadas invaliden un método de clase base. De forma predeterminada, no se pueden invalidar los métodos definidos en la clase base.
La clase
Publication
no tiene ningún métodoabstract
, pero la propia clase esabstract
.Si una clase derivada representa la clase final en la jerarquía de herencia y no se puede usar como clase base para clases derivadas adicionales. De forma predeterminada, cualquier clase puede servir como una clase base. Se puede aplicar la palabra clave sealed para indicar que una clase no puede servir como clase base para las clases adicionales. Al intentar derivar de una clase sellada, se generó el error del compilador CS0509, "no se puede derivar del tipo sellado <typeName>".
Para tu ejemplo, marcarás la clase que derivaste como
sealed
.
En el ejemplo siguiente se muestra el código fuente de la clase Publication
, así como una enumeración PublicationType
que devuelve la propiedad Publication.PublicationType
. Además de los miembros que hereda de Object, la clase Publication
define los siguientes miembros únicos e invalidaciones de miembros:
public enum PublicationType { Misc, Book, Magazine, Article };
public abstract class Publication
{
private bool _published = false;
private DateTime _datePublished;
private int _totalPages;
public Publication(string title, string publisher, PublicationType type)
{
if (string.IsNullOrWhiteSpace(publisher))
throw new ArgumentException("The publisher is required.");
Publisher = publisher;
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("The title is required.");
Title = title;
Type = type;
}
public string Publisher { get; }
public string Title { get; }
public PublicationType Type { get; }
public string? CopyrightName { get; private set; }
public int CopyrightDate { get; private set; }
public int Pages
{
get { return _totalPages; }
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
_totalPages = value;
}
}
public string GetPublicationDate()
{
if (!_published)
return "NYP";
else
return _datePublished.ToString("d");
}
public void Publish(DateTime datePublished)
{
_published = true;
_datePublished = datePublished;
}
public void Copyright(string copyrightName, int copyrightDate)
{
if (string.IsNullOrWhiteSpace(copyrightName))
throw new ArgumentException("The name of the copyright holder is required.");
CopyrightName = copyrightName;
int currentYear = DateTime.Now.Year;
if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
CopyrightDate = copyrightDate;
}
public override string ToString() => Title;
}
Un constructor
Dado que la clase
Publication
esabstract
, no se puede crear una instancia directamente desde código como en el ejemplo siguiente:var publication = new Publication("Tiddlywinks for Experts", "Fun and Games", PublicationType.Book);
Sin embargo, se puede llamar a su constructor de instancia directamente desde constructores de clase derivadas, como se muestra en el código fuente de la clase
Book
.Dos propiedades relacionadas con la publicación
Title
es una propiedad String de solo lectura cuyo valor se proporciona llamando al constructorPublication
.Pages
es una propiedad Int32 de lectura y escritura que indica cuántas páginas totales tiene la publicación. El valor se almacena en un campo privado denominadototalPages
. Debe ser un número positivo o se inicia una excepción ArgumentOutOfRangeException.Miembros relacionados con el publicador
Dos propiedades de solo lectura,
Publisher
yType
. Originalmente, la llamada al constructor de clasePublication
proporciona los valores.Miembros relacionados con la publicación
Dos métodos,
Publish
yGetPublicationDate
, establecen y devuelven la fecha de publicación. El métodoPublish
establece una marcapublished
privada entrue
cuando se llama y asigna la fecha pasada a él como argumento al campodatePublished
privado. El métodoGetPublicationDate
devuelve la cadena "NYP" si la marcapublished
esfalse
y el valor del campo dedatePublished
si estrue
.Miembros relacionados con los derechos de autor
El método
Copyright
toma el nombre del titular del derecho de autor y el año del derecho de autor como argumentos y los asigna a las propiedadesCopyrightName
yCopyrightDate
.Una invalidación del método
ToString
Si un tipo no invalida el método Object.ToString, devuelve el nombre completo del tipo, que es de poco uso para diferenciar una instancia de otra. La clase
Publication
invalida Object.ToString para devolver el valor de la propiedadTitle
.
En la ilustración siguiente se muestra la relación entre la clase base Publication
y su clase Object heredada implícitamente.
La clase Book
.
La clase Book
representa un libro como un tipo especializado de publicación. En el ejemplo siguiente se muestra el código fuente de la clase Book
.
using System;
public sealed class Book : Publication
{
public Book(string title, string author, string publisher) :
this(title, string.Empty, author, publisher)
{ }
public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
{
// isbn argument must be a 10- or 13-character numeric string without "-" characters.
// We could also determine whether the ISBN is valid by comparing its checksum digit
// with a computed checksum.
//
if (!string.IsNullOrEmpty(isbn))
{
// Determine if ISBN length is correct.
if (!(isbn.Length == 10 | isbn.Length == 13))
throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
if (!ulong.TryParse(isbn, out _))
throw new ArgumentException("The ISBN can consist of numeric characters only.");
}
ISBN = isbn;
Author = author;
}
public string ISBN { get; }
public string Author { get; }
public decimal Price { get; private set; }
// A three-digit ISO currency symbol.
public string? Currency { get; private set; }
// Returns the old price, and sets a new price.
public decimal SetPrice(decimal price, string currency)
{
if (price < 0)
throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
decimal oldValue = Price;
Price = price;
if (currency.Length != 3)
throw new ArgumentException("The ISO currency symbol is a 3-character string.");
Currency = currency;
return oldValue;
}
public override bool Equals(object? obj)
{
if (obj is not Book book)
return false;
else
return ISBN == book.ISBN;
}
public override int GetHashCode() => ISBN.GetHashCode();
public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}
Además de los miembros que hereda de Publication
, la clase Book
define los siguientes miembros únicos e invalidaciones de miembros:
Dos constructores
Los dos constructores
Book
comparten tres parámetros comunes. Dos, title y publisher, corresponden a los parámetros del constructorPublication
. La tercera es author, que se almacena para una propiedadAuthor
pública inmutable. Un constructor incluye un parámetro isbn, que se almacena en la propiedad automáticaISBN
.El primer constructor usa la palabra clave this para llamar al otro constructor. El encadenamiento de constructores es un patrón común en la definición de constructores. Los constructores con menos parámetros proporcionan valores predeterminados al llamar al constructor con el mayor número de parámetros.
El segundo constructor usa la palabra clave base para pasar el título y el nombre del publicador al constructor de clase base. Si no realiza una llamada explícita a un constructor de clase base en el código fuente, el compilador de C# proporciona automáticamente una llamada al constructor predeterminado o sin parámetros de la clase base.
Una propiedad
ISBN
de solo lectura, que devuelve el ISBN (International Standard Book Number) del objetoBook
, un número exclusivo de 10 y 13 caracteres. El ISBN se proporciona como argumento para uno de los constructoresBook
. El ISBN se almacena en un campo de respaldo privado, que el compilador genera automáticamente.Una propiedad
Author
de solo lectura. El nombre del autor se proporciona como argumento para ambos constructoresBook
y se almacena en la propiedad.Dos propiedades relacionadas con el precio de solo lectura,
Price
yCurrency
. Sus valores se proporcionan como argumentos en una llamada de métodoSetPrice
. La propiedadCurrency
es el símbolo de moneda ISO de tres dígitos (por ejemplo, USD para el dólar estadounidense). Los símbolos de moneda ISO se pueden recuperar de la propiedad ISOCurrencySymbol. Ambas propiedades son de solo lectura externa, pero ambas se pueden establecer mediante código en la claseBook
.Método
SetPrice
, que establece los valores de las propiedadesPrice
yCurrency
. Esas propiedades devuelven los mismos valores.Invalida el método
ToString
(heredado dePublication
) y los métodos Object.Equals(Object) y GetHashCode (heredados de Object).A menos que se invalide, el método Object.Equals(Object) comprueba la igualdad de referencia. Es decir, dos variables de objeto se consideran iguales si hacen referencia al mismo objeto. Por otro lado, en la clase
Book
, dos objetosBook
deben ser iguales si tienen el mismo ISBN.Al invalidar el método Object.Equals(Object), también debe invalidar el método GetHashCode, que devuelve un valor que el tiempo de ejecución usa para almacenar elementos en colecciones hash para una recuperación eficaz. El código hash debe devolver un valor coherente con la prueba de igualdad. Puesto que se ha invalidado Object.Equals(Object) para devolver
true
, si las propiedades de ISBN de dos objetosBook
son iguales, se devuelve el código hash calculado mediante la llamada al método GetHashCode de la cadena devuelta por la propiedadISBN
.
En la ilustración siguiente se muestra la relación entre la clase Book
y Publication
, su clase base.
Ahora puede crear una instancia de un objeto Book
, invocar sus miembros únicos y heredados y pasarlo como argumento a un método que espera un parámetro de tipo Publication
o de tipo Book
, como se muestra en el ejemplo siguiente.
public class ClassExample
{
public static void Main()
{
var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
"Public Domain Press");
ShowPublicationInfo(book);
book.Publish(new DateTime(2016, 8, 18));
ShowPublicationInfo(book);
var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
$"{((Publication)book).Equals(book2)}");
}
public static void ShowPublicationInfo(Publication pub)
{
string pubDate = pub.GetPublicationDate();
Console.WriteLine($"{pub.Title}, " +
$"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
}
}
// The example displays the following output:
// The Tempest, Not Yet Published by Public Domain Press
// The Tempest, published on 8/18/2016 by Public Domain Press
// The Tempest and The Tempest are the same publication: False
Diseño de clases base abstractas y sus clases derivadas
En el ejemplo anterior, definió una clase base que proporcionó una implementación de varios métodos para permitir que las clases derivadas compartan código. Sin embargo, en muchos casos, no se espera que la clase base proporcione una implementación. En su lugar, la clase base es una clase abstracta que declara métodos abstractos; sirve como una plantilla que define los miembros que debe implementar cada clase derivada. Normalmente, en una clase base abstracta, la implementación de cada tipo derivado es única para ese tipo. La clase se ha marcado con la palabra clave abstract porque no tenía mucho sentido crear instancias de un objeto Publication
, aunque la clase proporcionara las implementaciones de funcionalidad común a las publicaciones.
Por ejemplo, cada forma geométrica bidimensional cerrada incluye dos propiedades: área, la extensión interna de la forma; y perímetro, o la distancia a lo largo de los bordes de la forma. Sin embargo, la forma en que se calculan estas propiedades depende completamente de la forma específica. La fórmula para calcular el perímetro (o circunferencia) de un círculo, por ejemplo, es diferente del de un cuadrado. La clase Shape
es una clase abstract
con métodos abstract
. Esto indica que las clases derivadas comparten la misma funcionalidad, pero esas clases derivadas implementan esa funcionalidad de forma diferente.
En el ejemplo siguiente se define una clase base abstracta denominada Shape
que define dos propiedades: Area
y Perimeter
. Además de marcar la clase con la palabra clave abstracta , cada miembro de instancia también se marca con la palabra clave abstracta . En este caso, Shape
también invalida el método Object.ToString para devolver el nombre del tipo, en lugar de su nombre completo. Y define dos miembros estáticos, GetArea
y GetPerimeter
, que permiten a los llamadores recuperar fácilmente el área y el perímetro de una instancia de cualquier clase derivada. Cuando se pasa una instancia de una clase derivada a cualquiera de estos métodos, el runtime llama a la invalidación del método de la clase derivada.
public abstract class Shape
{
public abstract double Area { get; }
public abstract double Perimeter { get; }
public override string ToString() => GetType().Name;
public static double GetArea(Shape shape) => shape.Area;
public static double GetPerimeter(Shape shape) => shape.Perimeter;
}
A continuación, puede derivar algunas clases de Shape
que representan formas específicas. En el ejemplo siguiente se definen tres clases, Square
, Rectangle
y Circle
. Cada uno usa una fórmula única para esa forma concreta para calcular el área y el perímetro. Algunas de las clases derivadas también definen propiedades, como Rectangle.Diagonal
y Circle.Diameter
, que son exclusivas de la forma que representan.
using System;
public class Square : Shape
{
public Square(double length)
{
Side = length;
}
public double Side { get; }
public override double Area => Math.Pow(Side, 2);
public override double Perimeter => Side * 4;
public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
}
public class Rectangle : Shape
{
public Rectangle(double length, double width)
{
Length = length;
Width = width;
}
public double Length { get; }
public double Width { get; }
public override double Area => Length * Width;
public override double Perimeter => 2 * Length + 2 * Width;
public bool IsSquare() => Length == Width;
public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
}
public class Circle : Shape
{
public Circle(double radius)
{
Radius = radius;
}
public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);
public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);
// Define a circumference, since it's the more familiar term.
public double Circumference => Perimeter;
public double Radius { get; }
public double Diameter => Radius * 2;
}
En el ejemplo siguiente se usan objetos derivados de Shape
. Se crea una instancia de una matriz de objetos derivados de Shape
y se llama a los métodos estáticos de la clase Shape
, que ajusta los valores de propiedad Shape
devueltos. El runtime recupera los valores de las propiedades invalidadas de los tipos derivados. En el ejemplo también se convierte cada objeto Shape
de la matriz en su tipo derivado y, si la conversión se realiza correctamente, recupera las propiedades de esa subclase concreta de Shape
.
using System;
public class Example
{
public static void Main()
{
Shape[] shapes = { new Rectangle(10, 12), new Square(5),
new Circle(3) };
foreach (Shape shape in shapes)
{
Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
$"perimeter, {Shape.GetPerimeter(shape)}");
if (shape is Rectangle rect)
{
Console.WriteLine($" Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
continue;
}
if (shape is Square sq)
{
Console.WriteLine($" Diagonal: {sq.Diagonal}");
continue;
}
}
}
}
// The example displays the following output:
// Rectangle: area, 120; perimeter, 44
// Is Square: False, Diagonal: 15.62
// Square: area, 25; perimeter, 20
// Diagonal: 7.07
// Circle: area, 28.27; perimeter, 18.85