C#: 重写返回类型

99

有没有办法在C#中覆盖返回类型?如果有,如何覆盖?如果没有,为什么,以及推荐的方法是什么?

我的情况是我有一个具有抽象基类和派生类的接口。我想这样做(好吧,不是真的,但作为一个例子!):

public interface Animal
{
   Poo Excrement { get; }
}

public class AnimalBase
{
   public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog
{
  // No override, just return normal poo like normal animal
}

public class Cat
{
  public override RadioactivePoo Excrement { get { return new RadioActivePoo(); } }
}

RadioactivePoo当然是从Poo继承而来的。

我希望这样做是为了让那些使用Cat对象的人能够使用Excrement属性,而不必将Poo转换为RadioactivePoo,例如,Cat仍然可以是Animal列表的一部分,用户可能不知道或不关心它们的放射性粪便。希望这有意思...

就我所看到的,编译器至少不允许这样做。所以我猜这是不可能的。但你会推荐什么解决方案呢?


2
泛型呢?它们难道不能帮助吗? - Arnis Lapsa
3
Cat.Excrement() 应该返回一个 RadioActivePoo 实例作为 Poo 吗?你们有一个共同的接口,使用它即可。(感谢提供滑稽的例子。) - hometoast
2
我也想感谢这个例子:@goodgai建议创建抽象的Poo - 我想知道如何向非程序员解释这个概念... - Paolo Tedesco
2
@Konrad:我有点不确定我是应该只是笑一笑,还是也应该问一下是否确实存在一个非常可怕的代码气味需要指出(因为如果是这种情况,我想知道:p)。 - Svish
6
我差点对自己发笑的这些例子感到失望,哈哈。 - Gurgadurgen
1
放射性便便?那TNTPoo和UselessPoo怎么样? - Simple Fellow
15个回答

51

那么一个通用的基类怎么样?

public class Poo { }
public class RadioactivePoo : Poo { }

public class BaseAnimal<PooType> 
    where PooType : Poo, new() {
    PooType Excrement {
        get { return new PooType(); }
    }
}

public class Dog : BaseAnimal<Poo> { }
public class Cat : BaseAnimal<RadioactivePoo> { }

编辑:使用扩展方法和标记接口的新解决方案...

public class Poo { }
public class RadioactivePoo : Poo { }

// just a marker interface, to get the poo type
public interface IPooProvider<PooType> { }

// Extension method to get the correct type of excrement
public static class IPooProviderExtension {
    public static PooType StronglyTypedExcrement<PooType>(
        this IPooProvider<PooType> iPooProvider) 
        where PooType : Poo {
        BaseAnimal animal = iPooProvider as BaseAnimal;
        if (null == animal) {
            throw new InvalidArgumentException("iPooProvider must be a BaseAnimal.");
        }
        return (PooType)animal.Excrement;
    }
}

public class BaseAnimal {
    public virtual Poo Excrement {
        get { return new Poo(); }
    }
}

public class Dog : BaseAnimal, IPooProvider<Poo> { }

public class Cat : BaseAnimal, IPooProvider<RadioactivePoo> {
    public override Poo Excrement {
        get { return new RadioactivePoo(); }
    }
}

class Program { 
    static void Main(string[] args) {
        Dog dog = new Dog();
        Poo dogPoo = dog.Excrement;

        Cat cat = new Cat();
        RadioactivePoo catPoo = cat.StronglyTypedExcrement();
    }
}

这种方式使得狗和猫都从动物继承(如评论所述,我的第一个解决方案没有保留继承关系)
需要使用标记接口明确标记类,这很麻烦,但也许这可以给你一些思路...

第二次编辑 @Svish:我修改了代码以明确显示扩展方法不以任何方式强制要求iPooProvider继承自BaseAnimal。您所说的“更加强类型”是什么意思?


刚想到同样的事情。 - Doctor Jones
9
你难道不会损失犬和猫之间的多态性吗? - almog.ori
如果您还想覆盖其他类型,怎么办?从技术上讲,您可能最终会得到一堆类型参数。我认为这可能会很烦人...但是,这是一个解决方案。 - Svish
@Svish:我想一旦发生这种情况,就是使用依赖注入框架的时候了。 - Brian
据我所知,如果我错了请纠正我(真的,我是认真的,我也想学习!:P),但是“iPooProvider”对“Excrement”的调用自然会调用动物类中的Excrement方法(在这种情况下,是“猫”的方法)。 - Olivier Tremblay
显示剩余2条评论

37

我知道这个问题已经有很多解决方案了,但我认为我想出了一种可以解决现有解决方案中存在问题的方法。

我对以下某些现有解决方案不满意的原因如下:

