C#支持返回类型协变吗?

98

我正在使用.NET框架,我真的想能够制作一个自定义类型的页面,让我的整个网站都使用它。但当我尝试从控件中访问该页面时出现问题。我希望能够返回我的特定类型的页面而不是默认页面。有没有办法做到这一点?

public class MyPage : Page
{
    // My own logic
}

public class MyControl : Control
{
    public MyPage Page { get; set; }
}

可能是c#协变返回类型利用泛型的重复问题。 - nawfal
9个回答

188

更新:此答案编写于2011年。经过20年的人们提议将C#引入返回类型协变,现在已经实现了。请参见https://devblogs.microsoft.com/dotnet/c-9-0-on-the-record/.


看起来你想要的是返回类型协变(return type covariance)。但是,C#不支持返回类型协变。

返回类型协变是指覆盖一个基类方法,该方法返回一个较不具体的类型,而使用一个返回更具体类型的方法:

abstract class Enclosure
{
    public abstract Animal Contents();
}
class Aquarium : Enclosure
{
    public override Fish Contents() { ... }
}

这样是安全的,因为通过Enclosure消费Contents的用户期望一个动物,而Aquarium承诺不仅满足该要求,而且还做出更严格的承诺:该动物始终是一条鱼。

C#不支持这种协变,并且很可能永远不会支持。 CLR也不支持它。(C ++以及CLR上的C ++ / CLI实现支持它;它通过生成建议下面的类似神奇的辅助方法来支持。)

(一些语言也支持形式参数类型逆变——您可以用接受Fish的方法覆盖接受Animal的方法。同样,合同得到履行;基类要求处理任何Fish,派生类承诺不仅处理Fish,而且处理任何动物。类似地,C#和CLR不支持形式参数类型逆变。)

您可以通过执行以下操作来解决此限制:

abstract class Enclosure
{
    protected abstract Animal GetContents();
    public Animal Contents() { return this.GetContents(); }
}
class Aquarium : Enclosure
{
    protected override Animal GetContents() { return this.Contents(); }
    public new Fish Contents() { ... }
}

现在你既可以重写虚方法带来的好处,又能在使用编译时类型为Aquarium的东西时获得更强的类型安全性。


4
有没有任何地方可以阅读不支持返回协变和参数逆变的理由? - porges
3
@Porges:我们不需要为支持某些功能提供理由。功能是昂贵的,而且默认情况下不会被实现。只有当功能被考虑、设计、规定、实现、测试并交付给客户后,它们才会被实现。 - Eric Lippert
27
作为C#的粉丝,我总是很好奇它的“后台”,所以我必须问一下:是否曾考虑过为C#引入返回类型协变,但认为投资回报率太低而放弃了?我理解时间和预算的限制,但从你的“不太可能得到支持”的说法中可以看出,设计团队实际上更倾向于不将其纳入语言中。另一方面,Java在Java 5中引入了这个语言特性,因此我想知道在支配语言设计过程的更广泛、更高级原则方面有什么区别。 - Cristian Diaconescu
7
@Cyral:正确,它在“适度关注”的列表中——它已经在那里很长时间了!它可能会发生,但我非常怀疑。 - Eric Lippert
3
这个功能可悲地没有被添加到C# 7中。但仍然有一个问题跟踪它:https://github.com/dotnet/roslyn/issues/357 - Søren Boisen
显示剩余10条评论

10

在接口方面,我通过显式实现接口来解决它:

public interface IFoo {
  IBar Bar { get; }
}
public class Foo : IFoo {
  Bar Bar { get; set; }
  IBar IFoo.Bar => Bar;
}

4
这是一个关于即将发布的C# 9.0 (.Net 5)的功能,您现在可以下载预览版
以下代码现在可以成功构建(不会出现错误提示:error CS0508: 'Tiger.GetFood()': return type must be 'Food' to match overridden member 'Animal.GetFood()')。
class Food { }
class Meat : Food { }

abstract class Animal {
    public abstract Food GetFood();
}

class Tiger : Animal {
    public override Meat GetFood() => default;
}

class Program {
    static void Main() => new Tiger();
}

2
将此放置在MyControl对象中将起作用:
 public new MyPage Page {get return (MyPage)Page; set;}'

你无法覆盖该属性,因为它返回了不同的类型...但你可以重新定义它。

在这个例子中,你不需要协变,因为它相对简单。你所做的就是从MyPage继承基本对象Page。任何想要返回MyPage而不是PageControl都需要重新定义ControlPage属性。


1

是的,它支持协变性,但这取决于您要实现的确切目标。

我也倾向于经常使用泛型,这意味着当您执行以下操作时:

class X<T> {
    T doSomething() {
    }

}

class Y : X<Y> {
    Y doSomethingElse() {
    }
}

var Y y = new Y();
y = y.doSomething().doSomethingElse();

不要“丢失”你的类型。


0

您可以通过沿着父级树向上遍历来从任何控件访问您的页面。

myParent = this;

while(myParent.parent != null)
  myParent = myParent.parent;

*未编译或测试。

或在当前上下文中获取父页面(取决于您的版本)。


然后我喜欢做的是:创建一个接口,包含我想在控件中使用的函数(例如IHostingPage)。
然后我将父页面强制转换为'IHostingPage host = (IHostingPage)Parent;',这样我就可以从我的控件中调用所需页面上的函数了。

0
我没有尝试过,但这不会起作用吗?
YourPageType myPage = (YourPageType)yourControl.Page;

2
是的,确实如此。我只想尽量避免到处进行强制转换。 - Kyle Sletten

0
是的。有多种方法可以做到这一点,以下只是其中一种选择:
您可以使您的页面实现一些自定义接口,该接口公开了一个名为“GetContext”或类似名称的方法,它返回您特定的信息。然后,您的控件可以简单地请求该页面并进行类型转换:
var myContextPage = this.Page as IMyContextGetter;

if(myContextPage != null)
   var myContext = myContextPage.GetContext();

然后您可以随意使用该上下文。

0
我会这样做:
class R {
    public int A { get; set; }
}

class R1: R {
    public int B { get; set; }
}

class A
{        
    public R X { get; set; }
}

class B : A 
{
    private R1 _x;
    public new R1 X { get => _x; set { ((A)this).X = value; _x = value; } }
}

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