我可以用派生类型来进行覆盖吗?

46

据我所知,在C# 2.0中无法实现以下操作。

public class Father
{
    public virtual Father SomePropertyName
    {
        get
        {
            return this;
        }
    }
}

public class Child : Father
{
    public override Child SomePropertyName
    {
        get
        {
            return this;
        }
    }
}

我通过在派生类中将属性创建为 "new" 来解决了这个问题,但这当然不是多态的。

public new Child SomePropertyName

2.0有什么解决方案吗? 3.5中是否有任何解决此问题的功能?


1
除了那个说你不能做的解决方案之外,其他所有解决方案都没有真正帮助到这种情况。我认为重点是能够拥有一个Child类(作为父类进行转换)并调用SomeProperty,使其返回一个Child而无需进行转换。当然,我像其他人一样猜测真正的目的 :) - mattlant
尽管没有完美的解决方案,但想法是找到最清晰的实现方式,尽量避免使用嘈杂的东西,如强制转换,使用“is”检查类型等。 - Luis Filipe
3
虽然你无法在C#中做到这一点,但我已经写了一系列关于如何实现这一点的博客文章:http://www.simple-talk.com/community/blogs/simonc/archive/2010/07/14/93495.aspx。 - thecoop
从C#9开始,是的,您可以使用派生类型进行重写 - MarredCheese
9个回答

44

你可以重新声明(new),但你不能同时重新声明并覆盖同一个名称。 一种选择是使用受保护的方法来隐藏细节,这样可以同时实现多态性和隐藏:

public class Father
{
    public Father SomePropertyName
    {
        get {
            return SomePropertyImpl();
        }
    }
    protected virtual Father SomePropertyImpl()
    {
        // base-class version
    }
}

public class Child : Father
{
    public new Child SomePropertyName
    {
        get
        { // since we know our local SomePropertyImpl actually returns a Child
            return (Child)SomePropertyImpl();
        }
    }
    protected override Father SomePropertyImpl()
    {
        // do something different, might return a Child
        // but typed as Father for the return
    }
}

30
由于类型安全的考虑,在任何.NET语言中都不可能实现这一点。在类型安全的语言中,您必须为返回值提供协变性,并为参数提供逆变性。看看这段代码:
class B {
    S Get();
    Set(S);
}
class D : B {
    T Get();
    Set(T);
}

对于“Get”方法,协变意味着T必须是S或从S派生的类型。否则,如果您有一个类型为D的对象引用存储在类型为B的变量中,并调用B.Get(),则不会返回可表示为S的对象--破坏了类型系统。
对于“Set”方法,逆变意味着T必须是S或S派生的类型。否则,如果您有一个类型为D的对象引用存储在类型为B的变量中,并调用B.Set(X),其中X的类型为S但不是T,则D :: Set(T)将获得一个它不期望的类型的对象。
在C#中,有一个有意识的决定禁止在重载属性时更改类型,即使它们只有getter / setter对中的一个,因为否则它将具有非常不一致的行为(“你的意思是,我可以更改具有getter的那个类型,但没有getter和setter的那个类型吗? 为什么不?!”- 匿名替代宇宙新手)。

3
你的观点是正确的,但是这个例子有一个只读属性。 - Anthony
2
你正在引入一种人为的限制,即Get必须返回与Set接收到的相同的T。如果没有这个限制,你的论点就不成立了。请参考这个答案以获得更好的解释:https://dev59.com/2m025IYBdhLWcg3w76lq - lex82

13
不可以,但是您可以在2及以上版本中使用泛型:
public class MyClass<T> where T: Person
{
    public virtual T SomePropertyName
    {
        get
        {
            return  ...;
        }
    }
}

那么,Father和Child是同一类的通用版本。