  • Paolo Tedesco的第一个解决方案:猫和狗没有共同的基类。
  • Paolo Tedesco的第二个解决方案:它有点复杂且难以阅读。
  • Daniel Daranas的解决方案:这个方法可以工作,但它会在您的代码中添加大量不必要的强制类型转换和Debug.assert()语句。
  • hjb417的解决方案:这种解决方案不允许你将逻辑放在基类中。在这个例子中逻辑非常简单(调用构造函数),但在实际应用中可能并不是那么简单。

我的解决方案

通过使用泛型和方法隐藏,这种解决方案应该能够克服我上面提到的所有问题。

public class Poo { }
public class RadioactivePoo : Poo { }

interface IAnimal
{
    Poo Excrement { get; }
}

public class BaseAnimal<PooType> : IAnimal
    where PooType : Poo, new()
{
    Poo IAnimal.Excrement { get { return (Poo)this.Excrement; } }

    public PooType Excrement
    {
        get { return new PooType(); }
    }
}

public class Dog : BaseAnimal<Poo> { }
public class Cat : BaseAnimal<RadioactivePoo> { }

使用这种解决方案,您无需覆盖Dog或Cat中的任何内容!以下是一些示例用法:

Cat bruce = new Cat();
IAnimal bruceAsAnimal = bruce as IAnimal;
Console.WriteLine(bruce.Excrement.ToString());
Console.WriteLine(bruceAsAnimal.Excrement.ToString());

这将会输出两次"RadioactivePoo",说明多态并未被破坏。

更多阅读

  • 显式接口实现
  • new 修饰符。我在这个简化的解决方案中没有使用它,但在更复杂的解决方案中可能需要使用它。例如,如果您想为 BaseAnimal 创建一个接口,那么您需要在 "PooType Excrement" 的声明中使用它。
  • out 泛型修饰符(协变性)。同样,在此解决方案中我没有使用它,但如果您想做一些类似于从 IAnimal 返回 MyType<Poo> 并从 BaseAnimal 返回 MyType<PooType> 的操作,则需要使用它以便能够在两者之间进行转换。

9
兄弟,这可能很酷。我现在没有时间进一步分析,但看起来你可能已经搞定了,如果你真的解决了这个问题,高五并感谢分享。不幸的是,“便便”和“排泄物”这个话题很让人分心。 - Nicholas Petersen
1
我认为我找到了一个相关问题的解决方案,其中一个方法必须返回继承的类型——即从“动物”继承的“狗”中的一个方法仍然返回Dog(this),而不是Animal。这可以通过扩展方法完成,如果我完成了,我可能会在这里分享。 - Nicholas Petersen
关于类型安全,bruce.Excement和bruceAsAnimal.Excrement两者都将是RadioactivePoo类型吗? - Anestis Kivranoglou
@AnestisKivranoglou 是的,两者都将是 RadioactivePoo 类型。 - rob
2
我已经在 Stack Overflow 上耕耘了 48 小时,而这个答案刚好解决了我的问题。然后我就想...“哥们,这太酷了。” - Martin Hansen Lennox
这个规则真是个隐藏的宝藏。 - pixelpax

35

这被称为返回类型协变,尽管一些人希望,但在C#或.NET中一般不支持。

我会保持相同的签名,但在派生类中添加一个额外的ENSURE子句,在其中确保该函数返回RadioActivePoo。简而言之,我会通过设计契约来做到语法无法实现的效果。

其他人则更喜欢模拟它。我觉得这也可以,但我倾向于节省“基础设施”代码行数。如果代码的语义足够清晰,我就很满意,而设计契约让我能够实现这一点,尽管它不是编译时机制。

对于泛型也是如此,其他答案建议使用它们,但我会出于比返回放射性粪便更好的原因使用它们-这只是我的个人见解。


什么是ENSURE子句?它如何工作?它是.NET中的属性吗? - Svish
1
在使用 .Net 之前,我会将 ENSURE(x) 子句简单地写成 "Debug.Assert(x)"。有关更多参考,请参阅 http://archive.eiffel.com/doc/manuals/technology/contract/page.html 或 Bertrand Meyer(1994)的《面向对象软件构造》第11章。 - Daniel Daranas
7
“我会用它们来做比只是把放射性粪便送回去更有意义的事情 - 但这只是我的想法。” 这句话现在是我最喜欢的自己的名言之一 :) - Daniel Daranas

