C#中关于`+=`的运算符重载?

131
我试图对+=进行运算符重载,但是我不能。我只能对+进行运算符重载。 为什么呢?
编辑 这不起作用的原因是我有一个向量类(带有X和Y字段)。考虑以下示例。
vector1 += vector2;

如果我的运算符重载被设置为:

public static Vector operator +(Vector left, Vector right)
{
    return new Vector(right.x + left.x, right.y + left.y);
}

那么结果将不会被添加到vector1中,相反,vector1也将成为一个全新的向量。


2
看起来已经有一个长时间的讨论关于这个问题:http://maurits.wordpress.com/2006/11/27/c-operator-overloading-where-is-my/ - Chris S
39
为什么你想要这样做?当你重载"+"时,你会免费得到重载"+="的效果。是否存在这样的情况:你希望重载"+=",但不希望重载"+"? - Eric Lippert
3
从C++来看,这种做法似乎不太对,但在C#中却非常合理。 - Jon Purdy
13
@Mathias: 针对你的更新:向量应该表现得像不可变的数学对象。当你将2加3时,你不会将对象3改变为对象5,而是创建一个全新的对象5。重载加法运算符的目的是制作自己的数学对象;使它们可变会适得其反。我建议把你的向量类型设置为不可变值类型。 - Eric Lippert
显示剩余2条评论
10个回答

160

可重载的运算符,来自微软MSDN:

赋值运算符不能被重载,但是例如+=会使用可以被重载的+进行计算。

此外,所有的赋值运算符都不能被重载。我认为这是因为在CLR强类型的世界中,这将导致垃圾回收和内存管理方面的影响,这可能是一个潜在的安全漏洞。

不过,让我们看看运算符到底是什么。根据著名的Jeffrey Richter的书所说,每种编程语言都有其自己的操作符列表,它们被编译成特殊的方法调用,而CLR本身对运算符一无所知。那么让我们看看++=运算符背后到底发生了什么。

请看下面这段简单的代码:

Decimal d = 10M;
d = d + 10M;
Console.WriteLine(d);

让我们查看这些指令的IL代码:

  IL_0000:  nop
  IL_0001:  ldc.i4.s   10
  IL_0003:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32)
  IL_0008:  stloc.0
  IL_0009:  ldloc.0
  IL_000a:  ldc.i4.s   10
  IL_000c:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32)
  IL_0011:  call       valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Addition(valuetype [mscorlib]System.Decimal,
                                                                                                valuetype [mscorlib]System.Decimal)
  IL_0016:  stloc.0

现在让我们看一下这段代码:

Decimal d1 = 10M;
d1 += 10M;
Console.WriteLine(d1);

并且这是该代码的 IL 代码:

  IL_0000:  nop
  IL_0001:  ldc.i4.s   10
  IL_0003:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32)
  IL_0008:  stloc.0
  IL_0009:  ldloc.0
  IL_000a:  ldc.i4.s   10
  IL_000c:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32)
  IL_0011:  call       valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Addition(valuetype [mscorlib]System.Decimal,
                                                                                                valuetype [mscorlib]System.Decimal)
  IL_0016:  stloc.0

它们是相等的!所以在 C# 中,+= 操作符只是语法糖,你可以简单地重载 + 操作符。

例如:

class Foo
{
    private int c1;

    public Foo(int c11)
    {
        c1 = c11;
    }

    public static Foo operator +(Foo c1, Foo x)
    {
        return new Foo(c1.c1 + x.c1);
    }
}

static void Main(string[] args)
{
    Foo d1 =  new Foo (10);
    Foo d2 = new Foo(11);
    d2 += d1;
}

这段代码将被编译并成功运行:

  IL_0000:  nop
  IL_0001:  ldc.i4.s   10
  IL_0003:  newobj     instance void ConsoleApplication2.Program/Foo::.ctor(int32)
  IL_0008:  stloc.0
  IL_0009:  ldc.i4.s   11
  IL_000b:  newobj     instance void ConsoleApplication2.Program/Foo::.ctor(int32)
  IL_0010:  stloc.1
  IL_0011:  ldloc.1
  IL_0012:  ldloc.0
  IL_0013:  call       class ConsoleApplication2.Program/Foo ConsoleApplication2.Program/Foo::op_Addition(class ConsoleApplication2.Program/Foo,
                                                                                                          class ConsoleApplication2.Program/Foo)
  IL_0018:  stloc.1

更新:

根据您的更新 - 正如@EricLippert所说,您确实应该将向量作为不可变对象。两个向量相加的结果是一个的向量,而不是第一个大小不同的向量。

如果出于某种原因您需要更改第一个向量,可以使用此重载(但对我来说,这是非常奇怪的行为):

