C# 支持用户自定义 struct
和 class
的运算符重载。但是对于自增运算符(operator ++
),有一些细节需要留意。特别是如果按照 C++ 自增运算符重载的思路去理解 C# 的自增运算符重载,那会遇到很多问题。
1. 不要在 class
上定义自增运算。
2. 在 struct
上定义自增运算函数(operator++
)时,必须返回一个比原值大 1 的新 struct
。
用一个例子引入。比如我们定义如下的一个简单的 struct
,来 wrap 一个整数(这样做的目的有多种可能,比如在算术溢出时抛出异常;但这不是本文的重点):
struct MyInteger
{
public int Value { get; private set; }
public MyInteger(int value)
{
this.Value = value;
}
}
如果我们希望像使用寻常 int
变量那样使用 MyInteger
,比如像下面这样使用:
for (MyInteger i = 0; i < 5; i++)
{
// ...
}
那该怎么办呢?自然,我们需要重载 <
运算符(及 >
运算符)和 ++
运算符。本文只探讨如何重载 ++
运算符的问题。
先看看一种“自然”但错误的方式(这种方式和 C++ 里重载后置++
运算符的形式非常相似):
// The following code will NOT work as expected.
public static MyInteger operator++(MyInteger x)
{
MyInteger y = x;
x.Value++;
return y;
}
这段代码实际上是行不通的。比如在前一段循环中,i
永远都是零。
要理解其原因,必须看看 C# 是如何处理 ++
重载的。C# 里的 ++
运算符重载方式与 C++ 不同:在 C++ 中,前置++
和后置 ++
通过不同的函数重载,其行为完全由实现者控制,并且也由实现者来保证其行为符合常规的前置和后置语义。而在 C# 中,++
运算符的重载函数只有一个,这个函数应该做的事情就是返回一个比参数大 1 的值;而前置和后置的语义是由编译器决定的,实现者自己没法修改。
说了这么多废话,其实需要知道的只是下面两条规则:
1. C# 的前置自增(j = ++i
)等价于
i = operator++(i);
j = i;
2. C# 的后置自增(j = i++
)等价于
j = i;
i = operator++(i);
这两条规则对 struct
和 class
都适用。
尽管看上去很简单,但是这与 C++ 中的自增运算有微妙的区别。可以这么说,在 C# 里不存在“自”增这回事;看上去的“自”增,实际上是令被自增的变量等于一个以该变量为参数的函数的值。
进一步,由于 struct
和 class
的特性,前置和后置 ++
的特性也不相同:
struct
1. 为了实现前置自增(j=++i
)语义,operator++
函数必须返回 i+1
的值;但其内部对i
的修改并不能产生任何效果(因为 struct
按值传递)。C# 会保证 j
等于自增后的i
值。
2. 为了实现后置自增(j=i++
)语义,operator++
函数必须返回 i+1
的值;但其内部对i
的修改并不能产生任何效果(因为 struct
按值传递)。C# 会保证 j
等于自增前的i
值。
class
1. 为了实现前置自增(j=++i
)语义,operator++
函数必须返回等价于 i+1
的对象;这个对象可以是新对象,也可以是自增之后的i
本身;而 j
永远和 i
指向同一个对象。
2. 为了实现后置自增(j=i++
)语义,operator++
函数必须返回等价于 i+1
的新对象,因为j
已经指向了 i
自增前的那个对象,如果 operator++
仅仅是修改了 i
的值就直接返回 i
的话,那么 j
的值也会变成了自增以后的值了。
综合上述四种情况可以发现,唯一放之四海而皆准的 operator++
实现方法,就是返回一个新的值(对于 struct
)或者对象(对于 class
)。
比如,在最初的例子当中,正确的(或者说恰当的)operator++
的定义方式是这样的:
// This is the right way to implement ++ on a struct.
public static MyInteger operator++(MyInteger x)
{
return new MyInteger(x.Value + 1);
}
然而,对于类上定义的自增运算,还有一个问题。考虑下面的代码:
// i, j are objects of the same class.
j = i;
++j;
在
++j
之后,
j
的值自然是 +1 了,那么
i
的值应该不变还是 +1 呢?答案并不显然。如果
operator++
函数返回了一个新对象,那么
i
的值不变;如果
operator++
返回了修改后的对象,那么
i
的值会跟着变。而从直观上讲,
++j
应当增加
j
指向的对象呢,还是让
j
另外指向一个比原来大 1 的对象呢?这也没有很自然的答案。因此,最好遵循下面这条规则:
不要在 class
上定义自增运算。而只在 struct
上定义自增运算。