11

C#9 提供了协变重写返回类型的功能。简单来说:你所想要的就能正常工作


从 https://learn.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance: "从 C# 9 开始支持协变返回类型。重写方法可以声明一个比它重写的方法更派生的返回类型,而重写的只读属性可以声明一个更派生的类型。" - Bill Menees

10

还有这个选项(显式接口实现)

public class Cat:Animal
{
  Poo Animal.Excrement { get { return Excrement; } }
  public RadioactivePoo Excrement { get { return new RadioactivePoo(); } }
}

你失去了使用基类实现Cat的能力,但另一方面,你保留了Cat和Dog之间的多态性。

但我怀疑增加的复杂度是否值得。


4

为什么不定义一个受保护的虚方法来创建“Excrement”,并保持返回“Excrement”的公共属性非虚拟。然后派生类可以覆盖基类的返回类型。

在下面的示例中,我使“Excrement”非虚拟,但提供了属性ExcrementImpl,以允许派生类提供正确的“Poo”。派生类型可以通过隐藏基类实现来覆盖“Excrement”的返回类型。

E.x.:

namepace ConsoleApplication8

{
public class Poo { }

public class RadioactivePoo : Poo { }

public interface Animal
{
    Poo Excrement { get; }
}

public class AnimalBase
{
    public Poo Excrement { get { return ExcrementImpl; } }

    protected virtual Poo ExcrementImpl
    {
        get { return new Poo(); }
    }
}

public class Dog : AnimalBase
{
    // No override, just return normal poo like normal animal
}

public class Cat : AnimalBase
{
    protected override Poo ExcrementImpl
    {
        get { return new RadioactivePoo(); }
    }

    public new RadioactivePoo Excrement { get { return (RadioactivePoo)ExcrementImpl; } }
}
}

这个例子让理解变得更加困难了。但是代码很棒! - rposky

3

试试这个:

namespace ClassLibrary1
{
    public interface Animal
    {   
        Poo Excrement { get; }
    }

    public class Poo
    {
    }

    public class RadioactivePoo
    {
    }

    public class AnimalBase<T>
    {   
        public virtual T Excrement
        { 
            get { return default(T); } 
        }
    }


    public class Dog : AnimalBase<Poo>
    {  
        // No override, just return normal poo like normal animal
    }

    public class Cat : AnimalBase<RadioactivePoo>
    {  
        public override RadioactivePoo Excrement 
        {
            get { return new RadioactivePoo(); } 
        }
    }
}

这里动物接口的意义是什么?没有任何类继承它。 - rob

2

如果我理解得不对,请纠正我。多态的整个意义不就是如果RadioActivePoo继承自Poo,那么可以返回RadioActivePoo吗?这个协议与抽象类相同,但只是返回RadioActivePoo()。


2
你说得完全正确,但他想避免额外的转换和一些更强的类型化,这正是泛型的用途... - Paolo Tedesco

1

我认为我已经找到了一种不依赖于泛型或扩展方法的方法,而是使用方法隐藏。但是它可能会破坏多态性,所以如果您进一步继承Cat,请特别小心。

我希望这篇文章能够帮助到某些人,尽管已经晚了8个月。

public interface Animal
{
    Poo Excrement { get; }
}

