一个常见的API设计问题是无法在重写方法时使用更具体的返回类型。Clone方法就是一个很好的例子。
public abstract Request Clone();
在子类中,你可能会希望像下面这样实现这个方法:
public override FtpRequest Clone() { ... }
由于FtpRequest是Request的子类,从逻辑上讲这是合理的。但在.NET中你不能这样实现,因为重写必须精确匹配。你也不能通过重写获得一个仅有返回类型不同的新方法。所以通常你会得到一些复杂的东西,比如:
public Request Clone() => OnClone();
protected abstract Request OnClone();
然后,在子类里:
public new FtpRequest Clone() => (FtpRequest)OnClone();
protected override Request OnClone() { ... }
提案49在“协变返回类型”中探讨了改变被重写方法返回类型的能力。 在2017年最初提出时,这个特性应该是使用一些“编译器魔法”实现的。到2019年10月,重点已经转向使它成为CLR的一等特性。
在协变返回类型规范草案中,IL指令.override将变成:
重写方法的返回类型必须可以通过标识或隐式引用转换为被重写的基方法的返回类型。
当前的规则是:
重写方法和被重写的基方法应具有相同的返回类型。
属性和索引器包括在此特性中,但仅当它们是只读的。该特性不会对逆变属性和索引设置器提供匹配支持。
接口上的方法可以使用与子类/基类相同的规则重写基接口上的协变方法。
当类实现接口时,实现方法可以与接口方法协变。
为了进行接口映射,类成员A在以下情况下与接口成员B匹配: A和B是方法,A和B的名称和形式参数列表相同,A的返回类型可以通过一个隐式引用标识转换为B的返回类型。
对于隐式实现的接口,这种规则变化可能会导致破坏性更改。这种情况会在子类重新实现基类已经实现的接口时发生。
interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
为了避免破坏性更改,Andy Gocke对规则做了一个小小的修改:
如果没有其他实现(包括默认实现),我们是否可以更改映射成员的搜索,将具有不同协变返回类型的隐式实现考虑进来?
遗憾的是,这与接口的默认实现不兼容。Neal Gafter写到:
我看不出这在二进制兼容的情况下是如何工作的。如果发布了带有默认实现的新版本的接口,那么运行时将更改为使用该接口,而不是使用基类的实现?
微软内部正在跟踪对协变返回类型提供必要的运行时支持的优先级。
原文链接:
领取专属 10元无门槛券
私享最新 技术干货