8

随着我在我的小数学库上的进步,我偶然发现了 C# 和 .NET Framework 的某些方面,我在理解这些方面有些困难。

这次是运算符重载,特别是术语重载本身。为什么叫重载?默认情况下,所有对象都具有所有运算符的实现吗?也就是说,是:

public static object operator +(object o1, object o2)

在某处以某种方式预定义?如果是这样,为什么如果我尝试o1 + o2我得到编译时错误Operator '+' cannot be applied to operands of type 'object' and 'object'?这会以某种方式暗示默认情况下对象没有预定义的运算符,那么重载一词是怎么来的?

我问这个,因为在我的数学库中,使用 3D 几何元素,我有以下结构:VectorPoint. 现在,在内部,我想允许以下构造创建向量:

static Vector operator -(Point p1, Point p2) {...}

由于这在数学上不是 100% 正确,但在内部对减少代码混乱非常有用,我不想公开这个运算符,所以我最初的意图是简单地做:

internal static Vector operator -(Point p1, Point p2) {...}

令人惊讶的是(对我来说)我得到了以下编译时错误:User-defined operator 'Geometry.operator +(Geometry.Vector, Geometry.Vector)' must be declared static and public“。

现在,所有运算符必须是公共的这种限制似乎对运算符的整个重载方面有一定的意义,但它似乎有点不一致:

  1. 默认情况下,对象没有预定义的运算符:object + object默认情况下或没有事先myTpye + myType明确定义+运算符,我不能这样做。
  2. 定义运算符没有被描述为创建一个运算符,它被描述为重载,它在某种程度上与第 1 点不一致(对我来说)。
  3. 您不能限制 a 的访问修饰符operator,它们必须是public,考虑到第 1 点,这没有意义。但考虑到第 2 点,这是有道理的。

有人可以简单地解释一下我把这一切弄得一团糟吗?

4

3 回答 3

17

为什么叫重载?

它被称为“重载”,因为它重载。当我们为该事物提供两种可能的实现时,我们“重载”了该事物,然后必须决定使用哪个(这称为重载决议)。

当我们重载方法时,我们会给出具有给定名称的方法的两个或多个实现。当我们重载运算符时,我们会为具有给定语法的运算符提供两个或多个可能的实现。这是同一件事。

确保您没有将重载覆盖混淆。重载只是在同一个声明空间中存在两个具有相同名称/语法的方法/运算符。覆盖处理如何在运行时填充虚拟方法槽的内容。

默认情况下,所有对象都具有所有运算符的实现吗?

不。

public static object operator +(object o1, object o2)某处以某种方式预定义?

不。

这会以某种方式暗示默认情况下对象没有预定义的运算符,那么重载一词是怎么来的?

我不明白这个问题。当然 C# 有预定义的运算符。

我不想公开这个运营商

然后不要使用运算符;创建一个私有的、内部的或受保护的方法。

C# 的设计是运算符始终是类型的公共表面区域的一部分。具有取决于使用发生在哪个可访问域中的含义的运算符是非常令人困惑的。C# 被精心设计为“质量坑”语言,语言设计者的选择使您远离编写混乱,有缺陷的,难以重构的程序。要求用户定义的操作符是公共的和静态的是这些微妙的设计点之一。

(1) 对象默认没有预定义的操作符

当然可以;在各种对象上有数百个预定义的运算符。此外,例如,有以下预定义的 operator 重载+

int + int
uint + uint
long + long
ulong + ulong
double + double
float + float
decimal + decimal
enum + underlying  (for any enum type)
underlying + enum
int? + int?
uint? + uint?
long? + long?
ulong? + ulong?
double? + double?
float? + float?
decimal? + decimal?
enum? + underlying?
underlying? + enum?
string + string
object + string
string + object
delegate + delegate (for any delegate type)

有关所有其他运算符的所有预定义重载的列表,请参阅 C# 规范。

请注意,运算符的重载解析有两个阶段:首先,重载解析尝试找到唯一最佳的用户定义重载;仅当这样做找不到适用的候选者时,重载决议才会考虑预定义的重载。

(2) 定义运算符没有被描述为创建一个运算符,它被描述为重载,它在某种程度上与第 1 点不一致(对我来说)。

我不明白你为什么觉得它不一致,或者,就此而言,你觉得不一致。术语“重载”始终用于描述运算符和方法。在这两种情况下,这意味着使用相同的语法来引用两个或多个不同的事物,然后通过“重载解析”解决歧义。方法和算子重载决策算法的具体细节不同,但它们在整体算法中是相似的:首先确定一个候选集,然后删除不适用的候选集,然后一个更好的算法消除比另一个更差的适用候选集,然后是bestness 算法确定剩下的唯一最佳候选者(如果有)。

(3)您不能限制运算符的访问修饰符,它们必须是公共的,考虑到第 1 点,这没有意义。但考虑到第 2 点,有点意义。

