Перегрузка оператора 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;
}

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

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

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


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 .


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), и это намного понятнее для всех, кто участвует.


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

Если вы перегрузите +оператор следующим образом:

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 06

2
Привет, бензадо. В чем-то вы правы, но у нас есть платформа для высокопроизводительных вычислений для создания прототипов приложений. С одной стороны, нам нужна производительность, с другой - простая семантика. На самом деле нам также нравится иметь больше операторов, чем в настоящее время предоставляет C #. Здесь я надеюсь, что C # 5 и компилятор как сервисная техника позволят получить больше от языка C #. Тем не менее, поскольку я вырос на C ++ и был бы признателен, если бы в C # было немного больше возможностей C ++, хотя я не хотел бы снова касаться C ++, поскольку я # занимаюсь C #.
— мседи

2
Инженерное дело - это компромисс; все, что вы хотите, имеет свою цену.
— benzado

3
Арифметические операторы возвращают новые экземпляры по соглашению, поэтому они обычно переопределяются для неизменяемых типов. Вы не можете добавить новый элемент в, например, с List<T>помощью оператора list += "new item". Вы называете этоAddВместо этого метод.
— Шафак Гюр


0

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.