Partilhar via


Herança em C# e .NET

Este tutorial introduz você à herança em C#. A herança é um recurso de linguagens de programação orientadas a objetos que permite definir uma classe base que fornece funcionalidade específica (dados e comportamento) e definir classes derivadas que herdam ou substituem essa funcionalidade.

Pré-requisitos

Instruções de instalação

No Windows, este arquivo de configuração WinGet para instalar todos os pré-requisitos. Se você já tiver algo instalado, o WinGet ignorará essa etapa.

  1. Baixe o arquivo e clique duas vezes para executá-lo.
  2. Leia o contrato de licença, digite ye selecione Enter quando solicitado a aceitar.
  3. Se receber um aviso de Controle de Conta de Utilizador (UAC) intermitente na Barra de Tarefas, permita que a instalação continue.

Em outras plataformas, você precisa instalar cada um desses componentes separadamente.

  1. Baixe o instalador recomendado na página de download do .NET SDK e clique duas vezes para executá-lo. A página de download deteta sua plataforma e recomenda o instalador mais recente para sua plataforma.
  2. Baixe o instalador mais recente da página inicial do Visual Studio Code e clique duas vezes para executá-lo. Essa página também deteta a sua plataforma e o link deve estar correto para o seu sistema.
  3. Clique no botão "Instalar" na página de extensão C# DevKit. Isso abre o código do Visual Studio e pergunta se você deseja instalar ou habilitar a extensão. Selecione "instalar".

Executando os exemplos

Para criar e executar os exemplos neste tutorial, use o utilitário dotnet da linha de comando. Siga estas etapas para cada exemplo:

  1. Crie um diretório para armazenar o exemplo.

  2. Digite o comando dotnet new console em um prompt de comando para criar um novo projeto .NET Core.

  3. Copie e cole o código do exemplo no editor de códigos.

  4. Digite o comando dotnet restore na linha de comando para carregar ou restaurar as dependências do projeto.

    Não é necessário executádotnet restore porque ele é executado implicitamente por todos os comandos que exigem uma restauração para ocorrer, como dotnet new, dotnet build, dotnet run, dotnet test, dotnet publishe dotnet pack. Para desativar a restauração implícita, use a opção --no-restore.

    O comando dotnet restore ainda é útil em determinados cenários em que a restauração explícita faz sentido, como compilações de integração contínua no de Serviços de DevOps do Azure ou em sistemas de compilação que precisam controlar explicitamente quando a restauração ocorre.

    Para obter informações sobre como gerenciar feeds NuGet, consulte a documentação dotnet restore.

  5. Digite o comando dotnet run para compilar e executar o exemplo.

Antecedentes: O que é herança?

Herança é um dos atributos fundamentais da programação orientada a objetos. Permite definir uma classe filha que reutiliza (herda), estende ou modifica o comportamento de uma classe mãe. A classe cujos membros são herdados é chamada de classe base . A classe que herda os membros da classe base é chamada de classe derivada .

C# e .NET suportam apenas herança única. Ou seja, uma classe só pode herdar de uma única classe. No entanto, a herança é transitiva, o que permite definir uma hierarquia de herança para um conjunto de tipos. Em outras palavras, o tipo D pode herdar do tipo C, que herda do tipo B, que herda do tipo de classe base A. Como a herança é transitiva, os membros do tipo A estão disponíveis para o tipo D.

Nem todos os membros de uma classe base são herdados por classes derivadas. Os seguintes membros não são herdados:

  • Construtores estáticos, que inicializam os dados estáticos de uma classe.

  • Construtores de instância, que você chama para criar uma nova instância da classe. Cada classe deve definir os seus próprios construtores.

  • Finalizadores, que são chamados pelo coletor de lixo do tempo de execução para destruir as instâncias de uma classe.

