Pergunta

Estou tendo alguns problemas de herança, pois tenho um grupo de classes abstratas inter-relacionadas que precisam ser substituídas juntas para criar uma implementação de cliente.Idealmente, eu gostaria de fazer algo como o seguinte:

abstract class Animal
{
  public Leg GetLeg() {...}
}

abstract class Leg { }

class Dog : Animal
{
  public override DogLeg Leg() {...}
}

class DogLeg : Leg { }

Isso permitiria que qualquer pessoa que usasse a classe Dog obtivesse DogLegs automaticamente e qualquer pessoa que usasse a classe Animal obtivesse Legs.O problema é que a função substituída deve ter o mesmo tipo da classe base, portanto não será compilada.Não vejo por que não deveria, já que DogLeg é implicitamente convertível em Leg.Eu sei que há muitas maneiras de contornar isso, mas estou mais curioso para saber por que isso não é possível/implementado em C#.

EDITAR:Modifiquei um pouco isso, pois na verdade estou usando propriedades em vez de funções em meu código.

EDITAR:Mudei de volta para funções, porque a resposta só se aplica a essa situação (covariância no parâmetro valor da função definida de uma propriedade não deveria trabalhar).Desculpe pelas flutuações!Sei que isso faz com que muitas respostas pareçam irrelevantes.

Foi útil?

Solução

A resposta curta é que GetLeg é invariante em seu tipo de retorno.A resposta longa pode ser encontrada aqui: Covariância e contravariância

Gostaria de acrescentar que, embora a herança seja geralmente a primeira ferramenta de abstração que a maioria dos desenvolvedores retira de sua caixa de ferramentas, quase sempre é possível usar a composição.A composição é um pouco mais trabalhosa para o desenvolvedor da API, mas torna a API mais útil para seus consumidores.

Outras dicas

Claramente, você precisará de um elenco se estiver operando em uma doglana quebrada.

Dog deve retornar Leg e não DogLeg como tipo de retorno.A classe real pode ser DogLeg, mas o objetivo é desacoplar para que o usuário de Dog não precise saber sobre DogLegs, ele só precisa saber sobre Legs.

Mudar:

class Dog : Animal
{
  public override DogLeg GetLeg() {...}
}

para:

class Dog : Animal
{
  public override Leg GetLeg() {...}
}

Não faça isso:

 if(a instanceof Dog){
       DogLeg dl = (DogLeg)a.GetLeg();

isso anula o propósito de programar para o tipo abstrato.

A razão para ocultar DogLeg é porque a função GetLeg na classe abstrata retorna um Abstract Leg.Se você estiver substituindo GetLeg, deverá retornar um Leg.Esse é o objetivo de ter um método em uma classe abstrata.Para propagar esse método para seus filhos.Se você deseja que os usuários do Dog conheçam DogLegs, crie um método chamado GetDogLeg e retorne um DogLeg.

Se você PUDESSE fazer o que o questionador deseja, então todo usuário do Animal precisaria saber sobre TODOS os animais.

É um desejo perfeitamente válido que a assinatura de um método substituído tenha um tipo de retorno que seja um subtipo do tipo de retorno no método substituído (ufa).Afinal, eles são compatíveis com o tipo de tempo de execução.

Mas C# ainda não suporta "tipos de retorno covariantes" em métodos substituídos (ao contrário de C++ [1998] e Java [2004]).

Você precisará contornar e se preparar para o futuro próximo, como afirmou Eric Lippert em o blog dele[19 de junho de 2008]:

Esse tipo de variação é chamado de “covariância do tipo de retorno”.

não temos planos de implementar esse tipo de variação em C#.

abstract class Animal
{
  public virtual Leg GetLeg ()
}

abstract class Leg { }

class Dog : Animal
{
  public override Leg GetLeg () { return new DogLeg(); }
}

class DogLeg : Leg { void Hump(); }

Faça assim, então você poderá aproveitar a abstração em seu cliente:

Leg myleg = myDog.GetLeg();

Então, se precisar, você pode lançá-lo:

if (myleg is DogLeg) { ((DogLeg)myLeg).Hump()); }

Totalmente artificial, mas a questão é que você pode fazer isso:

foreach (Animal a in animals)
{
   a.GetLeg().SomeMethodThatIsOnAllLegs();
}

