Przeciążenie operatora C # dla `+ =`?


114

Próbuję zrobić przeciążenia operatorów dla += , ale nie mogę. Mogę tylko przeciążać operatora +.

Dlaczego?

Edytować

Powodem, dla którego to nie działa, jest to, że mam klasę Vector (z polami X i Y). Rozważmy następujący przykład.

vector1 += vector2;

Jeśli moje przeciążenie operatora jest ustawione na:

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

Wtedy wynik nie zostanie dodany do vector1, ale zamiast tego vector1 stanie się zupełnie nowym Vector przez odniesienie.


2
Wygląda na to, że odbyła się już długa dyskusja na ten temat: maurits.wordpress.com/2006/11/27/…
Chris S

39
Czy możesz wyjaśnić, dlaczego próbujesz to zrobić? Otrzymujesz przeciążony operator „+ =” za darmo, gdy przeładujesz „+”. Czy jest jakaś sytuacja, w której chcesz , aby "+ =" był przeciążony, ale nie chcesz, aby "+" było przeciążone?
Eric Lippert,

3
Pochodząc z C ++, to po prostu wydaje się złe, ale w C # ma to sens.
Jon Purdy,


12
@Mathias: re your update: Wektory powinny zachowywać się jak niezmienne obiekty matematyczne. Kiedy dodajesz 2 do 3, nie zmieniasz obiektu 3 w obiekt 5. Tworzysz zupełnie nowy obiekt, 5. Celem przeciążenia operatorów dodawania jest tworzenie własnych obiektów matematycznych; uczynienie ich zmiennymi działa przeciwko temu celowi. Uczyniłbym twój typ wektora niezmiennym typem wartości.
Eric Lippert

Odpowiedzi:


147

Operatory z możliwością przeciążenia z możliwością , z MSDN:

Operatory przypisania nie mogą być przeciążane, ale +=na przykład są oceniane przy użyciu +, które mogą zostać przeciążone.

Co więcej, żaden z operatorów przypisania nie może być przeciążony. Myślę, że dzieje się tak, ponieważ będzie to miało wpływ na zbieranie śmieci i zarządzanie pamięcią, co jest potencjalną luką w zabezpieczeniach w świecie silnie typizowanym CLR.

Niemniej jednak zobaczmy, czym dokładnie jest operator. Według słynnej książki Jeffreya Richtera , każdy język programowania ma swoją własną listę operatorów, która jest kompilowana w specjalnych wywołaniach metod, a sam CLR nie wie nic o operatorach. Zobaczmy więc, co dokładnie pozostaje za operatorami +i +=.

Zobacz ten prosty kod:

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

Zobaczmy kod IL dla tej instrukcji:

  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

Teraz zobaczmy ten kod:

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

I kod IL do tego:

  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

Są równi! Więc +=operator jest tylko cukrem syntaktycznym dla twojego programu w C # i możesz po prostu przeładować+ .

Na przykład:

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

Ten kod zostanie skompilowany i pomyślnie uruchomiony jako:

  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

Aktualizacja:

Zgodnie z twoją aktualizacją - jak mówi @EricLippert, naprawdę powinieneś mieć wektory jako niezmienny obiekt. Efektem dodania dwóch wektorów jest nowy wektor, a nie pierwszy o różnych rozmiarach.

Jeśli z jakiegoś powodu musisz zmienić pierwszy wektor, możesz użyć tego przeciążenia (ale jak dla mnie jest to bardzo dziwne zachowanie):

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

2
Stwierdzenie, że tak jest, nie wyjaśnia dlaczego.
Jouke van der Maas

1
@Jouke van der Maas A jak mam odpowiedzieć, dlaczego nie jest to możliwe? Z założenia jest to niemożliwe, co jeszcze mogę powiedzieć?
VMAtm,

2
Dlaczego zaprojektowali to w ten sposób; o to właśnie chodzi. Zobacz inne odpowiedzi.
Jouke van der Maas

2
„Dziwne zachowanie” tylko wtedy, gdy „urodziłeś się” programując w C #: p. Ale ponieważ odpowiedź jest prawidłowa i bardzo dobrze wyjaśniona, otrzymujesz również moje +1;)
ThunderGr

5
@ThunderGr Nie, to dość dziwne, bez względu na język, na który patrzysz. Sprawienie, by oświadczenie v3 = v1 + v2;skutkowało v1zmianą, a także v3jest niezwykłe
Assimilater

17

Myślę, że ten link okaże się pouczający: Operatory przeciążalne

Operatory przypisania nie mogą być przeciążane, ale na przykład + = jest oceniane przy użyciu +, który może zostać przeciążony.


2
@pickypg - ten komentarz nie jest związany z tym wątkiem: cofnij usunięcie swojej odpowiedzi , odpowiada na moje pytanie, myślę, że nie mam innego wyjścia, jak tylko użyć Twojej metody, pomyślałem, że jest lepszy sposób na rozwiązanie tego problemu.
Shimmy Weitzhandler

16

Dzieje się tak z tego samego powodu, dla którego nie można przeciążać operatora przypisania. Nie możesz napisać kodu, który wykonałby przypisanie poprawnie.

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

Operatory przypisania nie mogą być przeciążane, ale na przykład + = jest oceniane przy użyciu +, który może zostać przeciążony.

Z MSDN .


16

Nie możesz przeciążać, +=ponieważ tak naprawdę nie jest to wyjątkowy operator, to po prostu cukier składniowy . x += yto tylko skrótowy sposób pisania x = x + y. Ponieważ +=jest zdefiniowany za pomocą operatorów +i =, umożliwienie oddzielnego nadpisania go może spowodować problemy w przypadkach, w których x += yi x = x + ynie zachowywał się dokładnie w ten sam sposób.

