Partilhar via


Herança em C# e .NET

Este tutorial apresenta a 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

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.

    Você não precisa executar 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 --no-restore opção.

    O dotnet restore comando ainda é útil em determinados cenários em que a restauração explícita faz sentido, como compilações de integração contínua nos 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 dotnet restore documentação.

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

Antecedentes: O que é herança?

A herança é um dos atributos fundamentais da programação orientada a objetos. Ele permite que você defina uma classe filho que reutiliza (herda), estende ou modifica o comportamento de uma classe pai. 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 Ade classe base. Como a herança é transitiva, os membros do tipo A estão disponíveis para digitar 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 seus próprios construtores.

  • Finalizadores, que são chamados pelo coletor de lixo do tempo de execução para destruir 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:

  • Os membros privados 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 A, e C deriva de A. O campo privado A._value é visível em A.B. No entanto, se você remover os comentários do C.GetValue método e tentar compilar o exemplo, ele produz 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
    
  • Os membros protegidos são visíveis apenas em classes derivadas.

  • Os membros internos são visíveis apenas em classes derivadas que estão localizadas 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.

  • Os membros públicos 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, class A define um método chamado Method1, e class 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 na 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 substituir um membro não virtual, como faz o exemplo a seguir, gera o erro do compilador CS0506: "<member> cannot override member <> member porque ele não está marcado como virtual, abstrato 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, "<class> does not implement inherited abstract member <member>", porque class 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 tipo .NET herdam implicitamente ou Object 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, SimpleClassque é 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 SimpleClass tipo. Embora você não tenha definido nenhum membro em sua SimpleClass classe, a saída do exemplo indica que ele realmente tem nove membros. Um desses membros é um construtor sem parâmetros (ou padrão) que é fornecido automaticamente para o SimpleClass tipo pelo compilador C#. Os oito restantes são membros do Object, o tipo do qual todas as classes e interfaces no sistema de tipo .NET finalmente 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 Object classe torna estes métodos disponíveis para a SimpleClass classe:

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

  • Três métodos que testam a igualdade de dois objetos: o método de instância Equals(Object) pública, o método estático Equals(Object, Object) público e o método estático ReferenceEquals(Object, Object) público. 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 public GetHashCode , 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 Type objeto que representa o SimpleClass tipo.

  • O método protegido Finalize , 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 protegido MemberwiseClone , que cria um clone superficial do objeto atual.

Devido à herança implícita, você pode chamar qualquer membro herdado de um SimpleClass objeto como se fosse realmente um membro definido na SimpleClass classe. Por exemplo, o exemplo a seguir chama o SimpleClass.ToString método, 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 disponibiliza um conjunto diferente de membros através de herança para tipos implicitamente derivados.

Categoria de tipo Herda implicitamente de
classe Object
estruturar ValueType, Object
enumeração 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 Publication classe representa uma publicação de qualquer tipo, e as Book classes e Magazine representam tipos específicos de publicações.

Nota

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 seu tipo de implementação do que a 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, seu ano de fabricação. Sua Automobile classe também tem um construtor cujos argumentos são atribuídos aos valores de propriedade e substitui o Object.ToString método para produzir uma cadeia de caracteres que identifica exclusivamente a Automobile instância em vez da Automobile classe.

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 Packard tipo para representar automóveis fabricados pela Packard Motor Car Company. Em vez disso, você pode representá-los criando um Automobile objeto 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, Publicationque representa uma publicação de qualquer tipo, como um livro, uma revista, um jornal, uma revista, um artigo, etc. Você também definirá uma Book classe que deriva de Publication. Você pode facilmente estender o exemplo para definir outras classes derivadas, como Magazine, Journal, Newspapere Article.

