将方法提取到通用基类中是一项重大更改

Jon*_*rup 2 c# api-design

我有一个当前结构为这样的库:

class Base<T> 
{
}

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

class AnotherDerived : Base<string>
{
}

Run Code Online (Sandbox Code Playgroud)

我想类似的添加Foo(),以AnotherDerived和为实现将是相同的两个,我想拉起Foo()到他们共享的父类Base像下面。

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

class Derived : Base<int>
{
}

class AnotherDerived : Base<string>
{
}
Run Code Online (Sandbox Code Playgroud)

否则会改变返回类型Foo()intT。更改成员的返回类型被列为重大更改。但是既然Derived是泛型的Base<T>T=int代码还是可以编译的,至少对于这个例子是这样。

int foo = myDerived.Foo();
Run Code Online (Sandbox Code Playgroud)

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

将方法移到其被移除类型的层次结构树中更高的类上

启用代码共享并且不应该是破坏性更改的替代实现,但如果上拉是非破坏性的,我想避免这种实现,可能是:

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();
}

Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 5

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

这很聪明。

我的问题是这是否以任何方式被认为是一个突破性的变化?

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

让我们首先澄清我们所说的重大更改的含义。出于我们的目的,假设有两个软件包,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 { }
Run Code Online (Sandbox Code Playgroud)

这是一个使用 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(); });
  }
}
Run Code Online (Sandbox Code Playgroud)

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

但是对于 v2,我们有两个选择:要么xBase要么xDerived,两者都有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
Run Code Online (Sandbox Code Playgroud)

这是一个令人感到有点惊讶,但请记住,我们在这里的是具有通话3个参数:thisnew D()x而且y1。我们选择的重载是一个接受Dfor thisintforxshortfory的方法,以及一个接受Bfor thisintforxintfor 的方法y

在 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
Run Code Online (Sandbox Code Playgroud)

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


另一个更新:

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

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

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

  • @LucaCremonesi:好问题。许多人在打开“警告是错误”的情况下进行编译,因此有人认为引入新警告是一个重大变化。但在这种情况下,您应该为自己获得了休息而感到高兴,因为警告可能告诉您一些重要的事情! (2认同)