Na niższym poziomie jest bardzo prawdopodobne, że kompilator C # skompiluje oba wyrażenia do tego samego kodu bajtowego, co oznacza, że ​​jest bardzo prawdopodobne, że środowisko wykonawcze nie może traktować ich inaczej podczas wykonywania programu.

Rozumiem, że możesz chcieć potraktować to jako oddzielną operację: w oświadczeniu takim jak x += 10wiesz, że możesz zmutować xobiekt w miejscu i być może zaoszczędzić trochę czasu / pamięci, zamiast tworzyć nowy obiekt x + 10przed przypisaniem go do starego odniesienia .

Ale rozważ ten kod:

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

Powinien a == bna końcu? W przypadku większości typów nie, awynosi 10 więcej niż b. Ale jeśli mógłbyś przeciążać +=operatora, aby dokonać mutacji w miejscu, to tak. Teraz rozważ to ai bmoże zostać przekazane do odległych części programu. Możliwa optymalizacja może spowodować mylące błędy, jeśli obiekt zacznie się zmieniać tam, gdzie kod tego nie oczekuje.

Innymi słowy, jeśli wydajność jest tak ważna, nie jest trudno ją zastąpić x += 10wywołaniem metody, takim jak x.increaseBy(10)i jest to o wiele jaśniejsze dla wszystkich zaangażowanych.


2
Osobiście zmieniłbym się it's just syntactic sugarna it's just syntactic sugar in C#; w przeciwnym razie brzmi zbyt ogólnie, ale w niektórych językach programowania nie jest to tylko cukier syntaktyczny, ale może w rzeczywistości dawać korzyści wydajnościowe.
Sebastian Mach,

Dla typów prostych (int, float itp.) +=Prawdopodobnie mógłby zostać zoptymalizowany przez inteligentny kompilator, ponieważ arytmetyka jest prosta. Ale kiedy masz do czynienia z przedmiotami, wszystkie zakłady są wyłączone. Każdy język ma prawie takie same problemy. Dlatego przeciążenie operatora jest złe.
benzado,

@SebastianMach Pytanie jest specjalnie oznaczone tagami C # i dotnet. Oczywiście na przykład w języku c ++ „+” i „+ =” (a nawet „=”) mogą być przeciążane osobno.
Bojidar Stanchev

1
@BojidarStanchev: To prawda. Przepraszam za moje 9 lat młodsze ja :-D
Sebastian Mach

9

Dzieje się tak, ponieważ ten operator nie może zostać przeciążony:

Operatory przypisania nie mogą być przeciążane, ale na przykład + = jest oceniane przy użyciu +, który może zostać przeciążony.

MSDN

Po prostu przeciąż +operatora, ponieważ

x += y równy x = x + y


6

Przeciążenie operatora dla +jest używane w +=operatorze, A += Bjest równe A = operator+(A, B).


6

Jeśli przeciążasz +operatora w ten sposób:

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

możesz to zrobić

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

lub

 foo = foo + 10;

Spowoduje to kompilację i równe działanie.


6

Zawsze istnieje ta sama odpowiedź na ten problem: po co ci ten plik +=, jeśli otrzymasz go za darmo, jeśli przeładujesz plik +. Ale co się stanie, jeśli mam taką klasę.

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

Czy nadal mówisz, że to dobrze, że +=jest „automatycznie wdrażany”. Jeśli próbujesz wykonywać obliczenia o wysokiej wydajności w C #, musisz mieć takie funkcje, aby skrócić czas przetwarzania i zużycie pamięci, jeśli ktoś ma dobre rozwiązanie, jest to bardzo cenne, ale nie mów mi, że muszę to robić metodami statycznymi , jest to tylko obejście i nie widzę powodu, dla którego C # wykonuje +=implementację, jeśli nie jest zdefiniowana, a jeśli jest zdefiniowana, zostanie użyta. Niektórzy mówią, że brak różnicy między błędami +i +=zapobieganie im, ale czy to nie jest mój własny problem?


2
Jeśli naprawdę zależy Ci na wydajności, nie będziesz się męczyć z przeciążeniem operatorów, co tylko utrudnia stwierdzenie, jaki kod jest wywoływany. Jeśli chodzi o to, czy zepsucie semantyki +=jest twoim własnym problemem ... jest to prawdą tylko wtedy, gdy nikt inny nigdy nie musi czytać, utrzymywać ani wykonywać twojego kodu.
benzado,

2
Cześć, benzado. W pewnym sensie masz rację, ale mamy platformę do wysokowydajnych obliczeń do tworzenia prototypowych aplikacji. Z jednej strony potrzebujemy wykonania, z drugiej strony potrzebujemy prostej semantyki. W rzeczywistości lubimy mieć więcej operatorów niż obecnie dostarcza język C #. Mam nadzieję, że C # 5 i kompilator jako technika usługi pozwolą uzyskać więcej z języka C #. Niemniej jednak, ponieważ dorastałem w C ++ i doceniłbym, gdyby było trochę więcej funkcji z C ++ w C #, chociaż nie chciałbym ponownie dotykać C ++, ponieważ zajmuję się C #.
msedi

2
Inżynieria polega na kompromisach; wszystko, czego chcesz, ma swoją cenę.
benzado

3
Operatory arytmetyczne zwracają nowe wystąpienia zgodnie z konwencją - dlatego są one zwykle zastępowane w niezmiennych typach. Nie możesz na przykład dodać nowego elementu do elementu za List<T>pomocą operatora takiego jak list += "new item". Zamiast tego wywołujesz jego Addmetodę.
Şafak Gür


0

Lepszą metodą projektowania jest rzutowanie jawne. Zdecydowanie możesz przeciążać Casting.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.