public static Vector operator +(Vector left, Vector right)
{
    left.x += right.x;
    left.y += right.y;
    return left;
}

2
仅仅陈述事实并不等于回答为什么。 - Jouke van der Maas
1
@Jouke van der Maas,你希望我如何回答为什么不可能呢?这是设计上的限制,我还能说什么呢? - VMAtm
2
为什么他们以这种方式设计它,这才是问题所在。请查看其他答案。 - Jouke van der Maas
2
如果你“出生”于C#编程,那么“奇怪的行为”就不会让你感到惊讶了 :p。但是,由于答案正确且解释得非常好,所以我会给你点赞的 ;)。 - ThunderGr
6
不管用哪种语言,这都相当奇怪。让 v3 = v1 + v2; 这个语句不仅改变 v3 的值,还改变 v1 的值是不寻常的。 - Assimilater
显示剩余5条评论

18
这是因为赋值运算符不能被重载的同样原因。你不能编写能够正确执行赋值操作的代码。
class Foo
{
   // Won't compile.
   public static Foo operator= (Foo c1, int x)
   {
       // duh... what do I do here?  I can't change the reference of c1.
   }
}

赋值运算符不能被重载,但是例如+=使用的是+,而+可以被重载。
来自MSDN

18
你不能重载+=,因为它并不是一个独特的运算符,它只是语法糖x += y只是x = x + y的简写方式。由于+=是基于+=操作符定义的,允许你单独重载它可能会在x += yx = x + y的行为不完全相同时引起问题。
在更低层次上,C#编译器很可能将这两个表达式编译成相同的字节码,这意味着在程序执行期间运行时无法将它们区别对待。
我可以理解你可能想将其视为单独的操作:在像x += 10这样的语句中,你知道可以就地改变x对象,可能可以节省一些时间/内存,而不是在分配给旧引用之前创建一个新对象x + 10
但考虑以下代码:
a = ...
b = a;
a += 10;

对于大多数类型,a都比b多10,因此a == b不成立。但是如果您可以重载+=运算符以就地修改,则可以成立。现在考虑将ab传递到程序的远程部分。如果对象开始在代码不希望的位置更改,则可能会创建混乱的错误。

换句话说,如果性能很重要,那么用类似于x.increaseBy(10)的方法调用替换x += 10并不难,而且对所有人来说更加清晰明了。


2
个人而言,我会将“它只是语法糖”改为“在C#中,它只是语法糖”,否则听起来太笼统了。在某些编程语言中,它不仅仅是语法糖,而且可能实际上会带来性能优势。 - Sebastian Mach
对于简单类型(int、float等),+= 可能会被智能编译器优化,因为算术运算很简单。但是一旦涉及到对象,情况就不一样了。任何语言都面临着几乎相同的问题。这就是为什么运算符重载是有害的原因。 - benzado
@SebastianMach 这个问题特别标记了c#和dotnet标签。显然,在c++中,例如,'+'和'+='(甚至'=')可以单独重载。 - Bojidar Stanchev
1
@BojidarStanchev:确实如此。我为自己9年前的言行向大家道歉 :-D - Sebastian Mach

18

我认为你会发现这个链接很有启发性:可重载运算符

赋值运算符不能被重载,但是例如 += 可以使用重载的 + 运算符进行求值。


2
@pickypg - 这条评论与本主题无关:请恢复你的答案,它回答了我的问题,我认为我别无选择,只能使用你的方法,我以为有更好的解决方法。 - Shimmy Weitzhandler

9
这是因为此运算符无法被重载:
赋值运算符不能被重载,但例如 += 是使用可以被重载的 + 运算符进行计算的。 MSDN 只需重载 + 运算符,因为
x += y 等同于 x = x + y。

6
如果您像这样重载+运算符:
class Foo
{
    public static Foo operator + (Foo c1, int x)
    {
        // implementation
    }
}

你可以做到

 Foo foo = new Foo();
 foo += 10;

或者
 foo = foo + 10;

这将编译并运行同样的结果。

6

对于这个问题,答案总是相同的:如果你可以通过重载 + 来免费获得它,为什么还需要 += 呢?但是,如果我有一个像这样的类,会发生什么。

using System;
using System.IO;

public class Class1
{
    public class MappableObject
    {
        FileStream stream;

        public  int Blocks;
        public int BlockSize;

        public MappableObject(string FileName, int Blocks_in, int BlockSize_in)
        {
            Blocks = Blocks_in;
            BlockSize = BlockSize_in;

            // Just create the file here and set the size
            stream = new FileStream(FileName); // Here we need more params of course to create a file.
            stream.SetLength(sizeof(float) * Blocks * BlockSize);
        }