我根本不明白第 (3) 点与第 (1) 或 (2) 点有什么关系。运算符必须是公共表面区域的一部分的限制是为了防止当您在课堂内时能够将 a 添加Fruit到 an而不是在课堂内时添加 a 的混乱情况。AnimalAppleGiraffe

运算符在类或结构中声明,因此“属于”所述类型,它们不会浮动“属于”没有给定类型。那么当我在一个类中声明一个运算符时,我究竟重载了什么?

您正在使运算符超载。

int 之间存在相同的运算符并不意味着我正在重载任何属于 int 的运算符。对我来说,这就像说Foo.Hello()andBar.Hello(string hello)是 的重载一样Hello。它们不是以两种不同的类型声明的。与运营商有什么区别?

您刚刚准确地描述了差异。方法重载和操作重载在许多细节上有所不同。

如果你想采取Foo.Hello()Bar.Hello(string)的“重载”的立场Hello,这不是一个常见的立场,但它在逻辑上是一致的。

我的印象是重载时无法更改访问修饰符。

你的印象是错误的;覆盖虚拟方法时不能更改访问修饰符。您已经将其与重载混淆了。

(我注意到有一种情况需要您在覆盖虚拟方法时更改访问修饰符;您能推断出它是什么吗?)

我也有这样的印象,如果没有至少一个操作数是您声明运算符的类型,就不能声明运算符。

这几乎是正确的。用户定义的运算符必须具有 type 的操作数T,其中T是封闭类或结构类型,或者 T?ifT是结构类型。

那么第三类如何能够访问给定的运算符,而另一个第三类却不能,除非一个属于外部程序集而另一个不属于外部程序集,在这种情况下,我根本不觉得它令人困惑甚至有用?

你错误地描述了我的例子,这本来可以更清楚。这是非法的:

public class Fruit 
{ 
    protected static Shape operator +(Fruit f, Animal a) { ... } 
}

因为这很奇怪:

public class Apple : Fruit
{ 
    ...
    Shape shape = this + giraffe; // Legal!
}
public class Giraffe : Animal
{
    ...
    Shape shape = apple + this; // Illegal!
}

这只是一个例子。一般来说,使运算符的重载解析依赖于可访问域是一件奇怪的事情,因此语言设计者通过要求用户定义的运算符是公共的来确保这种情况永远不会发生。

我只是在运算符的上下文中发现重载令人困惑。

很多人都这样做,包括编译器编写者。规范中用户定义的操作符部分极难解析,而且微软的实现是编译器错误的丰富来源,其中很多都是我的错。

我不明白为什么在类型中简单地声明运算符必须与描述声明任何其他静态方法的方式不同。

嗯,不同的东西是不同的;运算符在很多方面都不同于方法,包括它们的重载解析算法。

我从来没有特别喜欢 C# 具有可重载的运算符。C# 功能是 C++ 中相同功能的更好设计版本,但在两种语言中,我认为该功能所需的成本远高于相应的用户收益。

谢天谢地,至少 C# 并没有<<像惯用的 C++ 那样彻底滥用运算符——尽管它当然会滥用+-代表委托。

于 2013-06-18T16:08:11.400 回答
2
  1. 您不能这样做object + object,因为必须以操作数之一的类型声明运算符;Point + Point必须在 中声明Point;你不能定义object + object,因为你没有声明object;请注意,所有对象(至少在语言级别)都有==/!=运算符
  2. 这里唯一重要的区别是您不会将其称为覆盖-不应误认为它们是多态的-它们不是;如果您想将其视为“创建”,那很好,但请记住,在非平凡的情况下,可能存在多个涉及相同类型的重叠运算符,例如使用==/!=运算符,或者运算符继承自非平凡的层次结构
  3. 就是这样; 规范说它们需要public,所以要么制作它们public,要么使用非操作员internal方法
于 2013-06-18T11:41:20.040 回答
2

您可能将术语“重载”与术语“覆盖”混淆了。

这是重载方法的示例Foo

public class Parent
{
    public sealed void Foo(int n) { }
}

public class Child : Parent
{
    public sealed void Foo(string s) { }
}

这是覆盖方法的示例Foo

public class Parent
{
    public virtual int Foo() { return 0; }
}

public class Child : Parent
{
    public override int Foo() { return 1; }
}

如您所见,重载是添加一个全新的签名,该签名将始终独立于以前的现有签名被调用。覆盖涉及提供现有签名的全新实现。

在这种情况下,您可以将 + 运算符视为另一种方法(本质上是,在一些不同的语法下)。如果您要提供现有签名的新实现,比如说operator + (int a, int b)签名,那么您将覆盖该方法。(请注意,在 C# 中无法覆盖运算符或任何静态方法。)您正在做的是通过向加号运算符添加一个新的“签名”来重载它,该运算符采用一组以前没有的操作数存在于加号运算符的任何其他重载。

于 2013-06-18T16:17:32.747 回答