Перегрузка оператора C # для `+ =`?

114

Я пытаюсь выполнить перегрузку оператора +=, но не могу. Могу сделать только перегрузку оператора для +.

Как придешь?

редактировать

Причина, по которой это не работает, заключается в том, что у меня есть класс Vector (с полями 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
Похоже, по этому поводу уже велась долгая дискуссия: maurits.wordpress.com/2006/11/27/…
Chris S
39
Вы можете объяснить, почему вы пытаетесь это сделать? Вы получаете перегруженный оператор «+ =» бесплатно, когда вы перегружаете «+». Есть ли ситуация, в которой вы хотите, чтобы "+ =" был перегружен, но не хотите, чтобы "+" был перегружен?
Эрик Липперт
3
Исходя из C ++, это кажется неправильным, но в C # это действительно имеет смысл.
Jon Purdy
12
@Mathias: по поводу вашего обновления: векторы должны вести себя как неизменные математические объекты. Когда вы добавляете 2 к 3, вы не преобразуете объект 3 в объект 5. Вы создаете совершенно новый объект 5. Смысл перегрузки операторов сложения состоит в том, чтобы создавать ваши собственные математические объекты; превращение их в изменчивые работает против этой цели. Я бы сделал ваш векторный тип неизменяемым типом значения.
Эрик Липперт

Ответы:

147

Перегружаемые операторы из MSDN:

Операторы присваивания не могут быть перегружены, но +=, например, оцениваются с помощью +, который может быть перегружен.

Более того, ни один из операторов присваивания не может быть перегружен. Я думаю, это потому, что это повлияет на сборку мусора и управление памятью, что является потенциальной дырой в безопасности в мире строгой типизации CLR.

Тем не менее, давайте разберемся, что такое оператор. Согласно знаменитой книге Джеффри Рихтера , у каждого языка программирования есть свой список операторов, которые составляются в вызовах специальных методов, а сама среда 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;
}
VMAtm
источник
2
Утверждение, что это так, не отвечает на вопрос, почему.
Jouke van der Maas
1
@Jouke van der Maas И как вы хотите, чтобы я ответил, почему это невозможно? Это невозможно по замыслу, что еще сказать?
VMAtm 06
2
Почему они так спроектировали; вот в чем, собственно, вопрос. См. Другие ответы.
Jouke van der Maas
2
«Странное поведение», только если вы «родились» программированием на C #: p. Но, поскольку ответ правильный и очень хорошо объяснен, вы тоже получите мою +1;)
ThunderGr 08
5
@ThunderGr Нет, это довольно странно, независимо от того, на какой язык вы смотрите. Для того, чтобы сделать заявление v3 = v1 + v2;результат в v1изменяется, а также v3необычна
Assimilater
17

Думаю, вы найдете эту ссылку информативной: Перегружаемые операторы

Операторы присваивания не могут быть перегружены, но, например, + = оценивается с помощью +, который может быть перегружен.

pickypg
источник
2
@pickypg - этот комментарий не имеет отношения к этой теме: пожалуйста, отмените удаление своего ответа , он отвечает на мой вопрос, я думаю, у меня нет другого выбора, кроме как использовать ваш метод, я подумал, что есть лучший способ решить эту проблему.
Shimmy Weitzhandler
16

Это происходит по той же причине, что оператор присваивания не может быть перегружен. Вы не можете написать код, который правильно выполнял бы назначение.

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 .

Агент-J
источник
16

Вы не можете перегрузить, +=потому что на самом деле это не уникальный оператор, это просто синтаксический сахар . x += yэто просто сокращенный способ написания x = x + y. Потому что +=определяется в терминах +и =операторов, что позволяет переопределить его отдельно может создать проблемы, в тех случаях , когда x += yи x = x + yне ведут себя точно так же.

На более низком уровне весьма вероятно, что компилятор C # компилирует оба выражения до одного и того же байт-кода, а это означает, что весьма вероятно, что среда выполнения не сможет обрабатывать их по-разному во время выполнения программы.

