C# postfix and prefix increment/decrement overloading difference
大多数资料表明,重载C中的+和-运算符会导致同时重载后缀和前缀。但看起来他们的行为还是不同的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class Counter
{
public Counter (int v = 0)
{
this.v = v ;
}
public Counter (Counter c )
{
v = c .v;
}
public int GetValue () { return v ; }
public static Counter operator ++(Counter c )
{
c .v++;
return new Counter (c );
}
private int v ;
}
class Program
{
public static void Main ()
{
Counter c1 = new Counter (1);
Counter c2 = c1 ++;
Counter c3 = ++c1 ;
c3 ++;
System.Console.WriteLine("c1 = {0}", c1 .GetValue());
System.Console.WriteLine("c2 = {0}", c2 .GetValue());
System.Console.WriteLine("c3 = {0}", c3 .GetValue());
}
} |
奇怪的是,虽然重载的operator ++返回原始类的副本,但在本例中,c1和c3成为对同一对象的引用,而c2指向不同的对象(此处为c1=4, c2=2, c3=4)。将Counter c3 = ++c1;改为Counter c3 = c1++;输出c1=3, c2=2, c3=4。
那么,后缀和前缀增量/减量之间的确切区别是什么,以及它如何影响重载?这些运算符对类和基元类型的作用是否相同?
- 您的EDOCX1中有一个bug(0):您不仅返回递增值的对象,而且还修改了应用了该运算符的对象。应该是:public static Counter operator ++(Counter c) { return new Counter(c.v + 1); }。
- 但这就是增量应该做的,i++必须改变i的值,不是吗?
- 不,i++记住变量i的值(我们称之为i_old,计算值i+1(我们称之为i_new),将值i_new存储在变量i中,并返回值i_old。
- i++应该这样做,但++i应该这样做…我猜想C++不像C++那样在调用这些运算符时做一些事情,但是我不能捕捉到它。无论如何,这个例子表明,尽管C只允许为operator ++重载一个方法,但它对递增和递减调用的作用方式不同。
- ++i记住变量i的值(i_old),计算值i+1(i_new,将值i_new存储在变量i中,并返回值i_new。operator ++(x)方法是计算i+1的方法;所有其他步骤都由C编译器负责。所以它实际上是相当一致的。
- 为什么++i要求i_old这个案子?你能解释一下这是一个答案还是参考文献?究竟什么是过载递增/递减运算符的正确方法?为什么?i_old和i_new是对象的引用还是副本?
这是用C实现递增和递减的错误方法。如果你做错了,你会得到疯狂的结果;你做错了,你得到了疯狂的结果,所以系统工作。-)
巧合的是,上周我写了一篇关于这个主题的文章:
http://ericlippert.com/2013/09/25/bug-guys-meets-math-from-scratch/从头开始/
正如评论者DTB所指出的,正确的实现是:
1 2 3 4
| public static Counter operator ++(Counter c )
{
return new Counter (c .v + 1);
} |
在C中,递增运算符不能改变其参数。相反,它必须只计算递增的值并返回它,而不会产生任何副作用。改变变量的副作用将由编译器处理。
有了这个正确的实现,您的程序现在可以这样进行:
1
| Counter c1 = new Counter (1); |
调用c1现在引用的对象W。W.v为1。
其语义如下:
1 2 3
| temp = c1
c1 = operator++(c1) // create object X, set X.v to 2
c2 = temp |
因此,c1现在指X,c2指W。W.v为1,X.v为2。
它的语义是
1 2 3
| temp = operator++(c1) // Create object Y, set Y.v to 3
c1 = temp
c3 = temp |
所以c1和c3现在都指的是对象Y,Y.v是3。
它的语义是
1
| c3 = operator++(c3) // Create object Z, set Z.v to 4 |
所以当烟雾全部散去时:
1 2 3
| c1.v = 3 (Y)
c2.v = 1 (W)
c3.v = 4 (Z) |
以东十一〔三〕是孤儿。
这将产生与使用c1、c2和c3作为普通整数完全相同的结果。
- 在某些方面,operator声明使用++和--作为"加一"和"减一"操作的名称,这太糟糕了;我想知道,如果存在其他无效的令牌序列(如+%表示运算符重载计算的值),那么它是否会或多或少地混淆。o X=Y+%将X设为++Y计算的值,但不回写Y。如果存在这样的操作符,那么+%重载与前缀/后缀操作符之间的关系将比它们与名为++的重载之间的关系更直观。
- @supercat:如果存在这样的操作符,那么用户就不太容易感觉到它必须被用来重载++和--。不过,我同意你所说的,因为当你超载++和---不管怎样,这里都有一些非直觉性。
- @Robh:我不明白,除了特殊的编译器为事件生成"重载"之外,+=和-=操作符是如何被分别重载的+和-所重载的,这种设计我特别不喜欢BTW,尤其是像Delegate这样的类型。应该表现得像价值观。如果evt是一个自动事件,那么声明evt += someDelegate将自动地把someDelegate添加到evt中;它不等于evt = evt + someDelegate;。不幸的是,没有办法重载操作符来让其他委托这样做。
- @supercat:我一点也不介意,+=是通过调用+来实现的,但是我发现在一个事件上,+=意味着"调用事件添加访问器"有点奇怪。我认为,如果C/clr团队不得不在我们现在所知道的好处下重新开始,那么事件会简单得多。
- @Ericlippert:我不太喜欢.NET事件,但我关于+=的观点是,在语义上,操作员有时在某些东西上表现为就地操作(例如,通过Interlocked保护的更新循环)是有意义的,而不是作为一个读操作,然后再进行一次写操作。如果属性可以返回标记为"短暂"的类型(但可以允许隐式转换为普通类型),这样myThing.someProperty += something;可能导致myThing被告知附加something,而不必……
- …读取所有与EDOCX1相关的数据(25),修改它,然后全部写回。如果一个对象只有一个语义上有意义的引用,那么在语义上更新它就相当于生成一个新的修改版本,但通常速度要快得多。