Herança em C# e .NET
Este tutorial apresenta a herança em C#. Herança é um recurso das linguagens de programação orientadas a objeto que permite a definição de uma classe base que, por sua vez, fornece uma funcionalidade específica (dados e comportamento), e a definição de classes derivadas que herdam ou substituem essa funcionalidade.
Pré-requisitos
- Recomendamos o Visual Studio para Windows. Você pode baixar uma versão gratuita na página de downloads do Visual Studio. O Visual Studio inclui o SDK do .NET.
- Você também pode usar o editor do Visual Studio Code com o C# DevKit. Será preciso instalar o SDK do .NET mais recente separadamente.
- Se preferir outro editor, você precisará instalar o SDK do .NET mais recente.
Como executar os exemplos
Para criar e executar os exemplos neste tutorial, use o utilitário dotnet na linha de comando. Execute estas etapas para cada exemplo:
Crie um diretório para armazenar o exemplo.
Insira o comando dotnet new console no prompt de comando para criar um novo projeto do .NET Core.
Copie e cole o código do exemplo em seu editor de código.
Insira o comando dotnet restore na linha de comando para carregar ou restaurar as dependências do projeto.
Não é necessário executar
dotnet restore
, pois ele é executado implicitamente por todos os comandos que exigem uma restauração, comodotnet new
,dotnet build
,dotnet run
,dotnet test
,dotnet publish
edotnet pack
. Para desabilitar a restauração implícita, use a opção--no-restore
.O comando
dotnet restore
ainda é útil em determinados cenários em que realizar uma restauração explícita faz sentido, como compilações de integração contínua no Azure DevOps Services ou em sistemas de compilação que precisam controlar explicitamente quando a restauração ocorrerá.Para obter informações sobre como gerenciar feeds do NuGet, confira a documentação do
dotnet restore
.Insira o comando dotnet run para compilar e executar o exemplo.
Informações: O que é a herança?
Herança é um dos atributos fundamentais da programação orientada a objeto. Ela permite que você defina uma classe filha 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 oferecem suporte apenas à herança única. Ou seja, uma classe pode herdar apenas de uma única classe. No entanto, a herança é transitiva, o que permite que você defina 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 ao tipo D
.
Nem todos os membros de uma classe base são herdados por classes derivadas. Os membros a seguir 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 runtime para destruir as instâncias de uma classe.
Enquanto todos os outros membros de uma classe base são herdados por classes derivadas, o fato de serem visíveis ou não depende de sua acessibilidade. A acessibilidade de um membro afeta sua visibilidade para classes derivadas da seguinte maneira:
Membros Privados são visíveis apenas em classes derivadas que estão 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 derivada deA
, eC
deriva deA
. O campo particularA._value
é visível em A.B. No entanto, se você remover os comentários do métodoC.GetValue
e tentar compilar o exemplo, ele produzirá um erro do compilador CS0122: "'A._value' está inacessível em virtude do nível de proteção dele".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 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.
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, a classe
A
define um método chamadoMethod1
, e a classeB
herda da classeA
. Depois, o exemplo 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(); } }
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 o seguinte exemplo faz, gera o erro do compilador CS0506: "O <membro> não pode substituir o membro herdado <membro>, pois não está marcado como virtual, abstrato ou de substituição."
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. Membros de classe base marcados com a palavra-chave abstrato exigem que as classes derivadas os substituam. A tentativa de compilar o exemplo a seguir gera um erro do compilador CS0534, a "<classe> não implementa o membro abstrato herdado <membro>", pois a classe B
não fornece uma 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 se aplica apenas a classes e interfaces. Outras categorias de tipo (structs, delegados e enumerações) não dão suporte à herança. Devido a essas regras, tentar compilar código como o seguinte exemplo, produz o erro do compilador CS0527: "Tipo 'ValueType' na lista de interfaces não é uma interface". A mensagem de erro indica que, embora você possa definir as interfaces que um struct implementa, não há suporte para a herança.
public struct ValueStructure : ValueType // Generates CS0527.
{
}
Herança implícita
Apesar dos tipos possíveis de herança por meio de herança única, todos os tipos no sistema de tipo .NET herdam implicitamente de Object ou de um tipo derivado dele. A funcionalidade comum de 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
{ }
É possível usar reflexão (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ê ainda não tenha definido membros na 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 de C#. Os oito são membros do Object, o tipo do qual todas as classes e interfaces no sistema do tipo .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 objetoSimpleClass
em sua representação de cadeia de caracteres, retorna o nome de tipo totalmente qualificado. Nesse caso, o métodoToString
retorna a cadeia de caracteres "SimpleClass".Três métodos de teste de igualdade de dois objetos: o método
Equals(Object)
da instância pública, o métodoEquals(Object, Object)
do público estático e o métodoReferenceEquals(Object, Object)
de público estático. Por padrão, esses métodos testam a igualdade de referência; ou seja, para que seja iguais, duas variáveis de objeto devem fazer referência ao mesmo objeto.O método público
GetHashCode
, que calcula um valor que permite o uso de uma instância do tipo em conjuntos de hash.O método público
GetType
, que retorna um objeto Type que representa o tipoSimpleClass
.O método protegido Finalize, que é projetado para liberar recursos não gerenciados antes que a memória de um objeto seja reivindicada 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 objeto SimpleClass
como se ele fosse, na verdade, 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 de onde eles herdam implicitamente. Cada tipo base disponibiliza um conjunto diferente de membros por meio de herança para tipos derivados implicitamente.
Categoria do tipo | Herda implicitamente de |
---|---|
classe | Object |
struct | ValueType, Object |
enum | Enum, ValueType, Object |
delegado | MulticastDelegate, Delegate, Object |
Herança e um relacionamento "é um(a)"
Normalmente, a herança é usada para expressar um relacionamento "é um(a)" entre uma classe base e uma ou mais classes derivadas, em que as classes derivadas são versões especializadas da classe base; a classe derivada é um tipo de 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 da interface seja apresentada geralmente como uma alternativa para herança única, ou como uma forma de usar a herança com structs, ela tem como objetivo expressar um relacionamento diferente (um relacionamento "pode fazer") entre uma interface e seu tipo de implementação em comparação com a herança. Uma interface define um subconjunto de funcionalidades (como a capacidade de testar a igualdade, comparar ou classificar objetos ou dar suporte à formatação e análise sensível à cultura) que disponibiliza para seus tipos de implementação.
Observe que "é um(a)" também expressa o relacionamento 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 ano de fabricação. A classe Automobile
também tem um construtor cujos argumentos são atribuídos aos valores de propriedade. Ela também 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}";
}
Nesse caso, você não deve depender da herança para representar marcas e modelos de carro específicos. Por exemplo, você não precisa definir um tipo Packard
para representar automóveis fabricados pela empresa Packard Motor Car. Nesse caso, é possível representá-los criando um objeto Automobile
com os valores apropriados passados ao construtor de classe, como no 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
Um relacionamento é-um(a) baseado na herança é mais bem aplicado a uma classe base e em classes derivadas que adicionam outros membros à classe base, ou que exigem funcionalidades adicionais não incluídas na classe base.
Criação da classe base e das classes derivadas
Vamos examinar o processo de criação de uma classe base e de suas classes derivadas. Nesta seção, você definirá uma classe base, Publication
, que representa uma publicação de qualquer natureza, como um livro, uma revista, um jornal, um diário, um artigo etc. Você também definirá uma classe Book
que deriva de Publication
. É possível estender facilmente o exemplo para definir outras classes derivadas, como Magazine
, Journal
, Newspaper
e Article
.
A classe base de Publicação
Em projetar a classe Publication
, você precisa tomar várias decisões de design:
Quais membros devem ser incluídos na classe base
Publication
e se os membros dePublication
fornecem implementações de método, ou sePublication
é uma classe base abstrata que funciona como um modelo para suas classes derivadas.Nesse caso, a classe
Publication
fornecerá implementações de método. A seção Criação de classes base abstratas e de suas classes derivadas 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 adequada ao tipo derivado.A capacidade de reutilizar o código (ou seja, várias classes derivadas compartilham a declaração e a implementação dos métodos de classe base, e não é necessário substituí-las) é uma vantagem das classes base não abstratas. Portanto, você deverá adicionar membros à
Publication
se o código precisar ser compartilhado por um ou mais tiposPublication
especializados. Se você não conseguir fornecer implementações da classe base de forma eficiente, será necessário fornecer implementações de membro praticamente 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 possível fonte de bugs.Para maximizar a reutilização do código e criar uma hierarquia de herança lógica e intuitiva, inclua na classe
Publication
apenas dos dados e a funcionalidade comuns a todas as publicações ou à maioria delas. Depois, as classes derivadas implementam os membros exclusivos a determinados tipos de publicação que eles representam.O quanto devemos ampliar a 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 hierarquia pequena de uma classe
Publication
e uma única classe derivada, aBook
. É possível ampliar o exemplo facilmente para criar várias classes adicionais derivadas dePublication
, comoMagazine
eArticle
.Se faz sentido instanciar a classe base. Se não fizer, você deverá aplicar a palavra-chave abstract à classe. Caso contrário, a instância da classe
Publication
poderá ser criada chamando seu construtor de classe. Se for feita uma tentativa de criar uma instância de classe marcada com a palavra-chaveabstract
por uma chamada direta ao construtor dela, o compilador C# vai gerar o erro CS0144, "Impossível criar uma instância da interface ou da classe abstrata". Se for feita uma tentativa de instanciar a classe usando reflexão, o método de reflexão vai gerar o erro MemberAccessException.Por padrão, uma classe base pode ser instanciada chamando seu construtor de classe. Você não precisa definir um construtor de classe explicitamente. Se não houver um presente no código-fonte da classe base, o compilador de C# fornecerá automaticamente um construtor (sem parâmetros) padrão.
No seu exemplo, você marcará a classe
Publication
como abstract, para que não seja possível criar uma instância dela. Uma classeabstract
sem nenhum métodoabstract
indica que essa classe representa um conceito abstrato que é compartilhado entre várias classes concretas (comoBook
,Journal
).Se as classes derivadas precisam herdar a implementação de membros específicos da classe base, se elas têm a opção de substituir a implementação da classe base ou se precisam fornecer uma implementação. Use a palavra-chave abstract para forçar as classes derivadas a fornecer uma implementação. Use a palavra-chave virtual para permitir que as 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étodoabstract
, mas a classe em si éabstract
.Se uma classe derivada representa a classe final na hierarquia de herança e não pode se ser usada como uma classe base para outras classes derivadas. Por padrão, qualquer classe pode servir como classe base. Você pode aplicar a palavra-chave sealed para indicar que uma classe não pode funcionar como uma classe base para quaisquer classes adicionais. A tentativa de derivar de uma classe selada gerou o erro do compilador CS0509, "Não é possível derivar do tipo selado <typeName>."
No seu exemplo, você marcará a 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 herdados de Object, a classe Publication
define os seguintes membros exclusivos e substituições de membro:
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
, sua instância não pode ser criada diretamente no código, com no exemplo a seguir:var publication = new Publication("Tiddlywinks for Experts", "Fun and Games", PublicationType.Book);
No entanto, o construtor de instância pode ser chamado diretamente dos construtores de classe derivada, como mostra o código-fonte para a classe
Book
.Duas propriedades relacionadas à publicação
Title
é uma propriedade String somente leitura cujo valor é fornecido pela chamada do construtorPublication
.Pages
é uma propriedade Int32 de leitura-gravação que indica o número total de páginas da publicação. O valor é armazenado em um campo privado chamadototalPages
. O lançamento deve ser de um número positivo ou de um ArgumentOutOfRangeException.Membros relacionados ao publicador
Duas propriedades somente leitura,
Publisher
eType
. Originalmente, os valores eram fornecidos pela chamada para o construtor da classePublication
.Membros relacionados à publicação
Dois métodos,
Publish
eGetPublicationDate
, definem e retornam a data de publicação. O métodoPublish
define um sinalizador particularpublished
comotrue
quando é chamado e atribui a data passada para ele como argumento ao campo particulardatePublished
. O métodoGetPublicationDate
retorna a cadeia de caracteres "NYP" se o sinalizadorpublished
forfalse
, e o valor do campodatePublished
fortrue
.Membros relacionados a direitos autorais
O método
Copyright
usa o nome do proprietário dos direitos autorais e o ano dos direitos autorais como argumentos e os atribui às propriedadesCopyrightName
eCopyrightDate
.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, que é de pouca utilidade na diferenciação de uma instância para outra. A classe
Publication
substitui Object.ToString para retornar o valor da propriedadeTitle
.
A figura a seguir ilustra o relacionamento entre a classe base Publication
e sua classe herdada implicitamente Object.
A classe Book
A classe Book
representa um livro como tipo especializado de publicação. O exemplo a seguir mostra o código-fonte para a 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 herdados de Publication
, a classe Book
define os seguintes membros exclusivos e substituições de membro:
Dois construtores
Os dois construtores
Book
compartilham três parâmetros comuns. Dois, title e publisher, correspondem aos parâmetros do construtorPublication
. O terceiro é author, que é armazenado em uma propriedade públicaAuthor
imutável. Um construtor inclui um parâmetro isbn, que é armazenado na propriedade automáticaISBN
.O primeiro construtor usa a palavra-chave this para chamar o outro construtor. O encadeamento do construtor é 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 publicador 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 de C# fornecerá automaticamente uma chamada para a classe base padrão ou para o construtor sem parâmetros.
Uma propriedade
ISBN
somente leitura, que retorna o ISBN (International Standard Book Number) do objetoBook
, um número exclusivo com 10 ou 13 dígitos. O ISBN é fornecido como um argumento para um dos construtoresBook
. O ISBN é armazenado em um campo de suporte particular, gerado automaticamente pelo compilador.Uma propriedade
Author
somente leitura. O nome do autor é fornecido como um argumento para os dois construtoresBook
e é armazenado na propriedade.Duas propriedades somente leitura relacionadas ao preço,
Price
eCurrency
. Seus valores são fornecidos como argumentos em uma chamada do métodoSetPrice
. A propriedadeCurrency
é o símbolo de moeda ISO de três dígitos (por exemplo, USD para dólar americano). Símbolos de moeda ISO podem ser obtidos da propriedade ISOCurrencySymbol. Ambas as propriedades são somente leitura externamente, mas podem ser definidas por código na classeBook
.Um método
SetPrice
, que define os valores das propriedadesPrice
eCurrency
. Esses valores são retornados por essas mesmas propriedades.Substitui o método
ToString
(herdado dePublication
) 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 fizerem referência ao mesmo objeto. Na classe
Book
, por outro lado, dois objetosBook
devem ser iguais quando têm o mesmo ISBN.Ao substituir o método Object.Equals(Object), substitua também o método GetHashCode, que retorna um valor usado pelo runtime para armazenar itens em coleções de hash para uma recuperação eficiente. O código de hash deve retornar um valor que é consistente com o teste de igualdade. Como você substituiu Object.Equals(Object) para retornar
true
, se as propriedades de ISBN de dois objetosBook
forem iguais, retorne o código hash computado chamando o método GetHashCode da cadeia de caracteres retornada pela propriedadeISBN
.
A figura a seguir ilustra o relacionamento entre a classe Book
e Publication
, sua classe base.
Agora você pode criar a instância de um objeto Book
, invocar seus membros exclusivos e herdados e passá-lo como um argumento a 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
Criando classes base abstratas e suas classes derivadas
No exemplo anterior, você definiu uma classe base que forneceu uma implementação de diversos métodos para permitir que classes derivadas compartilhem código. Em muitos casos, no entanto, não espera-se que a classe base forneça uma implementação. Nesse caso, a classe base é uma classe abstrata que declara métodos abstratos. Ela funciona como um modelo que define os membros que cada classe derivada precisa 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 criar uma instância de um objeto Publication
, embora a classe fornecesse implementações de funcionalidades comuns para 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 entre as bordas da forma. A maneira com a qual essas propriedades são calculadas, no entanto, depende completamente da forma específica. A fórmula para calcular o perímetro (ou a circunferência) de um círculo, por exemplo, é diferente do 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 denominada Shape
que define duas propriedades: Area
e Perimeter
. Além da classe ser marcada com a palavra-chave abstract, cada membro da instância também é marcado com a palavra-chave abstract. Nesse caso, o 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 a recuperação fácil da área e do perímetro de uma instância de qualquer classe derivada. Quando você passa uma instância de uma classe derivada para um desses métodos, o runtime chama a substituição do 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
, Rectangle
e Circle
. Cada uma 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 cria uma matriz de objetos derivados de Shape
e chama os métodos estáticos da classe Shape
, que retorna valores de propriedade Shape
. O runtime recupera os valores das propriedades substituídas dos tipos derivados. O exemplo também converte cada objeto Shape
na matriz ao seu tipo derivado e, se a conversão for bem-sucedida, recupera as 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