将方法引入通用基础 class 是一项重大更改
Is pulling up a method into a generic base class a breaking change
我有一个当前结构如下的库:
class Base<T>
{
}
class Derived : Base<int>
{
public int Foo();
}
class AnotherDerived : Base<string>
{
}
我想添加一个类似的 Foo()
到 AnotherDerived
并且由于两者的实现是相同的,我想将 Foo()
拉到它们的共享父 class Base
如下所示。
class Base<T>
{
public T Foo();
}
class Derived : Base<int>
{
}
class AnotherDerived : Base<string>
{
}
这样做会将 Foo()
的 return 类型从 int
更改为 T
。
更改成员的 return 类型是 listed 的重大更改。
但是由于 Derived
在 Base<T>
和 T=int
中是通用的,代码仍然可以编译,至少对于这个例子是这样。
int foo = myDerived.Foo();
我不想向我的用户介绍重大更改,所以我的问题是这种上拉是否以任何方式被视为重大更改?
搬进基地本身不应该是breaking change
Moving a method onto a class higher in the hierarchy tree of the type from which it was removed
启用代码共享且不应该是破坏性更改的替代实现,但如果上拉是非破坏性的,我想避免它,可能是:
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();
}
如果这就是您所做的全部,那么这不是重大更改。任何依赖于旧设计的客户端代码都不可能被破坏。
第二个版本,我不太确定。您将隐藏基本方法(顺便说一句,您应该使用 new
关键字以使您的隐藏意图更清楚),这会改变方法解析在运行时发生的方式。它可能很好(尽管真的很难看),也可能会把事情搞砸。
编辑:实际上,我只是想到了一种可以破坏客户端代码的方式,但这不应该被视为重大更改,更像是一种好奇心,除非使用反射在你的 dll 上是可以预料的:任何使用 System.Reflection.TypeInfo DeclaredMethods
属性 或 GetDeclaredMethods()
方法的代码都可能被破坏。
无论如何,使用反射 的客户端代码应该 重新编译并可能在更新引用的库时修复。
I don't want to introduce breaking changes to my users
真聪明。
my question is whether this pull up in any way be considered a breaking change?
你问过它是否可以以任何方式成为一个突破性的变化,答案是肯定的,有一些方式可以可以是一个突破性的变化。但是,它可能成为重大更改的方式是 模糊 并且 不太可能成为 real-world 代码中的一个因素.
让我们首先阐明重大变更 的含义。出于我们的目的,假设有两个软件包,Alpha 1 和 Bravo 1,其中 Alpha 是 Bravo 的依赖项。问题是:假设我们有 Alpha 2 并且我们在没有更改的情况下重新编译 Bravo,但是针对 Alpha 2。如果 Bravo 不再编译,或者更糟,编译但改变了它的行为,那么对 Alpha 的更改据说是 breaking 太棒了。
接下来的问题是,我们能否想出一个 Bravo 来使用您的两个不同的 class 库,但不能在后一个版本中编译?我假设您所有的 classes 和成员都是 public
。另外,我们不需要基数 class 是通用的,所以让我们简化一下:
// 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 中发生了什么?我们有两个选择:x
是 Base
或 x
是 Derived
。前者是不可能的,因为Base
没有Foo
。因此 x
是明确的 Derived
并且程序可以编译。
但是对于 v2,我们有两个选择:要么 x
是 Base
,要么 x
是 Derived
,两者都有 Foo
,所以我们不能明确地说出 M
是什么意思。因此,我们回到决胜局回合,但我们没有理由更喜欢 Action<Base, Derived>
而不是 Action<Derived, Base>
,反之亦然。 Bravo 编译失败,出现歧义错误。
EXERCISE:构造一个 class Bravo,它可以同时使用 Alpha 1 和 Alpha 2 进行编译,但是重载解析选择 不同的 每个方法。这是更微妙的突破性变化。 (尽管我们希望改变调用哪个方法不会对程序的意义造成太大的改变;如果在同一个 class 上有两个同名的方法却做了截然不同的事情,那就太奇怪了!)
当我们将 lambda 添加到 C# 3 时,我在 Mads 的办公室开了很多长时间的会议来讨论这种情况,我们认为值得将这些 rare-in-practice 重大更改场景添加到语言中以换取强大的功能带有 lambda 类型推断。
更新:还有其他方法可以将方法移动到基 class 可以导致行为发生变化,但不涉及奇怪的 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
这有点令人惊讶,但请记住,我们这里有一个带有 三个 个参数的调用:this
是 new D()
,x
和 y
都是 1
。我们选择的重载是一种方法,它采用 D
表示 this
,采用 int
表示 x
,采用 short
表示 y
,以及一种方法this
需要 B
,x
需要 int
,y
需要 int
。
在 C# 中,如果文字 int
在短整型范围内,它会自动转换为 short
。那么问题来了:哪个更重要?我们将接收者与 this
紧密匹配,或者我们将文字的 compile-time 类型与 y
的类型紧密匹配? C# 决定接收者总是比任何参数都更重要,因此选择 more-derived 方法。
如果我们然后将方法移动到基础中 class:
// 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
现在编译器对两种方法的接收者类型没有区别,所以不再是决胜局。现在唯一可用的决胜局是 "should we think of 1
as more like int
or short
?",答案是 int
,所以这会打印 1
.
另一项更新:
发帖人已澄清,该问题旨在针对更新版本中方法泛化的问题专门征求意见;没看懂强调的是泛型,反而把注意力集中在了不管泛型与否,方法都在向基class移动
将 non-generic 方法变成通用方法也存在一些潜在问题,或者像本例一样,以保留其签名但添加通用替换步骤的方式更改方法所以。但是正如我在这个答案的前面部分指出的那样,这些问题在 real-world 代码中应该非常罕见。
C# 有一个 last-chance 决胜局规则,它说如果在重载决策期间你有两个方法 在它们的参数类型 上完全相同,但是其中一个方法通过泛型类型替换获得了它的签名,而另一个没有,那么 "natural" 方法获胜。理论上,打破规则可以导致提议的重构发生重大变化,但这种情况在实际代码中极为罕见。我试过了,但无法轻易地产生一个场景你的重构会 运行 解决这个问题。不过,我会继续研究它,如果我有任何合理的想法,我会添加另一个更新。
我有一个当前结构如下的库:
class Base<T>
{
}
class Derived : Base<int>
{
public int Foo();
}
class AnotherDerived : Base<string>
{
}
我想添加一个类似的 Foo()
到 AnotherDerived
并且由于两者的实现是相同的,我想将 Foo()
拉到它们的共享父 class Base
如下所示。
class Base<T>
{
public T Foo();
}
class Derived : Base<int>
{
}
class AnotherDerived : Base<string>
{
}
这样做会将 Foo()
的 return 类型从 int
更改为 T
。
更改成员的 return 类型是 listed 的重大更改。
但是由于 Derived
在 Base<T>
和 T=int
中是通用的,代码仍然可以编译,至少对于这个例子是这样。
int foo = myDerived.Foo();
我不想向我的用户介绍重大更改,所以我的问题是这种上拉是否以任何方式被视为重大更改? 搬进基地本身不应该是breaking change
Moving a method onto a class higher in the hierarchy tree of the type from which it was removed
启用代码共享且不应该是破坏性更改的替代实现,但如果上拉是非破坏性的,我想避免它,可能是:
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();
}
如果这就是您所做的全部,那么这不是重大更改。任何依赖于旧设计的客户端代码都不可能被破坏。
第二个版本,我不太确定。您将隐藏基本方法(顺便说一句,您应该使用 new
关键字以使您的隐藏意图更清楚),这会改变方法解析在运行时发生的方式。它可能很好(尽管真的很难看),也可能会把事情搞砸。
编辑:实际上,我只是想到了一种可以破坏客户端代码的方式,但这不应该被视为重大更改,更像是一种好奇心,除非使用反射在你的 dll 上是可以预料的:任何使用 System.Reflection.TypeInfo DeclaredMethods
属性 或 GetDeclaredMethods()
方法的代码都可能被破坏。
无论如何,使用反射 的客户端代码应该 重新编译并可能在更新引用的库时修复。
I don't want to introduce breaking changes to my users
真聪明。
my question is whether this pull up in any way be considered a breaking change?
你问过它是否可以以任何方式成为一个突破性的变化,答案是肯定的,有一些方式可以可以是一个突破性的变化。但是,它可能成为重大更改的方式是 模糊 并且 不太可能成为 real-world 代码中的一个因素.
让我们首先阐明重大变更 的含义。出于我们的目的,假设有两个软件包,Alpha 1 和 Bravo 1,其中 Alpha 是 Bravo 的依赖项。问题是:假设我们有 Alpha 2 并且我们在没有更改的情况下重新编译 Bravo,但是针对 Alpha 2。如果 Bravo 不再编译,或者更糟,编译但改变了它的行为,那么对 Alpha 的更改据说是 breaking 太棒了。
接下来的问题是,我们能否想出一个 Bravo 来使用您的两个不同的 class 库,但不能在后一个版本中编译?我假设您所有的 classes 和成员都是 public
。另外,我们不需要基数 class 是通用的,所以让我们简化一下:
// 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 中发生了什么?我们有两个选择:x
是 Base
或 x
是 Derived
。前者是不可能的,因为Base
没有Foo
。因此 x
是明确的 Derived
并且程序可以编译。
但是对于 v2,我们有两个选择:要么 x
是 Base
,要么 x
是 Derived
,两者都有 Foo
,所以我们不能明确地说出 M
是什么意思。因此,我们回到决胜局回合,但我们没有理由更喜欢 Action<Base, Derived>
而不是 Action<Derived, Base>
,反之亦然。 Bravo 编译失败,出现歧义错误。
EXERCISE:构造一个 class Bravo,它可以同时使用 Alpha 1 和 Alpha 2 进行编译,但是重载解析选择 不同的 每个方法。这是更微妙的突破性变化。 (尽管我们希望改变调用哪个方法不会对程序的意义造成太大的改变;如果在同一个 class 上有两个同名的方法却做了截然不同的事情,那就太奇怪了!)
当我们将 lambda 添加到 C# 3 时,我在 Mads 的办公室开了很多长时间的会议来讨论这种情况,我们认为值得将这些 rare-in-practice 重大更改场景添加到语言中以换取强大的功能带有 lambda 类型推断。
更新:还有其他方法可以将方法移动到基 class 可以导致行为发生变化,但不涉及奇怪的 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
这有点令人惊讶,但请记住,我们这里有一个带有 三个 个参数的调用:this
是 new D()
,x
和 y
都是 1
。我们选择的重载是一种方法,它采用 D
表示 this
,采用 int
表示 x
,采用 short
表示 y
,以及一种方法this
需要 B
,x
需要 int
,y
需要 int
。
在 C# 中,如果文字 int
在短整型范围内,它会自动转换为 short
。那么问题来了:哪个更重要?我们将接收者与 this
紧密匹配,或者我们将文字的 compile-time 类型与 y
的类型紧密匹配? C# 决定接收者总是比任何参数都更重要,因此选择 more-derived 方法。
如果我们然后将方法移动到基础中 class:
// 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
现在编译器对两种方法的接收者类型没有区别,所以不再是决胜局。现在唯一可用的决胜局是 "should we think of 1
as more like int
or short
?",答案是 int
,所以这会打印 1
.
另一项更新:
发帖人已澄清,该问题旨在针对更新版本中方法泛化的问题专门征求意见;没看懂强调的是泛型,反而把注意力集中在了不管泛型与否,方法都在向基class移动
将 non-generic 方法变成通用方法也存在一些潜在问题,或者像本例一样,以保留其签名但添加通用替换步骤的方式更改方法所以。但是正如我在这个答案的前面部分指出的那样,这些问题在 real-world 代码中应该非常罕见。
C# 有一个 last-chance 决胜局规则,它说如果在重载决策期间你有两个方法 在它们的参数类型 上完全相同,但是其中一个方法通过泛型类型替换获得了它的签名,而另一个没有,那么 "natural" 方法获胜。理论上,打破规则可以导致提议的重构发生重大变化,但这种情况在实际代码中极为罕见。我试过了,但无法轻易地产生一个场景你的重构会 运行 解决这个问题。不过,我会继续研究它,如果我有任何合理的想法,我会添加另一个更新。