Quá tải toán tử C # cho `+ =`?


114

Tôi đang cố gắng thực hiện quá tải toán tử cho +=, nhưng tôi không thể. Tôi chỉ có thể làm cho một toán tử quá tải cho +.

Làm thế nào mà?

Biên tập

Lý do điều này không hoạt động là tôi có một lớp Vector (với một trường X và Y). Hãy xem xét ví dụ sau.

vector1 += vector2;

Nếu quá tải toán tử của tôi được đặt thành:

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

Sau đó, kết quả sẽ không được thêm vào vector1, mà thay vào đó, vector1 cũng sẽ trở thành một Vector hoàn toàn mới theo tham chiếu.


2
Trông giống như một cuộc thảo luận dài đã được thăm về vấn đề này: maurits.wordpress.com/2006/11/27/...
Chris S

39
Bạn có thể giải thích tại sao bạn đang cố gắng làm điều này? Bạn nhận được toán tử "+ =" quá tải miễn phí khi bạn quá tải "+". Có một số tình huống trong đó bạn làm muốn "+ =" để bị quá tải nhưng làm không muốn "+" để bị quá tải?
Eric Lippert

3
Đến từ C ++, điều này chỉ cảm thấy sai, nhưng trong C # nó thực sự có ý nghĩa hoàn hảo.
Jon Purdy


12
@Mathias: cập nhật lại: Các vectơ phải hoạt động giống như các đối tượng toán học bất biến. Khi bạn thêm 2 thành 3, bạn không biến đổi đối tượng 3 thành đối tượng 5. Bạn tạo một đối tượng hoàn toàn mới, 5. Điểm nạp chồng các toán tử cộng là tạo các đối tượng toán học của riêng bạn; làm cho chúng có thể thay đổi hoạt động chống lại mục tiêu đó. Tôi sẽ làm cho kiểu vectơ của bạn trở thành kiểu giá trị bất biến.
Eric Lippert vào

Câu trả lời:


147

Các nhà khai thác quá tải , từ MSDN:

Các toán tử gán không thể được nạp chồng, nhưng +=ví dụ, được đánh giá bằng cách sử dụng +, có thể được nạp chồng.

Thậm chí, không có toán tử gán nào có thể bị quá tải. Tôi nghĩ điều này là do sẽ có ảnh hưởng đến việc thu thập Rác và quản lý bộ nhớ, đây là một lỗ hổng bảo mật tiềm ẩn trong thế giới được đánh máy mạnh mẽ của CLR.

Tuy nhiên, hãy xem toán tử chính xác là gì. Theo cuốn sách nổi tiếng của Jeffrey Richter , mỗi ngôn ngữ lập trình đều có danh sách toán tử riêng, được biên dịch theo các lệnh gọi phương thức đặc biệt, và bản thân CLR không biết gì về toán tử. Vì vậy, chúng ta hãy xem những gì chính xác nằm sau toán tử ++=.

Xem mã đơn giản này:

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

Hãy xem mã IL để biết hướng dẫn này:

  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

Bây giờ chúng ta hãy xem mã này:

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

Và mã IL cho điều này:

  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

Họ đều bình đẳng! Vì vậy, +=toán tử chỉ là đường cú pháp cho chương trình của bạn trong C # , và bạn có thể nạp chồng +toán tử một cách đơn giản .

Ví dụ:

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

Mã này sẽ được biên dịch và chạy thành công dưới dạng:

  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

Cập nhật:

Theo Bản cập nhật của bạn - như @EricLippert nói, bạn thực sự nên có các vectơ như một đối tượng bất biến. Kết quả của việc thêm hai vectơ là một vectơ mới , không phải là vectơ đầu tiên có kích thước khác nhau.

Nếu vì lý do nào đó mà bạn cần thay đổi vectơ đầu tiên, bạn có thể sử dụng quá tải này (nhưng đối với tôi, đây là hành vi rất lạ):

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

2
Nói rằng đó là trường hợp không trả lời tại sao.
Jouke van der Maas,

1
@Jouke van der Maas Và bạn muốn tôi trả lời như thế nào tại sao không thể? Đó là không thể bằng thiết kế, tôi có thể nói gì khác?
VMAtm