1
不完全是一回事。在他的代码下,这样写:Father child = new Child(); Child newChild = child.SomePropertyName;会需要一个强制转换。而在你的代码下,甚至连编译都无法通过。 - James Curran
你说得对,这并不是。这只是一个类似的基本示例 - 你无法使用泛型完全匹配他的代码,我认为你会得到一个循环类型引用。 - Keith

10

现代答案

C# 9 开始,支持返回类型协变。下面是从上述链接中复制的一个基本示例:

class Compilation ...
{
    public virtual Compilation WithOptions(Options options)...
}

class CSharpCompilation : Compilation
{
    public override CSharpCompilation WithOptions(Options options)...
}

5
请注意,它不适用于读写属性。 - montonero

7

来自维基百科

C#编程语言在2.0版本中增加了对委托的返回类型协变和参数逆变的支持。但是,方法重写不支持协变和逆变。

但它并未明确说明属性的协变性。


8
属性只是两个方法(set_Foo和get_Foo)。 - sehe

2
你可以创建一个通用接口,适用于父类和子类,并返回该接口的类型。

这是一个简短而懒惰的回答。但在这种情况下,它是最简单有效的答案,我喜欢它 :-) +1 - Mendelt
最懒的答案是:由于Child也是Father类型,所以你可以直接返回Child而不改变属性的返回类型。;-) - VVS
或者说,“C#中不存在返回类型协变”。 - VVS

2
这是我目前为止能找到的最接近的内容:
public sealed class JustFather : Father<JustFather> {}

public class Father<T> where T : Father<T>
{ 
    public virtual T SomePropertyName
    { get { return (T) this; }
    }
}

public class Child : Father<Child>
{ 
    public override Child SomePropertyName
    { get { return  this; }
    }
}

没有JustFather类,你不能实例化一个Father<T>除非它是某个其他派生类型。

1

不好意思,C#并不支持这个想法(被称为“返回类型协变性”)。 但是你可以做到这一点:

public class FatherProp
{
}

public class ChildProp: FatherProp
{
}


public class Father
{
    public virtual FatherProp SomePropertyName
    {
        get
        {
            return new FatherProp();
        }
    }
}


public class Child : Father
{
    public override FatherProp SomePropertyName
    {
        get
        {
            // override to return a derived type instead
            return new ChildProp();
        }
    }
}

即使用基类定义的协议,但返回一个派生类型。我已经制作了一个更详细的示例,以使这一点更清晰——再次返回“this”不会改变任何事情。

测试返回的对象实际类型是可能的(但很麻烦),但最好调用它的虚方法,该方法为其类型执行正确的操作。

基类虚方法(在本例中为虚属性)不仅具有实现,而且还定义了一个协议:子类可以提供DifferentPropertyName的另一种实现,如果它满足此协议(即SomePropertyName返回类型为“FatherProp”的对象)。返回从“FatherProp”派生的类型为“ChildProp”的对象符合此协议。但是您无法更改“Child”中的协议-此协议适用于所有从“Father”下降的类。

如果您退后一步,查看更广泛的设计,C#工具包中还有其他语言构造也值得考虑,例如泛型或接口。


不确定是否值得再回答一次,但在C++中,您可以将返回类型更改为返回Child而不是Father,因为返回类型允许在派生类虚函数中变得更加专业化。然而,唯一的原因是因为它们可以用作基本类型,所以这是多余的 :) - workmad3
Java 1.5版本开始支持返回类型协变。 - Jon Skeet

1
不支持此想法(C#中称为"返回类型协变")。根据维基百科的说法,C#编程语言在2.0版本中添加了委托的参数逆变和返回类型协变的支持,但是方法重写不支持协变和逆变。可以重新声明(new),但不能同时重新声明并覆盖(使用相同的名称)。一种选项是使用protected方法隐藏细节-这允许同时进行多态性和隐藏。最好的解决方案是使用泛型。
public class MyClass<T> where T: Person
{
   public virtual T SomePropertyNameA
   {        
      get { return  ...; }    
   }
}//Then the Father and Child are generic versions of the same class

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