首页 > 解决方案 > 将方法拉入通用基类是一项重大更改

问题描述

我有一个图书馆目前的结构是这样的:

class Base<T> 
{
}

class Derived : Base<int>
{
    public int Foo();
}

class AnotherDerived : Base<string>
{
}

我想添加一个类似Foo()的,AnotherDerived并且由于两者的实现是相同的,我想像下面一样拉Foo()入他们的共享父类Base

class Base<T> 
{
    public T Foo();
}

class Derived : Base<int>
{
}

class AnotherDerived : Base<string>
{
}

这样做会改变Foo()from intto的返回类型T。更改成员的返回类型被列为重大更改。但是由于Derived是通用的Base<T>T=int代码仍然可以编译,至少对于这个例子来说。

int foo = myDerived.Foo();

我不想向我的用户介绍重大更改,所以我的问题是这种拉升是否以任何方式被视为重大更改?搬进基地本身不应该是一个突破性的变化

将方法移动到层次结构树中更高层的类中,该类从该类中移除

启用代码共享并且不应该是重大更改的替代实现可能是:

class Base<T> 
{
    protected T Foo();
}

class Derived : Base<int>
{
    public new int Foo() => base.Foo();
}

class AnotherDerived : Base<string>
{
    public new string Foo() => base.Foo();
}

标签: c#api-design

解决方案


我不想向我的用户介绍重大更改

这很聪明。

我的问题是,这种拉升是否以任何方式被视为重大变化?

你问它是否可以以任何方式成为突破性的变化,答案是肯定的,在某些方面它可能是一个突破性的变化。然而,它可能是一个重大变化的方式是模糊的不太可能成为现实世界代码中的一个因素

让我们首先澄清一下我们所说的重大变更是什么意思。出于我们的目的,假设有两个软件包,Alpha 1 和 Bravo 1,其中 Alpha 是 Bravo 的依赖项。问题是:假设我们有 Alpha 2 并且我们重新编译 Bravo 而不做任何更改,但是针对 Alpha 2。如果 Bravo 不再编译,或者更糟糕的是,编译但改变了它的行为,那么对 Alpha 的更改据说破坏了Bravo。

那么问题是,我们能否想出一个使用您的两个不同类库但不能在后一个版本中编译的 Bravo?我假设你所有的类和成员都是public. 此外,我们不需要基类是通用的,所以让我们简化一下:

// Alpha 1
public class Base { }
public class Derived : Base { public int Foo() => 0; }

// Alpha 2
public class Base { public int Foo() => 0;}
public class Derived : Base { }

这是一个用 v1 但不是 v2 编译的 Bravo:

class Bravo
{
  static void M(Action<Base, Derived> a) {}
  static void M(Action<Derived, Base> a) {}
  static void Main()
  {
    M((x, y) => { x.Foo(); });
  }
}

v1 会发生什么?我们有两个选择:要么xisBase要么xis Derived。前者是不可能的,因为Base没有Foo。因此x是明确Derived和程序编译。

但是对于 v2,我们有两个选择:要么xisBase要么xisDerived和 both have Foo,所以我们不能明确地说出M是什么意思。因此,我们退回到决胜局,但我们没有理由更喜欢Action<Base, Derived>Action<Derived, Base>反之亦然。Bravo 编译失败,出现歧义错误。


EXERCISE:构造一个 Bravo 类,它可以同时使用 Alpha 1 和 Alpha 2 进行编译,但重载解决方案为每个选择不同的方法。这是更微妙的突破性变化。(虽然我们希望改变调用哪个方法不会改变程序的意义;如果在同一个类上有两个同名的方法做了非常不同的事情,那就太奇怪了!)


当我们将 lambdas 添加到 C# 3 时,我在 Mads 的办公室举行了多次长时间的会议来讨论这个场景,我们认为值得将这些实践中罕见的重大更改场景添加到语言中,以换取类型推断带来的强大功能拉姆达斯。


更新:还有其他方法可以将方法移动到基类可以导致不涉及奇怪的 lambda 绑定的行为变化。例如,考虑:

// version 1
class B 
{
  public void M(int x, int y) { Console.WriteLine(1); }
}
class D : B 
{
  public void M(int x, short y) { Console.WriteLine(2); }
}
...
new D().M(1, 1); // 2

这有点令人惊讶,但请记住,我们这里有一个带有三个参数的调用:isthisnew D()are both 。我们对重载的选择是采用for 、for和for的方法,以及采用for 、for和for的方法。 xy1DthisintxshortyBthisintxinty

在 C# 中,如果文字在 short 范围内,int则会自动转换为。short那么问题来了:哪个更重要?我们将接收者与 紧密匹配this,或者我们将文字的编译时类型与y? C# 决定接收者总是比任何参数更重要,因此选择派生更多的方法。

如果我们然后将该方法移动到基类中:

// version 2
class B 
{
  public void M(int x, int y) { Console.WriteLine(1); }
  public void M(int x, short y) { Console.WriteLine(2); }
}
class D : B  { }
...
new D().M(1, 1); // 1

现在编译器在两种方法的接收器类型上没有区别,因此不再是决胜局。现在唯一可用的决胜局是“我们应该认为1更像int还是short?” 答案是int,所以打印出来1


另一个更新:

原发帖人已澄清,该问题旨在专门征求关于该方法在更新版本中变得通用这一事实的意见;我不明白重点是在泛型上,而是集中在该方法正在移动到基类的事实上,而不管它是否是泛型的。

将非泛型方法变为泛型方法也存在一些潜在问题,或者在这种情况下,以保留其签名但添加泛型替换步骤的方式更改方法。但正如我在这个答案的前面部分所指出的,这些问题在现实世界的代码中应该是非常罕见的。

C# 有一个最后机会的决胜局规则,如果在重载决议期间,您有两个参数类型完全相同的方法,但其中一个方法通过泛型类型替换获得其签名,而另一个没有,那么“自然”方法获胜。理论上,该规则可以通过提议的重构导致重大更改,但这种情况在实际代码中极为罕见。我试过了,但不能轻易地产生一个你的重构会遇到这个问题的场景。不过我会继续玩它,如果我想出任何合理的东西,我会添加另一个更新。


推荐阅读