2
Tại sao họ thiết kế nó theo cách này; đó thực sự là câu hỏi. Xem một số câu trả lời khác.
Jouke van der Maas

2
"Hành vi kỳ lạ" chỉ khi bạn đã được "sinh ra" lập trình trong C #: p. Nhưng, kể từ khi câu trả lời là đúng và giải thích rất rõ, bạn sẽ có 1 mình, cũng;)
ThunderGr

5
@ThunderGr Không, nó khá lạ cho dù bạn đang xem ngôn ngữ nào. Để thực hiện các tuyên bố v3 = v1 + v2;kết quả trong v1bị thay đổi cũng như v3là không bình thường
Assimilater

17

Tôi nghĩ bạn sẽ thấy liên kết này có nhiều thông tin: Các nhà khai thác quá tải

Các toán tử gán không thể được nạp chồng, nhưng + =, ví dụ, được đánh giá bằng cách sử dụng +, có thể được nạp chồng.


2
@pickypg - nhận xét này không liên quan đến chủ đề này: vui lòng khôi phục câu trả lời của bạn , nó trả lời câu hỏi của tôi, tôi nghĩ tôi không có lựa chọn nào khác ngoài sử dụng phương pháp của bạn, tôi nghĩ có một số cách tốt hơn để giải quyết nó.
Shimmy Weitzhandler

16

Điều này là do cùng một lý do mà toán tử gán không thể được nạp chồng. Bạn không thể viết mã thực hiện nhiệm vụ một cách chính xác.

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

Các toán tử gán không thể được nạp chồng, nhưng + =, ví dụ, được đánh giá bằng cách sử dụng +, có thể được nạp chồng.

Từ MSDN .


16

Bạn không thể quá tải +=vì nó không thực sự là một toán tử duy nhất, nó chỉ là một đường cú pháp . x += ychỉ là một cách viết tắt x = x + y. Bởi vì +=được định nghĩa theo thuật ngữ +=toán tử, việc cho phép bạn ghi đè nó một cách riêng biệt có thể tạo ra các vấn đề, trong những trường hợp xảy ra x += yx = x + ykhông hoạt động theo cùng một cách.

Ở cấp độ thấp hơn, rất có thể trình biên dịch C # biên dịch cả hai biểu thức xuống cùng một mã byte, có nghĩa là rất có thể thời gian chạy không thể xử lý chúng khác nhau trong quá trình thực thi chương trình.

Tôi có thể hiểu rằng bạn có thể muốn coi nó như một thao tác riêng biệt: trong một câu lệnh như x += 10bạn biết rằng bạn có thể thay đổi xđối tượng tại chỗ và có thể tiết kiệm một chút thời gian / bộ nhớ, thay vì tạo một đối tượng mới x + 10trước khi gán nó trên tham chiếu cũ .

Nhưng hãy xem xét mã này:

a = ...
b = a;
a += 10;

a == bCuối cùng có nên không? Đối với hầu hết các loại, không, anhiều hơn 10 b. Nhưng nếu bạn có thể quá tải +=toán tử để thay đổi tại chỗ, thì có. Bây giờ hãy xem xét điều đó abcó thể được chuyển đến các phần xa của chương trình. Việc tối ưu hóa khả thi của bạn có thể tạo ra các lỗi khó hiểu nếu đối tượng của bạn bắt đầu thay đổi ở nơi mã không mong đợi.

Nói cách khác, nếu hiệu suất là quan trọng, không quá khó để thay thế x += 10bằng một cuộc gọi phương thức như thế nào x.increaseBy(10)và nó rõ ràng hơn rất nhiều cho tất cả mọi người tham gia.


2
Cá nhân tôi sẽ thay đổi it's just syntactic sugarthành it's just syntactic sugar in C#; nếu không, nó nghe có vẻ quá chung chung, nhưng trong một số ngôn ngữ lập trình, nó không chỉ là đường cú pháp mà thực sự có thể mang lại lợi ích về hiệu suất.
Sebastian Mach

Đối với các kiểu đơn giản (int, float, v.v.), +=có thể được tối ưu hóa bằng trình biên dịch thông minh, vì số học rất đơn giản. Nhưng một khi bạn đang giao dịch với các đối tượng, tất cả các cược sẽ tắt. Bất kỳ ngôn ngữ nào cũng gặp phải nhiều vấn đề giống nhau. Đây là lý do tại sao quá tải toán tử là xấu.
benzado