Enquanto todos os outros membros de uma classe base são herdados por classes derivadas, se eles são visíveis ou não depende de sua acessibilidade. A acessibilidade de um membro afeta sua visibilidade para classes derivadas da seguinte maneira:

  • membros do Private são visíveis apenas em classes derivadas aninhadas em sua classe base. Caso contrário, eles não são visíveis em classes derivadas. No exemplo a seguir, A.B é uma classe aninhada que deriva de Ae C deriva de A. O campo A._value privado é visível em A.B. No entanto, se você remover os comentários do método C.GetValue e tentar compilar o exemplo, ele produzirá o erro de compilador CS0122: "'A._value' está inacessível devido ao seu nível de proteção."

    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
    
  • Membros protegidos são visíveis apenas em classes derivadas.

  • Membros internos são visíveis apenas em classes derivadas que estão no mesmo assembly que a classe base. Eles não são visíveis em classes derivadas localizadas em um assembly diferente da classe base.

  • Membros do Public são visíveis em classes derivadas e fazem parte da interface pública da classe derivada. Os membros públicos herdados podem ser chamados como se estivessem definidos na classe derivada. No exemplo a seguir, a classe A define um método chamado Method1e a classe B herda da classe A. O exemplo então chama Method1 como se fosse um método de instância em 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();
        }
    }
    

As classes derivadas também podem substituir membros herdados fornecendo uma implementação alternativa. Para poder substituir um membro, o membro da classe base deve ser marcado com a palavra-chave virtual. Por padrão, os membros da classe base não são marcados como virtual e não podem ser substituídos. A tentativa de sobrepor um membro não virtual, como no exemplo a seguir, gera o erro CS0506 do compilador: "<membro> não pode sobrepor o membro herdado <membro> porque não está marcado como virtual, abstract ou override."

public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

Em alguns casos, uma classe derivada deve substituir a implementação da classe base. Os membros da classe base marcados com a palavra-chave abstrata exigem que as classes derivadas os substituam. Tentar compilar o exemplo a seguir gera erro de compilador CS0534, "<classe> não implementa membro abstrato herdado <membro>", porque a classe B não fornece nenhuma implementação para A.Method1.

public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

A herança aplica-se apenas a classes e interfaces. Outras categorias de tipo (structs, delegates e enums) não suportam herança. Devido a essas regras, tentar compilar código como o exemplo a seguir produz erro de compilador CS0527: "Tipo 'ValueType' na lista de interface não é uma interface." A mensagem de erro indica que, embora você possa definir as interfaces que um struct implementa, a herança não é suportada.

public struct ValueStructure : ValueType // Generates CS0527.
{
}

Herança implícita

Além de quaisquer tipos que eles possam herdar através de herança única, todos os tipos no sistema de tipos .NET herdam implicitamente de Object ou um tipo derivado dele. A funcionalidade comum do Object está disponível para qualquer tipo.

Para ver o que significa herança implícita, vamos definir uma nova classe, SimpleClass, que é simplesmente uma definição de classe vazia:

public class SimpleClass
{ }

Em seguida, você pode usar a reflexão (que permite inspecionar os metadados de um tipo para obter informações sobre esse tipo) para obter uma lista dos membros que pertencem ao tipo SimpleClass. Embora você não tenha definido nenhum membro em sua classe SimpleClass, a saída do exemplo indica que, na verdade, ela tem nove membros. Um desses membros é um construtor sem parâmetros (ou padrão) que é fornecido automaticamente para o tipo SimpleClass pelo compilador C#. Os oito restantes são membros de Object, o tipo do qual, em última instância, todas as classes e interfaces no sistema de tipos .NET herdam implicitamente.

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

A herança implícita da classe Object torna esses métodos disponíveis para a classe SimpleClass:

  • O método ToString público, que converte um objeto SimpleClass em sua representação de cadeia de caracteres, retorna o nome de tipo totalmente qualificado. Nesse caso, o método ToString retorna a cadeia de caracteres "SimpleClass".

  • Três métodos que testam a igualdade de dois objetos: o método público de instância Equals(Object), o método público estático Equals(Object, Object) e o método público estático ReferenceEquals(Object, Object). Por padrão, esses métodos testam a igualdade de referência; ou seja, para serem iguais, duas variáveis de objeto devem referir-se ao mesmo objeto.

  • O método GetHashCode público, que calcula um valor que permite que uma instância do tipo seja usada em coleções com hash.

  • O método public GetType, que retorna um objeto Type que representa o tipo SimpleClass.

  • O método Finalize protegido, que é projetado para liberar recursos não gerenciados antes que a memória de um objeto seja recuperada pelo coletor de lixo.

  • O método MemberwiseClone, protegido, que cria um clone superficial do objeto atual.