Я понимаю, что вы можете рассматривать его как отдельную операцию: в таком заявлении, как x += 10вы знаете, что вы можете изменить xобъект на месте и, возможно, сэкономить время / память, вместо того, чтобы создавать новый объект x + 10перед назначением его по старой ссылке ,

Но рассмотрим этот код:

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

Должен a == bв конце? Для большинства типов нет, aна 10 больше b. Но если бы вы могли перегрузить +=оператор для изменения на месте, тогда да. Теперь рассмотрим , что aи bможет получить розданы в отдаленные части программы. Ваша возможная оптимизация может создать сбивающие с толку ошибки, если ваш объект начнет меняться там, где этого не ожидает код.

Другими словами, если производительность так важна, ее не так сложно заменить x += 10вызовом метода, например x.increaseBy(10), и это намного понятнее для всех, кто участвует.

benzado
источник
2
Лично я бы изменил it's just syntactic sugarна it's just syntactic sugar in C#; в противном случае это звучит слишком обобщенно, но в некоторых языках программирования это не просто синтаксический сахар, но может действительно дать повышение производительности.
Себастьян Мах
Для простых типов (int, float и т. Д.), +=Вероятно , можно было бы оптимизировать умный компилятор, потому что арифметика проста. Но когда вы имеете дело с объектами, все ставки отменены. Любой язык сталкивается с примерно одинаковыми проблемами. Вот почему перегрузка операторов - зло.
benzado 06
@SebastianMach Вопрос специально помечен тегами c # и dotnet. Очевидно, что, например, в C ++ символы «+» и «+ =» (и даже «=») могут быть перегружены отдельно.
Божидар Станчев,
1
@BojidarStanchev: Верно. Прошу прощения за то, что я был моложе на 9 лет :-D
Себастьян Мах,
9

Это потому, что этот оператор не может быть перегружен:

Операторы присваивания не могут быть перегружены, но, например, + = оценивается с помощью +, который может быть перегружен.

MSDN

Просто перегрузите +оператор из-за

x += y равно x = x + y

Эндрю Орсич
источник
6

Перегрузка оператора для +используется в +=операторе, A += Bравно A = operator+(A, B).

Алекс Седов
источник
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 # выполняет +=реализацию, если она не определена, и если она определена, она будет использоваться. Некоторые говорят, что отсутствие разницы между ошибками +и +=предотвращение ошибок, но разве это не моя проблема?

msedi
источник
2
Если вы действительно заботитесь о производительности, вы не собираетесь возиться с перегрузкой операторов, что только затрудняет определение того, какой код вызывается. Что касается того, является ли испорченная семантика +=вашей собственной проблемой ... это верно только в том случае, если никому больше не нужно читать, поддерживать или выполнять ваш код.
benzado 06
2
Привет, бензадо. В чем-то вы правы, но у нас есть платформа для высокопроизводительных вычислений для создания прототипов приложений. С одной стороны, нам нужна производительность, с другой - простая семантика. На самом деле нам также нравится иметь больше операторов, чем в настоящее время предоставляет C #. Здесь я надеюсь, что C # 5 и компилятор как сервисная техника позволят получить больше от языка C #. Тем не менее, поскольку я вырос на C ++ и был бы признателен, если бы в C # было немного больше возможностей C ++, хотя я не хотел бы снова касаться C ++, поскольку я # занимаюсь C #.
мседи
2
Инженерное дело - это компромисс; все, что вы хотите, имеет свою цену.
benzado
3
Арифметические операторы возвращают новые экземпляры по соглашению, поэтому они обычно переопределяются для неизменяемых типов. Вы не можете добавить новый элемент в, например, с List<T>помощью оператора list += "new item". Вы называете этоAddВместо этого метод.
Шафак Гюр
0

Лучшим методом проектирования является явное приведение. Однозначно можно перегрузить Casting.

N_E
источник