@SebastianMach Câu hỏi được gắn thẻ cụ thể bằng thẻ c # và dotnet. Rõ ràng, trong c ++ chẳng hạn, '+' và '+ =' (và thậm chí '=') có thể được nạp chồng riêng.
Bojidar Stanchev

1
@BojidarStanchev: Đúng vậy. Tôi xin lỗi vì bản thân tôi trẻ hơn 9 tuổi :-D
Sebastian Mach.

9

Điều này là do toán tử này không thể bị quá tải:

Các toán tử gán không thể được nạp chồng, nhưng + =, ví dụ, được đánh giá bằng cách sử dụng +, có thể được nạp chồng.

MSDN

Chỉ cần quá tải +toán tử, vì

x += y tương đương với x = x + y


6

Quá tải toán tử for +được sử dụng trong +=toán tử, A += Bbằng với A = operator+(A, B).


6

Nếu bạn quá tải +toán tử như thế này:

class Foo
{
    public static Foo operator + (Foo c1, int x)
    {
        // implementation
    }
}

bạn có thể làm

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

hoặc là

 foo = foo + 10;

Điều này sẽ biên dịch và chạy như nhau.


6

Luôn có cùng một câu trả lời cho vấn đề này: Tại sao bạn cần +=, nếu bạn nhận được miễn phí nếu bạn quá tải +. Nhưng điều gì sẽ xảy ra nếu tôi có một lớp học như thế này.

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);
            }
        }
    }
}

Bạn vẫn nói rằng điều tốt +=là "tự động thực hiện". Nếu bạn cố gắng thực hiện tính toán hiệu suất cao trong C #, bạn cần phải có các tính năng như vậy để giảm thời gian xử lý và tiêu thụ bộ nhớ, nếu ai đó có giải pháp tốt sẽ được đánh giá cao, nhưng đừng nói với tôi rằng tôi phải làm điều này với phương pháp tĩnh , đây chỉ là một giải pháp thay thế và tôi không thấy lý do gì tại sao C # +=thực hiện triển khai nếu nó không được xác định và nếu nó được định nghĩa thì nó sẽ được sử dụng. Một số người nói rằng không có sự khác biệt giữa ++=ngăn ngừa lỗi, nhưng đây không phải là vấn đề của riêng tôi?


2
Nếu bạn thực sự quan tâm đến hiệu suất, bạn sẽ không phải lo lắng về việc quá tải toán tử, điều này chỉ khiến bạn khó biết mã nào đang được gọi. Về việc liệu việc lộn xộn ngữ nghĩa có phải +=là vấn đề của riêng bạn hay không ... điều đó chỉ đúng nếu không ai khác phải đọc, duy trì hoặc thực thi mã của bạn.
benzado

2
Xin chào, benzado. Theo một cách nào đó thì bạn đúng, nhưng những gì chúng tôi có là một nền tảng cho máy tính hiệu suất cao để tạo ra các ứng dụng nguyên mẫu. Theo một cách chúng ta cần có hiệu suất, mặt khác chúng ta cần một ngữ nghĩa đơn giản. Trên thực tế, chúng tôi cũng muốn có nhiều toán tử hơn C # hiện đang phân phối. Ở đây, tôi hy vọng C # 5 và trình biên dịch như một kỹ thuật dịch vụ để khai thác nhiều hơn ngôn ngữ C #. Tuy nhiên, khi tôi lớn lên với C ++ và sẽ đánh giá cao nếu có thêm một chút tính năng từ C ++ trong C #, tuy nhiên, tôi không muốn chạm vào C ++ nữa vì tôi đang làm C #.
msedi

2
Kỹ thuật là tất cả về sự cân bằng; mọi thứ bạn muốn đều có giá.
benzado

3
Các toán tử số học trả về các thể hiện mới theo quy ước - do đó chúng thường bị ghi đè trên các kiểu bất biến. Ví dụ, bạn không thể thêm một phần tử mới vào một List<T>toán tử như list += "new item". AddThay vào đó bạn gọi phương thức của nó .
Şafak Gür


0

Một phương pháp thiết kế tốt hơn là Truyền rõ ràng. Bạn chắc chắn có thể truyền quá tải.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.