对运算符重载感到困惑

时间:2013-06-18 11:31:26

标签: c# operator-overloading

随着我在小数学库上的进步,我绊倒了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 + objectmyTpye + myType未事先明确定义+运算符。
  2. 定义运算符不会被描述为创建运算符,它被描述为重载,它与点1在某种程度上是不一致的(对我而言)。
  3. 您不能限制operator的访问修饰符,它们必须是public,考虑到第1点,这是没有意义的。但考虑到第2点,这有点意义。
  4. 有人可以用简单的术语解释我所做的这些混乱吗?

3 个答案:

答案 0 :(得分:16)

  

为什么称为重载?

它被称为"重载"因为它 重载。我们过载"当我们为该事物提供两个可能的实现然后必须决定使用哪个(称为重载分辨率)时。

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

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

  

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

没有

  

public static object operator +(object o1, object o2)是否在某处预定义了?

没有

  

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

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

  

我不想公开宣传此运营商

然后不要使用操作员;制作私人,内部或受保护的方法。

C#的设计是运营商始终是某类公共表面区域的一部分。让运营商具有的意义取决于使用的可访问性域是非常令人困惑的.C#经过精心设计,是一个质量上乘的网站。语言,语言设计者的选择使您远离编写混乱,错误,难以重构的程序。要求用户定义的运算符是公共的和静态的是那些微妙的设计点之一。

  

(1)默认情况下,对象没有预定义的运算符

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

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在某种程度上是不一致的(对我而言)。

我不明白为什么你发现它不一致,或者就此而言,你发现不一致的东西。术语"过载"用于描述运算符和方法;在这两种情况下,它意味着使用相同的语法来引用两个或更多不同的东西,然后通过"重载解析"来解决歧义。方法和运算符重载决策算法的确切细节是不同的,但它们在整个算法中是相似的:首先识别候选集,然后删除不适用的候选者,然后更好的算法消除比另一个更差的适用候选者,然后a最佳性算法确定剩下的唯一最佳候选者(如果有的话)。

  

(3)你不能限制运营商的访问修饰符,它们必须是公开的,考虑到第1点,这没有意义。但考虑到第2点,这是有道理的。

我不明白第(3)点与第(1)或(2)点有什么关系。操作员必须成为公共表面区域一部分的限制是为了防止当您在班级Fruit内时能够向Animal添加Apple这一令人困惑的情况,但不是当你在课堂Giraffe内时。

  

运算符在类或结构中声明,因此"属于"对于这种类型,他们不会漂浮"属于"没有给定的类型。那么当我在一个类中声明一个运算符时,我的重载是什么呢?

您重载了运营商。

  

在int之间存在相同的运算符并不意味着我正在重载任何东西,因为该运算符属于int。对我而言,Foo.Hello()Bar.Hello(string hello)的重载与Hello的重载相同。它们不是以两种不同的类型声明它们。与运营商有什么区别?

您只是准确地描述了这种差异。方法重载和操作重载的许多细节都不同。

如果您想采取Foo.Hello()Bar.Hello(string)是"重载"的位置? Hello的{​​{1}},这不是一个共同的立场,但它在逻辑上是一致的。

  

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

你的印象错了; 覆盖虚拟方法时,无法更改访问修饰符。您对重载感到困惑。

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

  

我的印象是,如果没有至少一个操作数属于您声明操作符的类型,则无法声明操作符。

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

  

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

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

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!
}

这只是一个例子。一般来说,做一个奇怪的事情是使运算符的重载决策取决于可访问性域,因此语言设计者确保通过要求用户不会发生这种情况 - 定义的运营商是公开的。

  

我只是在运营商的背景下发现过载令人困惑。

许多人都这样做,包括编译器编写者。规范的用户定义的运算符部分非常难以解析,并且Microsoft实现是编译器错误的丰富来源,其中许多是我的错。

  

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

嗯,不同的事情是不同的;运算符在很多方面都与方法不同,包括它们的重载决策算法。

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

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

答案 1 :(得分:2)

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

答案 2 :(得分: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#中不可能覆盖运算符或任何静态方法。)你正在做的是通过向plus运算符添加一个新的“签名”来重载它,该运算符采用之前没有的一组操作数存在于加号运算符的任何其他重载。