Embora ainda mantenha a capacidade de ter um método Hump especial em Doglegs.

Você pode usar genéricos e interfaces para implementar isso em C#:

abstract class Leg { }

interface IAnimal { Leg GetLeg(); }

abstract class Animal<TLeg> : IAnimal where TLeg : Leg
 { public abstract TLeg GetLeg();
   Leg IAnimal.GetLeg() { return this.GetLeg(); }
 }

class Dog : Animal<Dog.DogLeg>
 { public class DogLeg : Leg { }
   public override DogLeg GetLeg() { return new DogLeg();}
 } 

GetLeg() deve retornar Leg para ser uma substituição.Sua classe Dog, entretanto, ainda pode retornar objetos DogLeg, pois eles são uma classe filha de Leg.os clientes podem então lançá-los e operá-los como doglegs.

public class ClientObj{
    public void doStuff(){
    Animal a=getAnimal();
    if(a is Dog){
       DogLeg dl = (DogLeg)a.GetLeg();
    }
  }
}

O conceito que está causando problemas é descrito em http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

Não que seja muito útil, mas talvez seja interessante notar que Java suporta retornos covariantes e, portanto, funcionaria exatamente como você esperava.Exceto, obviamente, que Java não possui propriedades;)

Talvez seja mais fácil ver o problema com um exemplo:

Animal dog = new Dog();
dog.SetLeg(new CatLeg());

Agora, isso deve ser compilado se você for compilado pelo Dog, mas provavelmente não queremos tal mutante.

Uma questão relacionada é Dog[] ser um Animal[], ou IList<Dog> um IList<Animal>?

C# possui implementações de interface explícitas para resolver apenas este problema:

abstract class Leg { }
class DogLeg : Leg { }

interface IAnimal
{
    Leg GetLeg();
}

class Dog : IAnimal
{
    public override DogLeg GetLeg() { /* */ }

    Leg IAnimal.GetLeg() { return GetLeg(); }
}

Se você tiver um Dog por meio de uma referência do tipo Dog, chamar GetLeg() retornará um DogLeg.Se você tiver o mesmo objeto, mas a referência for do tipo IAnimal, então ele retornará uma Leg.

Certo, entendo que posso simplesmente lançar, mas isso significa que o cliente precisa saber que Cães têm DogLegs.O que me pergunto é se existem razões técnicas pelas quais isso não é possível, visto que existe uma conversão implícita.

@Brian Leahy Obviamente, se você estiver operando apenas como uma perna, não há necessidade ou razão para lançar.Mas se houver algum comportamento específico de DogLeg ou Dog, às vezes há razões pelas quais o gesso é necessário.

Você também pode retornar a interface ILeg que Leg e/ou DogLeg implementam.

O importante a lembrar é que você pode usar um tipo derivado em cada lugar que usar o tipo base (você pode passar Dog para qualquer método/propriedade/campo/variável que espere Animal)

Vamos pegar esta função:

public void AddLeg(Animal a)
{
   a.Leg = new Leg();
}

Uma função perfeitamente válida, agora vamos chamar a função assim:

AddLeg(new Dog());

Se a propriedade Dog.Leg não for do tipo Leg, a função AddLeg de repente contém um erro e não pode ser compilada.

@Lucas

Acho que você talvez esteja entendendo mal a herança.Dog.GetLeg() retornará um objeto DogLeg.

public class Dog{
    public Leg GetLeg(){
         DogLeg dl = new DogLeg(super.GetLeg());
         //set dogleg specific properties
    }
}


    Animal a = getDog();
    Leg l = a.GetLeg();
    l.kick();

o método real chamado será Dog.GetLeg();e DogLeg.Kick() (estou assumindo que existe um método Leg.kick()) para isso, o tipo de retorno declarado sendo DogLeg é desnecessário, porque foi isso que retornou, mesmo que o tipo de retorno para Dog.GetLeg() seja Perna.

Você pode conseguir o que deseja usando um genérico com uma restrição apropriada, como a seguinte:

abstract class Animal<LegType> where LegType : Leg
{
    public abstract LegType GetLeg();
}

abstract class Leg { }

class Dog : Animal<DogLeg>
{
    public override DogLeg GetLeg()
    {
        return new DogLeg();
    }
}

class DogLeg : Leg { }
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top