Devido à herança implícita, você pode chamar qualquer membro herdado de um objeto SimpleClass como se fosse realmente um membro definido na classe SimpleClass. Por exemplo, o exemplo a seguir chama o método SimpleClass.ToString, que SimpleClass herda 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

A tabela a seguir lista as categorias de tipos que você pode criar em C# e os tipos dos quais eles herdam implicitamente. Cada tipo de base torna disponível um conjunto variado de membros por meio de herança para tipos que são implicitamente derivados.

Categoria de tipo Herda implicitamente de
classe Object
estrutura ValueType, Object
enum Enum, ValueType, Object
delegado MulticastDelegate, Delegate, Object

Herança e uma relação "é um"

Normalmente, a herança é usada para expressar uma relação "é um" entre uma classe base e uma ou mais classes derivadas, onde as classes derivadas são versões especializadas da classe base; a classe derivada é um tipo da classe base. Por exemplo, a classe Publication representa uma publicação de qualquer tipo, e as classes Book e Magazine representam tipos específicos de publicações.

Observação

Uma classe ou struct pode implementar uma ou mais interfaces. Embora a implementação de interface seja frequentemente apresentada como uma solução alternativa para herança única ou como uma forma de usar herança com estruturas, destina-se a expressar uma relação diferente (uma relação "pode fazer") entre uma interface e o tipo que a implementa, em vez de uma relação de herança. Uma interface define um subconjunto de funcionalidades (como a capacidade de testar a igualdade, comparar ou classificar objetos ou oferecer suporte à análise e formatação sensíveis à cultura) que a interface disponibiliza para seus tipos de implementação.

Observe que "is a" também expressa a relação entre um tipo e uma instanciação específica desse tipo. No exemplo a seguir, Automobile é uma classe que tem três propriedades somente leitura exclusivas: Make, o fabricante do automóvel; Model, o tipo de automóvel; e Year, o seu ano de fabrico. Sua classe Automobile também tem um construtor cujos argumentos são atribuídos aos valores de propriedade e substitui o método Object.ToString para produzir uma cadeia de caracteres que identifica exclusivamente a instância Automobile em vez da classe 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}";
}

Neste caso, você não deve confiar na herança para representar marcas e modelos de carros específicos. Por exemplo, você não precisa definir um tipo de Packard para representar automóveis fabricados pela Packard Motor Car Company. Em vez disso, você pode representá-los criando um objeto Automobile com os valores apropriados passados para seu construtor de classe, como faz o exemplo a seguir.

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

Uma relação is-a baseada em herança é melhor aplicada a uma classe base e a classes derivadas que adicionam membros adicionais à classe base ou que exigem funcionalidade adicional não presente na classe base.

Projetando a classe base e as classes derivadas

Vejamos o processo de criação de uma classe base e suas classes derivadas. Nesta seção, você definirá uma classe base, Publication, que representa uma publicação de qualquer tipo, como um livro, uma revista, um jornal, uma revista, um artigo, etc. Você também definirá uma classe Book que deriva de Publication. Você pode facilmente estender o exemplo para definir outras classes derivadas, como Magazine, Journal, Newspapere Article.

A classe base Publicação

