Перевантаження оператора C # для `+ =`?


114

Я намагаюся робити перевантаження оператора +=, але не можу. Я можу зробити лише перевантаження оператора для +.

Як це?

Редагувати

Причина цього не працює в тому, що у мене є векторний клас (з полями X і Y). Розглянемо наступний приклад.

vector1 += vector2;

Якщо перевантаження мого оператора встановлено на:

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

Тоді результат не буде доданий до vector1, але натомість вектор1 також стане абсолютно новим Vector за посиланням.


2
Схоже, про це вже тривало обговорення: maurits.wordpress.com/2006/11/27/…
Кріс С

39
Чи можете ви пояснити, чому ви намагаєтеся це зробити? Ви отримуєте перевантажений оператор "+ =" безкоштовно, коли перевантажуєте "+". Є деякі ситуації , в якій ви дійсно хочете «+ =» , щоб бути перевантажений , але робити НЕ хочуть «+» , щоб бути перевантажений?
Ерік Ліпперт

3
Починаючи з C ++, це просто почувається не так, але в C # це насправді має ідеальний сенс.
Джон Перді


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
Заявивши, що це так, це не відповідати чому.
Жоук ван дер Маас

1
@Jouke van der Maas А як ти хочеш, щоб я відповів, чому це неможливо? Дизайн це неможливо, що я ще можу сказати?
VMAtm

2
Чому вони спроектували це саме так; ось в чому питання, насправді. Дивіться деякі інші відповіді.
Жоук ван дер Маас

2
"Дивна поведінка", тільки якщо ви "народилися" програмуванням на C #: p. Але, оскільки відповідь правильна і дуже добре пояснена, ви отримуєте і мій +1;)
ThunderGr

5
@ThunderGr Ні, це досить дивно, незалежно від того, на якій мові ви дивитесь. Для того, щоб зробити заяву v3 = v1 + v2;результат в v1змінюється, а також v3незвичайна
Assimilater

17

Я думаю, ви знайдете це посилання інформативним: Оператори, що завантажуються

Оператори призначення не можуть бути перевантажені, але + =, наприклад, оцінюється за допомогою +, яка може бути перевантажена.


2
@pickypg - цей коментар не пов'язаний з цією темою: будь ласка, скасуйте відповідь , він відповідає на моє запитання, я думаю, що у мене немає іншого вибору, як використовувати ваш метод, я подумав, що є якийсь кращий спосіб вирішити його.
Шиммі Вайцхандлер

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 тощо) типів, +=можливо , може бути оптимізований розумний компілятор, оскільки арифметика проста. Але як тільки ви маєте справу з предметами, усі ставки знищуються. Будь-яка мова стикається з однаковими проблемами. Ось чому перевантаження оператора - це зло.
бензадо

@SebastianMach Питання спеціально позначено тегами c # та dotnet. Очевидно, що в c ++, наприклад, "+" і "+ =" (і навіть "=") можна перевантажувати окремо.
Божидар Станчев

1
@ БойдарСтанчев: Це правда. Прошу вибачення за мою молодшу на 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 # робить +=реалізацію, якщо вона не визначена, і якщо вона визначена, вона буде використана. Деякі люди кажуть, що немає різниці між +і +=запобігає помилкам, але це не моя власна проблема?


2
Якщо ви дійсно піклуєтесь про продуктивність, ви не збираєтеся возитися з перевантаженням оператора, що лише ускладнює визначення того, який код викликається. Що стосується того, чи зіпсувати семантику - +=це ваша власна проблема ... це справедливо лише в тому випадку, якщо нікому більше ніколи не доведеться читати, підтримувати або виконувати ваш код.
benzado

2
Привіт, бензадо. У чомусь ви праві, але у нас є платформа для високоефективних обчислень для створення прототипів. Одним чином нам потрібно мати продуктивність, з іншого боку нам потрібна проста семантика. Насправді ми також любимо мати більше операторів, ніж постачає C #. Тут я сподіваюся на C # 5 і компілятор як техніку обслуговування, щоб отримати більше користі від мови C #. Тим не менше, коли я виріс із C ++ і буду вдячний, якщо в C # є трохи більше функцій від C ++, хоча я не хотів би знову торкатися C ++, оскільки я # m займаюся C #.
msedi

2
Інженерія - це все про компроміси; все, що ви хочете, йде з ціною.
benzado

3
Арифметичні оператори повертають нові екземпляри за умовою - отже, вони зазвичай переосмислюються на незмінні типи. Ви не можете додати новий елемент до List<T>використання оператора, наприклад list += "new item", наприклад. Ви називаєте його Addметод замість цього.
Şafak Gür


0

Кращий метод проектування - явне лиття. Ви точно можете перевантажити кастинг.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.