Как сделать так, чтобы оператор + = сохранял ссылку на объект? - PullRequest
1 голос
/ 19 февраля 2010

Скажите, у меня есть класс:

class M
{
     public int val; 

А также оператор + внутри него:

    public static M operator +(M a, M b)
    {
        M c = new M();
        c.val = a.val + b.val;
        return c;
    }
}

И у меня есть List объектов класса:

List<M> ms = new List();
M obj = new M();
obj.val = 5;
ms.Add(obj);

Какой-то другой объект:

M addie = new M();
addie.val = 3;

Я могу сделать это:

ms[0] += addie;

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

M fromList = ms[0];
fromList += addie;

это не меняет значение в ms по понятным причинам.

Но интуитивно я ожидаю, что ms[0] также изменится после этого. На самом деле, я выбираю объект из списка и затем увеличиваю его значение другим объектом. Итак, поскольку я держал ссылку на ms[0] в fromList до добавления, я хочу все еще держать ее в fromList после ее выполнения.

Есть ли способы достичь этого?

Ответы [ 6 ]

6 голосов
/ 19 февраля 2010

Вы не должны ожидать ms[0] изменения. После инициализации fromList вообще не связан с ms - fromList и ms[0] имеют одинаковое значение, но это все. + = возвращает новое значение (как и должно быть), поэтому вы просто меняете значение, хранящееся в fromList, которое полностью не зависит от значения в списке. (Обратите внимание, что значения здесь являются ссылками, не объектов.)

Не пытайтесь изменить это поведение - оно делает правильные вещи. Вместо этого измените свои ожидания.

Если вы действительно хотите, чтобы содержимое списка отражало изменение, вам нужно либо изменить значение в списке, чтобы ссылаться на новый объект, либо изменить код, чтобы изменить существующий объект вместо создания нового. Если вы выберете последний подход, вы должны , а не делать это внутри оператора. Вместо этого создайте метод Add, чтобы вы вызывали:

fromList.Add(addie);

Совершенно очевидно, что это мутантная операция, поэтому она не нарушит никаких ожиданий.

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

4 голосов
/ 19 февраля 2010

Вы смешиваете изменчивое поведение с неизменяемым поведением, я думаю, что в этом его смущение.

Объект изменчив (т.е. вы можете изменять его свойства), и он ведет себя так, как вы ожидаете, когда назначаетезначение его свойства.

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

List<int> list = new List<int>() { 1, 2, 3};
int x = list[0];

// this will of cousre not change the content in the list:
x += 42;

Когда у вас есть этот код:

M fromList = ms[0];
fromList += addie;

Компилятор использует +оператор, подобный этому:

M fromList = ms[0];
fromlist = fromList + addie;

Выражение fromlist + addie возвращает ссылку на новый экземпляр класса, и эта ссылка присваивается переменной.Естественно, это не изменит объект, на который ссылалась переменная ранее.

0 голосов
/ 23 февраля 2010

Здесь есть другой угол, которого я не видел в других ответах.

В C ++ способ создания массива-подобного класса заключается в перегрузке [] (известный как оператор подписки):

std::string operator[](int nIndex);

Но это определение позволяет только читать элементы.Чтобы поддержать написание, вам нужно вернуть то, что мы, фанаты C ++, вскоре должны начать называть lvalue reference:

std::string& operator[](int nIndex);

Это означает (по крайней мере, в этом контексте), что оно может появиться в левой частивыражение присваивания:

list[3] = "hi";

Но это поднимает все виды страшных проблем времени жизни.Если вы можете получить такую ​​ссылку, вы можете привязать ее к имени:

std::string& oops = list[3];

Теперь oops относится к чему-то во внутренних органах list.Это просто не очень безопасная ситуация, потому что некоторые операции с list приведут к тому, что oops станет бомбой замедленного действия, и вам нужно иметь подробную спецификацию того, как list работает, чтобы избежать этого.

C # работает по-другому.Разработчик класса, похожего на массив, получает возможность определить два отдельных определения для оператора [] как часть indexer (имя C # для реализации оператора индекса):

public string this[int index]
{
    get { /* return a string somehow */ }
    set { /* do something with implicit 'value' param */ }
}

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

ms[0] += addie;

Как уже говорилось во многих других ответах, это реализовано с использованием обычного оператора +, поэтому он расширяется до:

ms[0] = ms[0] + addie;

Но эти два вхождения ms[0] на самом деле являются вызовами совершенно разных методов.Это немного похоже на:

ms.SetAt(0, ms.GetAt(0) + addie);

Вот почему этот пример работает.Он заменяет объект, хранящийся в списке.Это тот шаг, который отсутствует в вашем нерабочем примере.

0 голосов
/ 23 февраля 2010

Реальный ответ заключается в том, что его оператор + не работает с объектом lhs, он создает копию и увеличивает ее. Если его + оператор фактически изменил объект, то он изменит значение в списке.

после

 M fromlist = ms[0];

fromlist и ms [0] указывают на один и тот же объект. В настоящее время

 fromlist += addie;

на самом деле

 fromlist = fromlist.op+(addie);

op + возвращает новый M с val = fromlist.val + addie.val

так что теперь fromlist и ms [0] указывают на разные вещи с разными значениями

0 голосов
/ 23 февраля 2010

Связь между составным присваиванием и простыми операторами в C # полностью отличается от C ++.

В C # составные операторы присваивания вызывают простой оператор, который помещает результат в новый экземпляр, а затем изменяет исходную переменнуюссылаться на новый экземпляр.Исходный референт не затрагивается.

В C ++ составные операторы присваивания изменяют референт.В большинстве случаев простой арифметический оператор начинает с клонирования LHS во временное, затем вызывает сложный оператор присваивания, изменяя временное.

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

0 голосов
/ 19 февраля 2010

Старайтесь избегать реализации операторов.Большую часть времени это не стоит хлопот и делает код более запутанным.Вместо этого реализуйте простые методы добавления / удаления.Кроме того, если вы добавите другие значения в ваш объект, его будет проще расширять.Используя оператор, вы используете семантику, связанную со всем объектом, чтобы воздействовать на его часть.Операторы обычно имеют больше смысла при использовании с struct.Структуры - это то, что вы должны использовать при реализации типов, которые должны вести себя как типы значений.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...