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
- Recomendamos o Visual Studio para Windows. Você pode baixar uma versão gratuita da página de downloads do Visual Studio. O Visual Studio inclui o SDK do .NET.
- Você também pode usar o editor de código do Visual Studio com o C# DevKit. Você precisará instalar o SDK .NET mais recente separadamente.
- Se preferir um editor diferente, você precisa instalar o SDK .NET mais recente.
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:
Crie um diretório para armazenar o exemplo.
Digite o comando dotnet new console em um prompt de comando para criar um novo projeto .NET Core.
Copie e cole o código do exemplo no editor de códigos.
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, comodotnet new
,dotnet build
,dotnet run
,dotnet test
,dotnet publish
edotnet 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.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 A
de 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 deA
, eC
deriva deA
. O campo privadoA._value
é visível em A.B. No entanto, se você remover os comentários doC.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 chamadoMethod1
, e classB
herda da classeA
. O exemplo então chamaMethod1
como se fosse um método de instância emB
.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, 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 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 umSimpleClass
objeto em sua representação de cadeia de caracteres, retorna o nome de tipo totalmente qualificado. Nesse caso, oToString
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áticoEquals(Object, Object)
público e o método estáticoReferenceEquals(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 oSimpleClass
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, 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 Book
classe que deriva de Publication
. Você pode facilmente estender o exemplo para definir outras classes derivadas, como Magazine
, Journal
, Newspaper
e 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 osPublication
membros fornecem implementações de método ou sePublication
é 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 especializadosPublication
. 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 dePeriodical
, que por sua vez é uma classe base deMagazine
,Journal
eNewspaper
.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 dePublication
, comoMagazine
eArticle
.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 aabstract
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. Umaabstract
classe sem nenhumabstract
método indica que essa classe representa um conceito abstrato que é compartilhado entre várias classes concretas (como umBook
,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 nenhumabstract
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 oPublication
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 chamadototalPages
. Deve ser um número positivo ou um ArgumentOutOfRangeException é lançado.Membros relacionados ao editor
Duas propriedades
Publisher
somente leitura eType
. Os valores são originalmente fornecidos pela chamada para oPublication
construtor de classe.Membros relacionados com a publicação
Dois métodos,
Publish
eGetPublicationDate
, defina e retorne a data de publicação. OPublish
método define um sinalizador privadopublished
paratrue
quando ele é chamado e atribui a data passada a ele como um argumento para o campo privadodatePublished
. OGetPublicationDate
método retorna a cadeia de caracteres "NYP" se o sinalizadorpublished
forfalse
, e odatePublished
valor do campo se fortrue
.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 àsCopyrightName
eCopyrightDate
propriedades.Uma substituição do
ToString
métodoSe 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 daTitle
propriedade.
A figura a seguir ilustra a relação entre sua classe base Publication
e sua classe implicitamente herdada Object .
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 Publication
membros:
Dois construtores
Os dois
Book
construtores compartilham três parâmetros comuns. Dois, título e editor, correspondem aos parâmetros doPublication
construtor. O terceiro é o autor, que é armazenado em um imóvel público imutávelAuthor
. Um construtor inclui um parâmetro isbn , que é armazenado naISBN
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 oBook
International Standard Book Number do objeto, um número exclusivo de 10 ou 13 dígitos. O ISBN é fornecido como um argumento para um dosBook
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 osBook
construtores e é armazenado na propriedade.Duas propriedades
Price
relacionadas a preços somente leitura eCurrency
. Seus valores são fornecidos como argumentos em uma chamada deSetPrice
método. ACurrency
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 naBook
classe.Um
SetPrice
método, que define os valores dasPrice
propriedades eCurrency
. Esses valores são retornados por essas mesmas propriedades.Substitui o
ToString
método (herdado dePublication
) 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, doisBook
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 doisBook
objetos forem iguais, você retornará o código hash calculado chamando o GetHashCode método da cadeia de caracteres retornada pelaISBN
propriedade.
A figura a seguir ilustra a relação entre a classe e Publication
, Book
sua classe base.
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