Ao projetar sua classe Publication, você precisa tomar várias decisões de design:

  • Quais membros incluir em sua classe base Publication e se os membros Publication fornecem implementações de método ou se Publication é uma classe base abstrata que serve como modelo para suas classes derivadas.

    Nesse caso, a classe Publication fornecerá implementações de método. A seção Designing abstract base classes and their derived classes contém um exemplo que usa uma classe base abstrata para definir os métodos que as classes derivadas devem substituir. As classes derivadas são livres para fornecer qualquer implementação que seja adequada para o tipo derivado.

    A capacidade de reutilizar código (ou seja, várias classes derivadas compartilham a declaração e a implementação de métodos de classe base e não precisam substituí-los) é uma vantagem das classes base não abstratas. Portanto, deve-se adicionar membros a Publication se for provável que o seu código seja partilhado por alguns ou a maioria dos tipos de Publication especializados. Se você não conseguir fornecer implementações de classe base de forma eficiente, acabará tendo que fornecer implementações de membro em grande parte idênticas em classes derivadas em vez de uma única implementação na classe base. A necessidade de manter o código duplicado em vários locais é uma fonte potencial de bugs.

    Para maximizar a reutilização de código e criar uma hierarquia de herança lógica e intuitiva, você quer ter certeza de incluir na classe Publication apenas os dados e a funcionalidade comuns a todas ou à maioria das publicações. Em seguida, as classes derivadas implementam membros que são exclusivos para os tipos específicos de publicação que representam.

  • Até onde estender sua hierarquia de classe. Você deseja desenvolver uma hierarquia de três ou mais classes, em vez de simplesmente uma classe base e uma ou mais classes derivadas? Por exemplo, Publication poderia ser uma classe base de Periodical, que por sua vez é uma classe base de Magazine, Journal e Newspaper.

    Para seu exemplo, você usará a pequena hierarquia de uma classe Publication e uma única classe derivada, Book. Você pode facilmente estender o exemplo para criar várias classes adicionais que derivam de Publication, como Magazine e Article.

  • Se faz sentido instanciar a classe base. Se isso não acontecer, você deve aplicar a palavra-chave abstrato à classe. Caso contrário, sua classe Publication pode ser instanciada chamando seu construtor de classe. Se for feita uma tentativa de instanciar uma classe marcada com a palavra-chave abstract por uma chamada direta para seu construtor de classe, o compilador C# gerará o erro CS0144, "Não é possível criar uma instância da classe abstrata ou interface." Se for feita uma tentativa de instanciar a classe usando reflexão, o método de reflexão lançará um MemberAccessException.

    Por padrão, uma classe base pode ser instanciada chamando seu construtor de classe. Não é necessário definir explicitamente um construtor de classe. Se um não estiver presente no código-fonte da classe base, o compilador C# fornece automaticamente um construtor padrão (sem parâmetros).

    Para o seu exemplo, você marcará a classe Publication como abstrato para que ela não possa ser instanciada. Uma classe abstract sem nenhum método abstract indica que essa classe representa um conceito abstrato que é compartilhado entre várias classes concretas (como um Book, Journal).

  • Se as classes derivadas devem herdar a implementação da classe base de membros específicos, se eles têm a opção de substituir a implementação da classe base ou se devem fornecer uma implementação. Você usa a palavra-chave abstrato para obrigar classes derivadas a fornecerem uma implementação. Use a palavra-chave virtual para permitir que classes derivadas substituam um método de classe base. Por padrão, os métodos definidos na classe base não são substituíveis.

    A classe Publication não tem nenhum método abstract, mas a classe em si é abstract.

  • Se uma classe derivada representa a classe final na hierarquia de herança e não pode ser usada como uma classe base para classes derivadas adicionais. Por padrão, qualquer classe pode servir como uma classe base. Você pode aplicar a palavra-chave selada para indicar que uma classe não pode ser usada como classe base para outras classes. Tentando derivar de uma classe selada gerou erro de compilador CS0509, "não pode derivar de tipo selado <typeName>."

    Para o seu exemplo, você marcará sua classe derivada como sealed.