public class Poo
{
}

public class RadioActivePoo : Poo
{
}

public class AnimalBase : Animal
{
    public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog : AnimalBase
{
    // No override, just return normal poo like normal animal
}

public class CatBase : AnimalBase
{
    public override Poo Excrement { get { return new RadioActivePoo(); } }
}

public class Cat : CatBase
{
    public new RadioActivePoo Excrement { get { return (RadioActivePoo) base.Excrement; } }
}

啊,算了吧。我没意识到hjb417已经发布了一个类似的解决方案。至少我的解决方案不需要修改基类。 - Cybis
你的“解决方案”确实破坏了多态性,因此它并不是真正的解决方案。另一方面,hjb的解决方案是真正的解决方案,而且在我看来非常聪明。 - greenoldman

0

以下内容结合了其他答案的最佳方面,以及一种技术,允许 Cat 拥有所需的 RadioactivePoo 类型的 Excrement 属性,但如果我们只知道我们拥有一个 AnimalBase 而不是具体的 Cat,则可以将其作为简单的 Poo 返回。

调用者不需要使用泛型,即使它们存在于实现中,也不需要调用不同命名的函数来获取特殊的 Poo

中间类 AnimalWithSpecialisations 仅用于封装 Excrement 属性,通过非公共的 SpecialPoo 属性将其连接到派生类 AnimalWithSpecialPoo<TPoo>,后者具有派生返回类型的 Excrement 属性。

如果只有猫的粪便是特殊的,或者我们不希望粪便类型成为猫的主要定义特征,那么可以跳过中间的通用类,使得猫直接从AnimalWithSpecialisations派生。但是,如果有几种不同的动物其主要特征是它们的粪便在某种程度上是特殊的,将“样板”分离到中间类中有助于保持猫类本身相当干净,尽管需要额外的虚函数调用。
示例代码显示大多数预期操作都“按预期”工作。
public interface IExcretePoo<out TPoo>
  where TPoo : Poo
{
  TPoo Excrement { get; }
}

public class Poo
{ }

public class RadioactivePoo : Poo
{ }

public class AnimalBase : IExcretePoo<Poo>
{
  public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog : AnimalBase
{
  // No override, just return normal poo like normal animal
}

public abstract class AnimalWithSpecialisations : AnimalBase
{
  // this class connects AnimalBase to AnimalWithSpecialPoo<TPoo>
  public sealed override Poo Excrement { get { return SpecialPoo; } }

  // if not overridden, our "special" poo turns out just to be normal animal poo...
  protected virtual Poo SpecialPoo { get { return base.Excrement; } }
}

public abstract class AnimalWithSpecialPoo<TPoo> : AnimalWithSpecialisations, IExcretePoo<TPoo>
  where TPoo : Poo
{
  sealed protected override Poo SpecialPoo { get { return Excrement; } }
  public new abstract TPoo Excrement { get; }
}

public class Cat : AnimalWithSpecialPoo<RadioactivePoo>
{
  public override RadioactivePoo Excrement { get { return new RadioactivePoo(); } }
}

class Program
{
  static void Main(string[] args)
  {
    Dog dog = new Dog();
    Poo dogPoo = dog.Excrement;

    Cat cat = new Cat();
    RadioactivePoo catPoo = cat.Excrement;

    AnimalBase animal = cat;

    Poo animalPoo = catPoo;
    animalPoo = animal.Excrement;

    AnimalWithSpecialPoo<RadioactivePoo> radioactivePooingAnimal = cat;
    RadioactivePoo radioactivePoo = radioactivePooingAnimal.Excrement;

    IExcretePoo<Poo> pooExcreter = cat; // through this interface we don't know the Poo was radioactive.
    IExcretePoo<RadioactivePoo> radioactivePooExcreter = cat; // through this interface we do.

    // we can replace these with the dog equivalents:
    animal = dog;
    animalPoo = dogPoo;
    pooExcreter = dog;

    // but we can't do:
    // radioactivePooExcreter = dog;
    // radioactivePooingAnimal = dog;
    // radioactivePoo = dogPoo;
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接