        public float[] GetBlock(int BlockNo)
        {
            long BlockPos = BlockNo * BlockSize;

            stream.Position = BlockPos;

            using (BinaryReader reader = new BinaryReader(stream))
            {
                float[] resData = new float[BlockSize];
                for (int i = 0; i < BlockSize; i++)
                {
                    // This line is stupid enough for accessing files a lot and the data is large
                    // Maybe someone has an idea to make this faster? I tried a lot and this is the simplest solution
                    // for illustration.
                    resData[i] = reader.ReadSingle();
                }
            }

            retuen resData;
        }

        public void SetBlock(int BlockNo, float[] data)
        {
            long BlockPos = BlockNo * BlockSize;

            stream.Position = BlockPos;

            using (BinaryWriter reader = new BinaryWriter(stream))
            {
                for (int i = 0; i < BlockSize; i++)
                {
                    // Also this line is stupid enough for accessing files a lot and the data is large
                    reader.Write(data[i];
                }
            }

            retuen resData;
        }

        // For adding two MappableObjects
        public static MappableObject operator +(MappableObject A, Mappableobject B)
        {
            // Of course we have to make sure that all dimensions are correct.

            MappableObject result = new MappableObject(Path.GetTempFileName(), A.Blocks, A.BlockSize);

            for (int i = 0; i < Blocks; i++)
            {
                float[] dataA = A.GetBlock(i);
                float[] dataB = B.GetBlock(i);

                float[] C = new float[dataA.Length];

                for (int j = 0; j < BlockSize; j++)
                {
                    C[j] = A[j] + B[j];
                }

                result.SetBlock(i, C);
            }
        }

        // For adding a single float to the whole data.
        public static MappableObject operator +(MappableObject A, float B)
        {
            // Of course we have to make sure that all dimensions are correct.

            MappableObject result = new MappableObject(Path.GetTempFileName(), A.Blocks, A.BlockSize);

            for (int i = 0; i < Blocks; i++)
            {
                float[] dataA = A.GetBlock(i);

                float[] C = new float[dataA.Length];

                for (int j = 0; j < BlockSize; j++)
                {
                    C[j] = A[j] + B;
                }

                result.SetBlock(i, C);
            }
        }

        // Of course this doesn't work, but maybe you can see the effect here.
        // when the += is automimplemented from the definition above I have to create another large
        // object which causes a loss of memory and also takes more time because of the operation -> altgough its
        // simple in the example, but in reality it's much more complex.
        public static MappableObject operator +=(MappableObject A, float B)
        {
            // Of course we have to make sure that all dimensions are correct.

            MappableObject result = new MappableObject(Path.GetTempFileName(), A.Blocks, A.BlockSize);

            for (int i = 0; i < Blocks; i++)
            {
                float[] dataA = A.GetBlock(i);

                for (int j = 0; j < BlockSize; j++)
                {
                    A[j]+= + B;
                }

                result.SetBlock(i, A);
            }
        }
    }
}

你还觉得"+="的“自动实现”很棒吗? 如果你想在C#中进行高性能计算,就需要这样的功能来减少处理时间和内存消耗。如果有人有好的解决方案,那么它将受到高度赞赏。但是不要告诉我必须使用静态方法来完成这项任务,这只是一种权宜之计。我认为,如果"+"未定义,则没有理由C#执行"+="的实现;而如果已定义,它将被使用。有些人说,没有++=之间的区别会导致错误,但这不是我的问题吗?

2
如果你真的关心性能,你不会去玩弄运算符重载,因为这只会让人更难以确定哪些代码被调用。至于是否搞砸+=的语义是你自己的问题...那只有在没有其他人需要阅读、维护或执行你的代码时才是真的。 - benzado
2
你好,benzado。在某种程度上,你是正确的,但我们拥有的是一个高性能计算平台,用于创建原型应用程序。一方面,我们需要性能,另一方面,我们需要简单的语义。事实上,我们也希望拥有比C#当前提供的更多运算符。在这里,我希望通过C# 5和编译器作为服务技术来更好地利用C#语言。尽管我成长于C++,并且希望在C#中有更多来自C++的功能,但我不想再碰C++了,因为我正在使用C#。 - msedi
2
工程学涉及权衡取舍;你想要的每一件事都有一个代价。 - benzado
3
算术运算符通常按惯例返回新的实例 - 因此它们通常在不可变类型上被重写。例如,您不能使用操作符像 list += "new item"List<T> 添加新元素。相反,您需要调用其 Add 方法。 - Şafak Gür

6

+ 的运算符重载被用于 += 运算符,A += B 等同于 A = operator+(A, B)


3

我有完全相同的问题,但我无法比这个人回答得更好。


(意思是:这个链接里的人已经回答得非常好了)

-1
一个更好的设计方法是显式转换。你绝对可以重载类型转换。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接