O exemplo a seguir mostra o código-fonte para a classe Publication, bem como uma enumeração PublicationType que é retornada pela propriedade Publication.PublicationType. Além dos membros que herda de Object, a classe Publication define os seguintes membros únicos e substituições de membros:


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;
}
  • Um construtor

    Como a classe Publication é abstract, ela não pode ser instanciada diretamente do código como o exemplo a seguir:

    var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
                                      PublicationType.Book);
    

    No entanto, seu construtor de instância pode ser chamado diretamente de construtores de classe derivados, como mostra o código-fonte da classe Book.

  • Duas propriedades relacionadas à publicação

    Title é uma propriedade String de somente leitura e cujo valor é determinado ao chamar o construtor Publication.

    Pages é uma propriedade de leitura-escrita Int32 que indica a quantidade total de páginas que tem a publicação. O valor é armazenado em um campo privado chamado totalPages. Deve ser um número positivo ou um ArgumentOutOfRangeException é lançado.

  • Membros relacionados com o editor

    Duas propriedades somente leitura, Publisher e Type. Os valores são originalmente fornecidos através da chamada para o construtor da classe Publication.

  • Membros relacionados com a publicação

    Dois métodos, Publish e GetPublicationDate, definem e retornam a data de publicação. O método Publish define um sinalizador privado published para true quando é chamado e atribui a data passada como argumento ao campo privado datePublished. O método GetPublicationDate retorna a cadeia de caracteres "NYP" se o sinalizador published for false, e o valor do campo datePublished se for true.

  • Membros relacionados com direitos de autor

    O método Copyright toma o nome do detentor dos direitos autorais e o ano dos direitos autorais como argumentos e os atribui às propriedades CopyrightName e CopyrightDate.

  • Uma substituição do método ToString

    Se um tipo não substituir o método Object.ToString, ele retornará o nome totalmente qualificado do tipo, o que é de pouca utilidade para diferenciar uma instância de outra. A classe Publication substitui Object.ToString para retornar o valor da propriedade Title.

A figura a seguir ilustra a relação entre sua classe base Publication e sua classe Object implicitamente herdada.

As classes Objeto e Publicação

A classe Book

A classe Book representa um livro como um tipo especializado de publicação. O exemplo a seguir mostra o código-fonte da classe 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}";
}

Além dos membros que herda de Publication, a classe Book define os seguintes membros únicos e substituições de membros:

  • Dois construtores

    Os dois construtores Book compartilham três parâmetros comuns. Dois, título e editor, correspondem a parâmetros do construtor Publication. O terceiro é autor, que é armazenado em uma propriedade pública imutável Author. Um construtor inclui um parâmetro isbn, que é armazenado na ISBN auto-propriedade.

    O primeiro construtor usa a palavra-chave this para chamar o outro construtor. Encadeamento de construtores é um padrão comum na definição de construtores. Construtores com menos parâmetros fornecem valores padrão ao chamar o construtor com o maior número de parâmetros.

    O segundo construtor usa a palavra-chave base para passar o título e o nome do editor para o construtor da classe base. Se você não fizer uma chamada explícita para um construtor de classe base em seu código-fonte, o compilador C# fornecerá automaticamente uma chamada para o construtor padrão ou sem parâmetros da classe base.

  • Uma propriedade ISBN de leitura apenas, que retorna o Número Internacional Normalizado para Livros do objeto Book, um número único de 10 ou 13 dígitos. O ISBN é fornecido como um argumento para um dos construtores Book. O ISBN é armazenado em um campo de suporte privado, que é gerado automaticamente pelo compilador.

  • Propriedade somente leitura Author. O nome do autor é fornecido como um argumento tanto para ambos os construtores Book como é armazenado no atributo.

  • Duas propriedades de leitura apenas relacionadas a preços, Price e Currency. Seus valores são fornecidos como argumentos em uma chamada de método SetPrice. A propriedade Currency é o símbolo de moeda ISO de três dígitos (por exemplo, USD para o dólar americano). Os símbolos de moeda ISO podem ser recuperados da propriedade ISOCurrencySymbol. Ambas as propriedades são de leitura apenas externamente, mas podem ser definidas por código na classe Book.

  • Um método SetPrice, que define os valores das propriedades Price e Currency. Esses valores são retornados por essas mesmas propriedades.

  • Substitui o método ToString (herdado de Publication) e os métodos Object.Equals(Object) e GetHashCode (herdados de Object).

    A menos que seja substituído, o método Object.Equals(Object) testa a igualdade de referência. Ou seja, duas variáveis de objeto são consideradas iguais se se referirem ao mesmo objeto. Na classe Book, por outro lado, dois objetos Book devem ser iguais se tiverem o mesmo ISBN.

    Ao substituir o método Object.Equals(Object), você também deve substituir o método GetHashCode, que retorna um valor que o tempo de execução usa para armazenar itens em coleções com hash para recuperação eficiente. O código hash deve retornar um valor consistente com o teste de igualdade. Como você substituiu Object.Equals(Object) para retornar true se as propriedades ISBN de dois objetos Book forem iguais, você retorna o código hash calculado ao chamar o método GetHashCode da cadeia de caracteres retornada pela propriedade ISBN.

