Considere este código:
class Program
{
static void Main(string[] args)
{
Person person = new Teacher();
person.ShowInfo();
Console.ReadLine();
}
}
public class Person
{
public void ShowInfo()
{
Console.WriteLine("I am Person");
}
}
public class Teacher : Person
{
public new void ShowInfo()
{
Console.WriteLine("I am Teacher");
}
}
Quando executo esse código, é produzido o seguinte:
Eu sou pessoa
No entanto, você pode ver que é uma instância de Teacher
, não de Person
. Por que o código faz isso?
c#
class
derived-class
azulado
fonte
fonte
Respostas:
Há uma diferença entre
new
evirtual
/override
.Você pode imaginar que uma classe, quando instanciada, nada mais é do que uma tabela de ponteiros, apontando para a implementação real de seus métodos. A imagem a seguir deve visualizar isso muito bem:
Agora, existem diferentes maneiras de definir um método. Cada um se comporta diferente quando é usado com herança. A maneira padrão sempre funciona como a imagem acima ilustra. Se você deseja alterar esse comportamento, pode anexar palavras-chave diferentes ao seu método.
1. Classes abstratas
O primeiro é
abstract
.abstract
Os métodos simplesmente apontam para lugar nenhum:Se sua classe contiver membros abstratos, ela também precisará ser marcada como
abstract
, caso contrário, o compilador não compilará seu aplicativo. Você não pode criar instâncias deabstract
classes, mas pode herdar delas e criar instâncias de suas classes herdadas e acessá-las usando a definição de classe base. No seu exemplo, isso seria semelhante a:Se chamado, o comportamento de
ShowInfo
varia, com base na implementação:Ambos,
Student
s eTeacher
s sãoPerson
s, mas se comportam de maneira diferente quando solicitados a solicitar informações sobre si mesmos. No entanto, a maneira de solicitar que eles solicitem suas informações é a mesma: usando aPerson
interface da classe.Então, o que acontece nos bastidores, quando você herda
Person
? Ao implementarShowInfo
, o ponteiro não está mais apontando para lugar nenhum, agora aponta para a implementação real! Ao criar umaStudent
instância, ele aponta paraStudent
sShowInfo
:2. Métodos virtuais
A segunda maneira é usar
virtual
métodos. O comportamento é o mesmo, exceto que você está fornecendo uma implementação padrão opcional em sua classe base. Classes comvirtual
membros podem ser instanciadas, no entanto, classes herdadas podem fornecer implementações diferentes. Aqui está como seu código deve realmente funcionar:A principal diferença é que o membro base
Person.ShowInfo
não está mais apontando para lugar nenhum. Esse também é o motivo pelo qual você pode criar instâncias dePerson
(e, portanto, não precisa mais ser marcado comoabstract
):Você deve notar que isso não parece diferente da primeira imagem no momento. Isso ocorre porque o
virtual
método está apontando para uma implementação " da maneira padrão ". Usandovirtual
, você pode dizerPersons
, que eles podem (não devem ) fornecer uma implementação diferente paraShowInfo
. Se você fornecer uma implementação diferente (usandooverride
), como eu fiz para oTeacher
acima, a imagem terá a mesma aparência deabstract
. Imagine, não fornecemos uma implementação personalizada paraStudent
s:O código seria chamado assim:
E a imagem para
Student
ficaria assim:3. A palavra-chave mágica `new`, conhecida como" Shadowing "
new
é mais um truque em torno disso. Você pode fornecer métodos em classes generalizadas, com os mesmos nomes que os métodos na classe / interface base. Ambos apontam para sua própria implementação personalizada:A implementação se parece com a que você forneceu. O comportamento é diferente, com base na maneira como você acessa o método:
Esse comportamento pode ser desejado, mas no seu caso, é enganoso.
Espero que isso torne as coisas mais claras para você entender!
fonte
new
que breaks herança da função e faz com que a nova função de separar da superclasse funçãoPerson
, nãoStudent
;)O polimorfismo de subtipo em C # usa virtualidade explícita, semelhante ao C ++, mas diferente do Java. Isso significa que você precisa explicitamente marcar métodos como substituíveis (ou seja
virtual
). No C #, você também precisa marcar explicitamente os métodos de substituição como substituindo (ou sejaoverride
) para evitar erros de digitação.No código da sua pergunta, você usa o
new
que faz sombreamento em vez de substituir. O sombreamento afeta apenas a semântica em tempo de compilação em vez da semântica de tempo de execução, daí a saída não intencional.fonte
Você tem que fazer o método virtual e substituir a função na classe filho, a fim de chamar o método do objeto de classe que você coloca na referência da classe pai.
Métodos virtuais
Usando novo para sombreamento
Você está usando uma nova palavra-chave em vez de substituir, é isso que as novas
Se o método na classe derivada não for precedido por palavras-chave novas ou de substituição, o compilador emitirá um aviso e o método se comportará como se a nova palavra-chave estivesse presente.
Se o método na classe derivada for precedido com a nova palavra-chave, o método será definido como independente do método da classe base , este artigo do MSDN explica muito bem.
Ligação antecipada VS Ligação tardia
Temos ligação antecipada em tempo de compilação para o método normal (não virtual), que é o caso atual em que o compilador ligará a chamada para o método da classe base que é o método do tipo de referência (classe base) em vez do objeto ser mantido na referência da base classe ou seja, objeto de classe derivada . Isso ocorre porque
ShowInfo
não é um método virtual. A ligação tardia é executada em tempo de execução para (método virtual / substituído) usando a tabela de método virtual (vtable).fonte
Eu quero aproveitar a resposta de Achratt . Para completar, a diferença é que o OP espera que a
new
palavra - chave no método da classe derivada substitua o método da classe base. O que ele realmente faz é ocultar o método da classe base.Em C #, como outra resposta mencionada, a substituição do método tradicional deve ser explícita; o método da classe base deve ser marcado como
virtual
e a classe derivada deve especificamenteoverride
o método da classe base. Se isso for feito, não importa se o objeto é tratado como uma instância da classe base ou da classe derivada; o método derivado é encontrado e chamado. Isso é feito de maneira semelhante à do C ++; um método marcado como "virtual" ou "substituir", quando compilado, é resolvido como "atrasado" (em tempo de execução), determinando o tipo real do objeto referenciado e percorrendo a hierarquia do objeto para baixo ao longo da árvore do tipo de variável para o tipo de objeto real, para encontrar a implementação mais derivada do método definido pelo tipo de variável.Isso difere do Java, que permite "substituições implícitas"; por exemplo, métodos (não estáticos), simplesmente definir um método da mesma assinatura (nome e número / tipo de parâmetros) fará com que a subclasse substitua a superclasse.
Como geralmente é útil estender ou substituir a funcionalidade de um método não virtual que você não controla, o C # também inclui a
new
palavra-chave contextual. Anew
palavra-chave "oculta" o método pai em vez de substituí-lo. Qualquer método herdável pode ser oculto, seja virtual ou não; isso permite que você, desenvolvedor, aproveite os membros que deseja herdar de um pai, sem ter que contornar os que não são, enquanto ainda permite apresentar a mesma "interface" aos consumidores do seu código.Ocultar funciona de maneira semelhante à substituição da perspectiva de uma pessoa que usa seu objeto no nível de herança ou abaixo do nível em que o método de ocultação está definido. Do exemplo da pergunta, um codificador que cria um professor e armazena essa referência em uma variável do tipo professor verá o comportamento da implementação ShowInfo () do professor, que oculta o de pessoa. No entanto, alguém que trabalha com seu objeto em uma coleção de registros de Pessoa (como você é) verá o comportamento da implementação de ShowInfo (); como o método do professor não substitui seu pai (o que também exigiria que Person.ShowInfo () seja virtual), o código que funciona no nível de abstração da pessoa não encontrará a implementação do professor e não a usará.
Além disso, a
new
palavra - chave não apenas fará isso explicitamente, como também o C # permite ocultar o método implícito; simplesmente definir um método com a mesma assinatura que um método de classe pai, semoverride
ounew
, irá ocultá-lo (embora produza um aviso do compilador ou uma reclamação de determinados assistentes de refatoração, como ReSharper ou CodeRush). Esse é o compromisso que os designers do C # criaram entre as substituições explícitas do C ++ e as implícitas do Java e, embora seja elegante, nem sempre produz o comportamento que você esperaria se tivesse experiência em um dos idiomas mais antigos.Aqui estão as novidades: isso fica complexo quando você combina as duas palavras-chave em uma longa cadeia de herança. Considere o seguinte:
Resultado:
O primeiro conjunto de cinco é de se esperar; como cada nível possui uma implementação e é referenciado como um objeto do mesmo tipo que foi instanciado, o tempo de execução resolve cada chamada para o nível de herança referenciado pelo tipo de variável.
O segundo conjunto de cinco é o resultado da atribuição de cada instância a uma variável do tipo pai imediato. Agora, algumas diferenças de comportamento se agitam;
foo2
, que na verdade é umBar
elenco como umFoo
, ainda encontrará o método mais derivado do tipo de objeto real Bar.bar2
é umBaz
, mas diferente defoo2
, porque o Baz não substitui explicitamente a implementação do Bar (não pode; Barsealed
it), ele não é visto pelo tempo de execução ao olhar "de cima para baixo"; portanto, a implementação do Bar é chamada. Observe que o Baz não precisa usar anew
palavra - chave; você receberá um aviso do compilador se omitir a palavra-chave, mas o comportamento implícito no C # é ocultar o método pai.baz2
é umBai
, que substituiBaz
onew
implementação, portanto seu comportamento é semelhante aoé a , que não possui implementação de nenhum tipo e simplesmente usa a de seu pai.foo2
's; a implementação do tipo de objeto real em Bai é chamada. , que novamente oculta a implementação do método pai e se comporta da mesma maneira que a implementação de Bai não está selada, então, teoricamente, Bat poderia ter substituído, em vez de ocultado o método. Finalmente, é umbai2
Bat
Bai
bar2
bat2
Bak
O terceiro conjunto de cinco ilustra o comportamento completo da resolução de cima para baixo. Na verdade, tudo está fazendo referência a uma instância da classe mais derivada da cadeia,
Bak
mas a resolução em todos os níveis do tipo de variável é executada iniciando nesse nível da cadeia de herança e detalhando a substituição explícita mais derivada do método, que é aqueles emBar
,Bai
, eBat
. O método oculto "quebra" a cadeia de herança primordial; você deve estar trabalhando com o objeto no nível de herança ou abaixo dele que oculta o método para que o método oculto seja usado. Caso contrário, o método oculto é "descoberto" e usado.fonte
Leia sobre o polimorfismo em C #: Polimorfismo (Guia de Programação em C #)
Este é um exemplo de lá:
fonte
Você precisa fazer isso
virtual
e substituir essa função emTeacher
. Como você está herdando e usando o ponteiro base para se referir a uma classe derivada, é necessário substituí-lo usandovirtual
.new
é para ocultar obase
método de classe em uma referência de classe derivada e não em umabase
referência de classe.fonte
Gostaria de adicionar mais alguns exemplos para expandir as informações em torno disso. Espero que isso ajude também:
Aqui está um exemplo de código que limpa o ar em torno do que acontece quando um tipo derivado é atribuído a um tipo de base. Quais métodos estão disponíveis e a diferença entre métodos substituídos e ocultos nesse contexto.
Outra pequena anomalia é que, para a seguinte linha de código:
O compilador VS (intellisense) mostraria a.foo () como A.foo ().
Portanto, fica claro que, quando um tipo mais derivado é atribuído a um tipo de base, a variável 'tipo de base' atua como o tipo de base até que um método que seja substituído em um tipo derivado seja referenciado. Isso pode se tornar um pouco contra-intuitivo com métodos ocultos ou métodos com o mesmo nome (mas não substituídos) entre os tipos pai e filho.
Este exemplo de código deve ajudar a delinear essas advertências!
fonte
C # é diferente de java no comportamento de substituição de classe pai / filho. Por padrão, em Java, todos os métodos são virtuais, portanto, o comportamento que você deseja é suportado imediatamente.
No C #, você precisa marcar um método como virtual na classe base, para obter o que deseja.
fonte
A nova palavra-chave informa que o método na classe atual só funcionará se você tiver uma instância da classe Teacher armazenada em uma variável do tipo Teacher. Ou você pode ativá-lo usando castings: ((Teacher) Person) .ShowInfo ()
fonte
O tipo de variável 'professor' aqui é
typeof(Person)
e esse tipo não sabe nada sobre a classe Professor e não tenta procurar nenhum método nos tipos derivados. Para chamar o método da classe Teacher, você deve converter sua variável:(person as Teacher).ShowInfo()
.Para chamar um método específico com base no tipo de valor, você deve usar a palavra-chave 'virtual' em sua classe base e substituir métodos virtuais em classes derivadas. Essa abordagem permite implementar classes derivadas com ou sem substituição de métodos virtuais. Os métodos da classe base serão chamados para tipos sem virtuais sobrecarregados.
fonte
Pode ser tarde demais ... Mas a pergunta é simples e a resposta deve ter o mesmo nível de complexidade.
No seu código, a variável person não sabe nada sobre Teacher.ShowInfo (). Não há como chamar o último método a partir da referência da classe base, porque não é virtual.
Existe uma abordagem útil para a herança - tente imaginar o que você quer dizer com sua hierarquia de códigos. Tente também imaginar o que uma ou outra ferramenta diz sobre si mesma. Por exemplo, se você adicionar uma função virtual a uma classe base, supõe: 1. ela pode ter implementação padrão; 2. pode ser reimplementado na classe derivada. Se você adicionar uma função abstrata, isso significa apenas uma coisa - a subclasse deve criar uma implementação. Mas caso você tenha uma função simples - você não espera que ninguém mude sua implementação.
fonte
O compilador faz isso porque não sabe que é a
Teacher
. Tudo o que sabe é que é umPerson
ou algo derivado disso. Então, tudo o que você pode fazer é chamar oPerson.ShowInfo()
métodofonte
Só queria dar uma breve resposta -
Você deve usar
virtual
eoverride
em classes que possam ser substituídas. Usevirtual
para métodos que podem ser substituídos por classes filho e useoverride
para métodos que devem substituir essesvirtual
métodos.fonte
Eu escrevi o mesmo código que você mencionou acima em java, exceto algumas alterações e funcionou bem como exceção. O método da classe base é substituído e, portanto, a saída exibida é "Eu sou professor".
Motivo: como estamos criando uma referência da classe base (que é capaz de ter instância de referência da classe derivada) que na verdade contém a referência da classe derivada. E como sabemos que a instância sempre olha seus métodos primeiro, se a encontrar lá, a executa e, se não encontrar a definição, subirá na hierarquia.
fonte
Com base na excelente demonstração de Keith S. e nas respostas de qualidade de todos os demais, e por uma questão de super uber, vamos em frente e lançar implementações explícitas de interface para demonstrar como isso funciona. Considere o seguinte:
namespace LinqConsoleApp {
}
Aqui está a saída:
pessoa: Eu sou Pessoa == LinqConsoleApp.Teacher
professor: Eu sou professor == LinqConsoleApp.Teacher
person1: Sou professor == LinqConsoleApp.Teacher
person2: Sou professor == LinqConsoleApp.Teacher
teacher1: Sou professor == LinqConsoleApp.Teacher
person4: Eu sou Person == LinqConsoleApp.Person
person3: Sou interface Person == LinqConsoleApp.Person
Duas coisas a serem observadas:
O método Teacher.ShowInfo () omite a nova palavra-chave. Quando new é omitido, o comportamento do método é o mesmo que se a nova palavra-chave tivesse sido explicitamente definida.
Você só pode usar a palavra-chave override em conjunto com a palavra-chave virtual. O método da classe base deve ser virtual. Ou abstrato, nesse caso a classe também deve ser abstrata.
person obtém a implementação base do ShowInfo porque a classe Teacher não pode substituir a implementação base (sem declaração virtual) e person é .GetType (Teacher), portanto oculta a implementação da classe Teacher.
O professor obtém a implementação derivada do professor de ShowInfo porque o professor é Typeof (Professor) e não está no nível de herança da Pessoa.
person1 obtém a implementação derivada do professor porque é .GetType (professor) e a nova palavra-chave implícita oculta a implementação básica.
person2 também obtém a implementação derivada do professor, embora implemente IPerson e obtém uma conversão explícita para IPerson. Isso ocorre novamente porque a classe Teacher não implementa explicitamente o método IPerson.ShowInfo ().
teacher1 também obtém a implementação derivada do professor porque é .GetType (Teacher).
Somente person3 obtém a implementação IPerson de ShowInfo porque apenas a classe Person implementa explicitamente o método e person3 é uma instância do tipo IPerson.
Para implementar explicitamente uma interface, você deve declarar uma instância var do tipo de interface de destino e uma classe deve implementar explicitamente (qualificar totalmente) os membros da interface.
Observe que nem person4 obtém a implementação IPerson.ShowInfo. Isso ocorre porque, embora person4 seja .GetType (Person) e mesmo que Person implemente IPerson, person4 não é uma instância do IPerson.
fonte
Exemplo do LinQPad para iniciar às cegas e reduzir a duplicação de código. Acho que é isso que você estava tentando fazer.
fonte