如何正确地解释+ =和 – =运算符?
+=
和-=
运算符究竟(底层)是什么?
或者它们是隐含的,它们是按照types定义的?
我已经广泛地使用了它们,这是一个非常简单的语法特征,但是我从来没有想过如何工作的深度。
是什么引起了这个问题
我可以像这样连接一个string值:
var myString = "hello "; myString += "world";
一切都好。 但为什么这不与collections?
var myCol = new List<string>(); myCol += "hi";
你可能会说'你试图附加一个不同的types,你不能附加一个string到一个不是string的types'。 但是下面也不行:
var myCol = new List<string>(); myCol += new List<string>() { "hi" };
好吧,也许它不适用于集合,但是下面不是一个(一种)事件处理程序的集合?
myButton.Click += myButton_Click;
我显然对这些运营商的工作方式缺乏深入的了解。
请注意:我并没有试图用一种真正的项目来构build集合myCol
。 我只是好奇这个运营商的运作,这是假设的。
+=
运算符隐式定义如下: a += b
变成a = a + b;
与-=
运算符相同。
(注意:正如Jeppe指出的那样,如果a
是一个expression式,那么只有在使用a+=b
时才评估一次,而使用a=a+b
则只评估两次)
您不能单独重载+=
和-=
运算符。 任何支持+
运算符的types也支持+=
。 您可以通过重载+
和-
来为自己的types添加对+=
和-=
支持。
然而,有一个例外硬编码到C#,你已经发现:
事件有一个+=
和-=
操作符,它将事件处理程序添加到订阅事件处理程序的列表中,并将其删除。 尽pipe如此,他们不支持+
和-
运营商。
这是不是你可以做自己的类与正常运营商重载。
正如另一个答案所说, +
运算符没有为List<>
定义。 你可以检查它试图重载它,编译器会抛出这个错误One of the parameters of a binary operator must be the containing type
。
但作为一个实验,您可以定义自己的inheritanceList<string>
并定义+
运算符。 像这样的东西:
class StringList : List<string> { public static StringList operator +(StringList lhs, StringList rhs) { lhs.AddRange(rhs.ToArray<string>()); return lhs; } }
那么你可以做到这一点没有问题:
StringList listString = new StringList() { "a", "b", "c" }; StringList listString2 = new StringList() { "d", "e", "f" }; listString += listString2;
编辑
根据@EugeneRyabtsev评论,我的+
运算符的实现将导致意外的行为。 所以应该更像这样:
public static StringList operator +(StringList lhs, StringList rhs) { StringList newList=new StringList(); newList.AddRange(lhs.ToArray<string>()); newList.AddRange(rhs.ToArray<string>()); return newList; }
简单的答案是,C#中的运算符必须重载给定的types。 这也适用于+=
。 string
包含此运算符的重载,但是List<>
没有。 因此,对列表使用+=
运算符是不可能的。 对于代表, +=
操作符也被重载,这就是为什么你可以在事件处理器上使用+=
原因。
稍长的答案是,你可以自由地超载操作员。 然而,你必须为自己的types重载它,所以为List<T>
创build一个重载是不可能的,而你实际上可以为你自己的类做这个,例如inheritance自List<T>
。
从技术上讲,你并不是真的重载+=
操作符,而是+
操作符。 然后通过将+
运算符与一个赋值相结合来推断+=
运算符。 为此, +
运算符应该被重载,使得结果types匹配第一个参数的types,否则当你尝试使用+=
时,C#编译器会抛出一个错误信息。
正确的实现实际上比人们想象的要复杂得多。 首先,仅仅说a += b
与a = a+b
完全相同是不够的。 它在最简单的情况下具有相同的语义,但不是简单的文本replace。
首先,如果左边的expression式比简单的variables更复杂,那么它只被评估一次。 所以M().a += b
与M().a = M().a + b
不一样, M().a = M().a + b
,因为那样会把值赋给一个完全不同于它的对象,或者会导致方法的副作用发生两次。
如果M()
返回一个引用types,则复合赋值运算符可以认为是var obj = M(); obj.a = obj.a+b;
var obj = M(); obj.a = obj.a+b;
(但仍然是一个expression)。 但是,如果obj
是一个值types的话,那么这个简化也不起作用,如果这个方法返回一个引用(C#7中的new),或者实际上是一个数组元素,或者是从一个索引器等返回的东西,那么运算符确保它不会创build比修改对象所需更多的副本,并将其应用到正确的位置,而不会产生额外的副作用。
事件分配是一个完全不同的野兽,虽然。 在类范围外 , +=
导致调用add访问器, -=
导致调用remove事件的访问器。 如果这些访问器不是用户实现的,则事件分配可能导致在类范围内的内部委托对象上调用Delegate.Combine和Delegate.Remove 。 这也是为什么你不能简单地在课堂以外获得事件对象,因为它不是公开的。 +=
/ -=
在这种情况下也不是一个expression式。
我build议阅读Eric Lippert的“ 化合物指定” 。 它更详细地描述了这一点。