A figura a seguir ilustra a relação entre a classe Book e Publication, sua classe base.

Categorias de Publicação e Livro

Agora você pode instanciar um objeto Book, invocar seus membros exclusivos e herdados e passá-lo como um argumento para um método que espera um parâmetro do tipo Publication ou do tipo Book, como mostra o exemplo a seguir.

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

Projetando classes base abstratas e suas classes derivadas

No exemplo anterior, você definiu uma classe base que forneceu uma implementação para vários métodos para permitir que classes derivadas compartilhem código. Em muitos casos, no entanto, não se espera que a classe base forneça uma implementação. Em vez disso, a classe base é uma classe abstrata que declara métodos abstratos; Ele serve como um modelo que define os membros que cada classe derivada deve implementar. Normalmente, em uma classe base abstrata, a implementação de cada tipo derivado é exclusiva para esse tipo. Você marcou a classe com a palavra-chave abstract porque não fazia sentido instanciar um objeto Publication, embora a classe fornecesse implementações de funcionalidade comuns a publicações.

Por exemplo, cada forma geométrica bidimensional fechada inclui duas propriedades: área, a extensão interna da forma; e perímetro, ou a distância ao longo das bordas da forma. A forma como essas propriedades são calculadas, no entanto, depende completamente da forma específica. A fórmula para calcular o perímetro (ou circunferência) de um círculo, por exemplo, é diferente da de um quadrado. A classe Shape é uma classe abstract com métodos abstract. Isso indica que as classes derivadas compartilham a mesma funcionalidade, mas essas classes derivadas implementam essa funcionalidade de forma diferente.

O exemplo a seguir define uma classe base abstrata chamada Shape que define duas propriedades: Area e Perimeter. Além de marcar a classe com a palavra-chave abstrato, cada membro da instância também é marcado com a palavra-chave abstrato. Nesse caso, Shape também substitui o método Object.ToString para retornar o nome do tipo, em vez de seu nome totalmente qualificado. E define dois membros estáticos, GetArea e GetPerimeter, que permitem que os chamadores recuperem facilmente a área e o perímetro de uma instância de qualquer classe derivada. Quando você passa uma instância de uma classe derivada para qualquer um desses métodos, o tempo de execução chama a substituição de método da classe 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;
}

Em seguida, você pode derivar algumas classes de Shape que representam formas específicas. O exemplo a seguir define três classes, Square, Rectanglee Circle. Cada um usa uma fórmula exclusiva para essa forma específica para calcular a área e o perímetro. Algumas das classes derivadas também definem propriedades, como Rectangle.Diagonal e Circle.Diameter, que são exclusivas para a forma que representam.

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;
}

O exemplo a seguir usa objetos derivados de Shape. Ele instancia uma matriz de objetos derivados de Shape e chama os métodos estáticos da classe Shape, que encapsulam os valores das propriedades de retorno Shape. O tempo de execução recupera valores das propriedades sobrepostas dos tipos derivados. O exemplo também converte cada objeto Shape na matriz para seu tipo derivado e, se a conversão for bem-sucedida, recupera propriedades dessa subclasse específica 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