A classe base Publication

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

  • Quais membros incluir em sua classe base Publication e se os Publication membros 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 fornecerá implementações de Publication 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, você deve adicionar membros se Publication é provável que seu código seja compartilhado por alguns ou mais tipos especializados Publication . 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 Publication classe apenas os dados e a funcionalidade que são 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 o seu exemplo, você usará a pequena hierarquia de uma Publication classe e uma única classe derivada, Book. Você pode facilmente estender o exemplo para criar várias classes adicionais derivadas de Publication, como Magazine e Article.

  • Se faz sentido instanciar a classe base. Se isso não acontecer, você deve aplicar a palavra-chave abstrata à classe. Caso contrário, sua Publication classe pode ser instanciada chamando seu construtor de classe. Se for feita uma tentativa de instanciar uma classe marcada com a abstract palavra-chave 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 MemberAccessExceptionarquivo .

    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 Publication classe como abstrata para que ela não possa ser instanciada. Uma abstract classe sem nenhum abstract método 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 abstract para forçar classes derivadas a fornecer 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 Publication classe não tem nenhum abstract método, 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 sealed para indicar que uma classe não pode servir como uma classe base para quaisquer classes adicionais. Tentando derivar de uma classe selada gerado 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 da Publication classe, bem como uma PublicationType enumeração que é retornada Publication.PublicationType pela propriedade. Além dos membros dos quais herda do , a Publication classe define os seguintes membros exclusivos e substituições de Objectmembros:


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 Publication classe é 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 Book classe.

  • Duas propriedades relacionadas à publicação

    Title é uma propriedade somente String leitura cujo valor é fornecido chamando o Publication construtor.

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

  • Membros relacionados ao editor

    Duas propriedades Publisher somente leitura e Type. Os valores são originalmente fornecidos pela chamada para o Publication construtor de classe.

  • Membros relacionados com a publicação

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

  • Membros relacionados com direitos de autor

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

  • Uma substituição do ToString método

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

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

As classes Object e Publication

A Book classe

A Book aula representa um livro como um tipo especializado de publicação. O exemplo a seguir mostra o código-fonte da Book classe.

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 dos quais herda do , a Book classe define os seguintes membros exclusivos e substituições de Publicationmembros:

  • Dois construtores

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

    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 de 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 somente ISBN leitura, que retorna o Book International Standard Book Number do objeto, um número exclusivo de 10 ou 13 dígitos. O ISBN é fornecido como um argumento para um dos Book construtores. O ISBN é armazenado em um campo de suporte privado, que é gerado automaticamente pelo compilador.

  • Uma propriedade somente Author leitura. O nome do autor é fornecido como um argumento para ambos os Book construtores e é armazenado na propriedade.

  • Duas propriedades Price relacionadas a preços somente leitura e Currency. Seus valores são fornecidos como argumentos em uma chamada de SetPrice método. A Currency propriedade é 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 ISOCurrencySymbol propriedade. Ambas as propriedades são somente leitura externamente, mas ambas podem ser definidas por código na Book classe.

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

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

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

    Ao substituir o Object.Equals(Object) método, você também deve substituir o GetHashCode método, 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 Book objetos forem iguais, você retornará o código hash calculado chamando o GetHashCode método da cadeia de caracteres retornada pela ISBN propriedade.

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

Aulas de Publicações e Livros

Agora você pode instanciar um Book objeto, 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, ela 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 abstrata porque não fazia sentido instanciar um Publication objeto, 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 Shape classe é uma abstract classe com abstract métodos. 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 abstrata , cada membro da instância também é marcado com a palavra-chave abstrata . Nesse caso, Shape também substitui o Object.ToString método para retornar o nome do tipo, em vez de seu nome totalmente qualificado. E define dois membros GetArea estáticos 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 que Shape representam formas específicas. O exemplo a seguir define três classes, Square, Rectangle, e 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 e Shape chama os métodos estáticos da classe, que encapsula valores de Shape propriedade de retorno Shape . O tempo de execução recupera valores das propriedades substituídas dos tipos derivados. O exemplo também converte cada Shape objeto 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