Compartir a través de


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

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.

  1. Descargue el archivo y haga doble clic para ejecutarlo.
  2. Lea el contrato de licencia, escriba yy seleccione Escriba cuando se le pida que acepte.
  3. 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.

  1. 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.
  2. 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.
  3. 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:

  1. Cree un directorio para almacenar el ejemplo.

  2. Escriba el comando dotnet new console en el símbolo del sistema para crear un nuevo proyecto de .NET Core.

  3. Copie y pegue el código del ejemplo en el editor de código.

  4. 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 como dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish y dotnet 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.

  5. 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 de Ay C deriva de A. El campo A._value privado está visible en A.B. Sin embargo, si quita los comentarios del método C.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 denominado Method1y la clase B hereda de la clase A. A continuación, el ejemplo llama a Method1 como si fuera un método de instancia en B.

    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 objeto SimpleClass en su representación de cadena, devuelve el nombre de tipo completo. En este caso, el método ToString 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úblico Equals(Object, Object) y el método estático público ReferenceEquals(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 tipo SimpleClass.

  • 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, Newspapery 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 de Publication proporcionan implementaciones de método o si Publication 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 tipos Publication 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 de Periodical, que a su vez es una clase base de Magazine, Journal y Newspaper.

    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 de Publication, como Magazine y Article.

  • 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 clave abstract 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 clase abstract sin ningún método abstract indica que esta clase representa un concepto abstracto que se comparte entre varias clases concretas (como un Book, 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étodo abstract, pero la propia clase es abstract.

  • 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 es abstract, 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 constructor Publication.

    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 denominado totalPages. Debe ser un número positivo o se inicia una excepción ArgumentOutOfRangeException.

  • Miembros relacionados con el publicador

    Dos propiedades de solo lectura, Publisher y Type. Originalmente, la llamada al constructor de clase Publication proporciona los valores.

  • Miembros relacionados con la publicación

    Dos métodos, Publish y GetPublicationDate, establecen y devuelven la fecha de publicación. El método Publish establece una marca published privada en true cuando se llama y asigna la fecha pasada a él como argumento al campo datePublished privado. El método GetPublicationDate devuelve la cadena "NYP" si la marca published es falsey el valor del campo de datePublished si es true.

  • 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 propiedades CopyrightName y CopyrightDate.

  • 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 propiedad Title.

En la ilustración siguiente se muestra la relación entre la clase base Publication y su clase Object heredada implícitamente.

Las clases de objeto y publicación

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 constructor Publication. La tercera es author, que se almacena para una propiedad Author pública inmutable. Un constructor incluye un parámetro isbn, que se almacena en la propiedad automática ISBN.

    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 objeto Book, un número exclusivo de 10 y 13 caracteres. El ISBN se proporciona como argumento para uno de los constructores Book. 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 constructores Book y se almacena en la propiedad.

  • Dos propiedades relacionadas con el precio de solo lectura, Price y Currency. Sus valores se proporcionan como argumentos en una llamada de método SetPrice. La propiedad Currency 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 clase Book.

  • Método SetPrice, que establece los valores de las propiedades Price y Currency. Esas propiedades devuelven los mismos valores.

  • Invalida el método ToString (heredado de Publication) 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 objetos Book 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 objetos Book son iguales, se devuelve el código hash calculado mediante la llamada al método GetHashCode de la cadena devuelta por la propiedad ISBN.

En la ilustración siguiente se muestra la relación entre la clase Book y Publication, su clase base.

Clases de libro y publicación

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, Rectangley 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