Почему операторы присваивания возвращают значение?

127

Это разрешено:

int a, b, c;
a = b = c = 16;

string s = null;
while ((s = "Hello") != null) ;

Насколько я понимаю, присвоение s = ”Hello”;должно вызывать “Hello”только назначение s, но операция не должна возвращать никакого значения. Если бы это было правдой, то ((s = "Hello") != null)выдала бы ошибку, так nullкак сравнивалась бы ни с чем.

В чем причина разрешения операторам присваивания возвращать значение?

user437291
источник
45
Для справки, это поведение определено в спецификации C # : «Результатом простого выражения присваивания является значение, присвоенное левому операнду. Результат имеет тот же тип, что и левый операнд, и всегда классифицируется как значение».
Майкл Петротта,
1
@Skurmedel Я не противник, я согласен с вами. Но, может быть, потому, что это был риторический вопрос?
jsmith
В назначении паскаль / дельфи: = ничего не возвращает. Я ненавижу это.
Андрей
20
Стандарт для языков в ветке C. Присвоения - это выражения.
Ганс Пассан,
«назначение ... должно вызывать присвоение s только« Hello », но операция не должна возвращать никакого значения» - Почему? «В чем причина разрешения операторам присваивания возвращать значение?» - Потому что это полезно, как показывают ваши собственные примеры. (Ну, ваш второй пример глуп, но while ((s = GetWord()) != null) Process(s);это не так).
Джим Балтер

Ответы:

160

Насколько я понимаю, присвоение s = "Hello"; должно вызывать присвоение s только "Hello", но операция не должна возвращать никакого значения.

Ваше понимание неверно на 100%. Можете ли вы объяснить, почему вы верите в эту ложь?

В чем причина разрешения операторам присваивания возвращать значение?

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

В C # есть только один вид выражения, который не производит какого-либо значения, а именно вызов чего-то, типизированного как возвращающий void. (Или, что то же самое, ожидание задачи без связанного значения результата.) Любой другой вид выражения производит значение или переменную, или ссылку, или доступ к свойству, или доступ к событию, и так далее.

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

Причина, по которой разрешена эта функция, заключается в том, что (1) она часто удобна и (2) она идиоматична для C-подобных языков.

Можно заметить, что возник вопрос: почему эта идиоматика в C-подобных языках?

К сожалению, Деннис Ричи больше не доступен для запроса, но я предполагаю, что присвоение почти всегда оставляет после себя значение, которое было только что присвоено в регистре. C - это язык, очень близкий к машинному. Кажется правдоподобным и согласуется с дизайном C, что есть функция языка, которая в основном означает «продолжать использовать значение, которое я только что назначил». Для этой функции очень легко написать генератор кода; вы просто продолжаете использовать регистр, в котором хранится присвоенное значение.

Эрик Липперт
источник
1
Не знал, что что-либо, что возвращает значение (даже конструкция экземпляра считается выражением)
user437291
9
@ user437291: Если конструкция экземпляра не была выражением, вы не могли ничего сделать со сконструированным объектом - вы не могли присвоить его чему-либо, вы не могли передать его в метод и вы не могли вызвать какие-либо методы в теме. Было бы бесполезно, правда?
Timwi
1
Изменение переменной на самом деле является «просто» побочным эффектом оператора присваивания, который при создании выражения дает значение в качестве своей основной цели.
mk12
2
«Ваше понимание на 100% неверно». - Несмотря на то, что ОП использует английский язык не самым лучшим образом, его / ее «понимание» касается того, что должно быть в действительности, и выражает свое мнение, так что это не та вещь, которая может быть «на 100% неверной». , Некоторые разработчики языков соглашаются с «следует» OP и делают назначения не имеющими значения или иным образом запрещают им быть подвыражениями. Я думаю, что это чрезмерная реакция на опечатку =/ ==, которую легко устранить, запретив использовать значение, =если оно не заключено в скобки. например, if ((x = y))или if ((x = y) == true)разрешено, но if (x = y)нет.
Джим Балтер
«Не знал, что все, что возвращает значение ... считается выражением» - Хм ... все, что возвращает значение, считается выражением - эти два понятия практически синонимы.
Джим Балтер
44

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

Обычный случай, когда используется это свойство оператора присваивания, - это чтение строк из файла ...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...
Timwi
источник
1
+1 Это должно быть одно из самых практических применений этой конструкции
Майк Бертон
11
Мне очень нравится, что это действительно простые инициализаторы свойств, но вы должны быть осторожны и рисовать линию для «простого» минимума для удобочитаемости: return _myBackingField ?? (_myBackingField = CalculateBackingField());гораздо меньше хлопот, чем проверка на null и присваивание.
Tom Mayfield
35

Я больше всего использую выражения присваивания для лениво инициализированных свойств.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}
Натан Баулч
источник
5
Вот почему так много людей предложили этот ??=синтаксис.
конфигуратор
27

Во-первых, он позволяет вам связывать свои задания, как в вашем примере:

a = b = c = 16;

Во-вторых, это позволяет вам назначать и проверять результат в одном выражении:

while ((s = foo.getSomeString()) != null) { /* ... */ }

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

Майкл Берр
источник
5
+1 Цепочка не является важной функцией, но приятно видеть, что она «просто работает», когда так много всего не работает.
Майк Бертон,
+1 также для цепочки; Я всегда думал об этом как об особом случае, который обрабатывается языком, а не естественным побочным эффектом «выражений, возвращающих значения».
Калеб Белл
2
Это также позволяет вам использовать присваивание в ответах:return (HttpContext.Current.Items["x"] = myvar);
Серж Шульц,
14

Помимо уже упомянутых причин (цепочка назначений, установка и проверка в циклах while и т. Д.), Для правильного использования usingоператора вам понадобится эта функция:

using (Font font3 = new Font("Arial", 10.0f))
{
    // Use font3.
}

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

Мартин Торнуолл
источник
4
Я не думаю, что это действительно цепочка назначений как таковая.
Кенни
1
@kenny: Э ... Нет, но я тоже никогда не утверждал, что это так. Как я уже сказал, помимо уже упомянутых причин, в том числе цепочки присваивания, оператор using было бы намного сложнее использовать, если бы не тот факт, что оператор присваивания возвращает результат операции присваивания. На самом деле это вообще не связано с цепочкой назначений.
Мартин Торнуолл,
1
Но, как заметил Эрик выше, «операторы присваивания не возвращают значения». На самом деле это просто синтаксис языка оператора using, доказательством чего является то, что нельзя использовать «выражение присваивания» в операторе using.
Кенни
@kenny: Извините, моя терминология была явно неправильной. Я понимаю, что оператор using может быть реализован без общей поддержки в языке выражений присваивания, возвращающих значение. Хотя, на мой взгляд, это было бы ужасно непоследовательно.
Мартин Торнуолл,
2
@kenny: На самом деле можно использовать либо оператор определения и присваивания, либо любое выражение в using. Все они являются законными: using (X x = new X()), using (x = new X()), using (x). Однако в этом примере содержимое оператора using представляет собой специальный синтаксис, который вообще не полагается на присваивание, возвращающее значение - Font font3 = new Font("Arial", 10.0f)не является выражением и недопустимо в любом месте, которое ожидает выражения.
конфигуратор
10

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

[...] присвоение почти всегда оставляет значение, которое только что было присвоено в регистре.

Я хотел бы сказать, что присвоение всегда будет оставлять после себя значение, которое мы пытались присвоить нашему левому операнду. Не просто «почти всегда». Но я не знаю, потому что я не нашел комментария к этой проблеме в документации. Теоретически может быть очень эффективной реализованной процедурой «оставить позади» и не переоценивать левый операнд, но эффективна ли она?

«Эффективно» да для всех примеров, построенных на данный момент в ответах этой ветки. Но эффективно в случае свойств и индексаторов, использующих методы доступа get и set? Не за что. Рассмотрим этот код:

class Test
{
    public bool MyProperty { get { return true; } set { ; } }
}

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

Test test = new Test();

if ((test.MyProperty = false) == true)
    Console.WriteLine("Please print this text.");

else
    Console.WriteLine("Unexpected!!");

Угадайте, что он печатает? Он печатает Unexpected!!. Как оказалось, действительно вызывается метод доступа set, который ничего не делает. Но после этого метод доступа get вообще никогда не вызывается. Присваивание просто оставляет falseзначение, которое мы пытались присвоить нашему свойству. И это falseзначение оценивается оператором if.

Я закончу реальным примером, который побудил меня исследовать эту проблему. Я сделал индексатор, который был удобной оболочкой для collection ( List<string>), который мой класс имел в качестве частной переменной.

Параметр, отправленный индексатору, представлял собой строку, которую следовало рассматривать как значение в моей коллекции. Метод доступа get просто вернет true или false, если это значение существует в списке или нет. Таким образом, метод доступа get был еще одним способом использования List<T>.Containsметода.

Если метод доступа set индексатора был вызван со строкой в ​​качестве аргумента, а правый операнд был логическим значением true, он бы добавил этот параметр в список. Но если тот же параметр был отправлен в метод доступа, а правый операнд был логическим значением false, он вместо этого удалял бы элемент из списка. Таким образом, набор аксессуаров использовался как удобная альтернатива обоим List<T>.Addи List<T>.Remove.

Я думал, что у меня есть аккуратный и компактный «API», который оборачивает список моей собственной логикой, реализованной в качестве шлюза. С помощью одного только индексатора я мог многое делать с помощью нескольких нажатий клавиш. Например, как я могу попытаться добавить значение в свой список и убедиться, что оно там есть? Я думал, что это единственная необходимая строка кода:

if (myObject["stringValue"] = true)
    ; // Set operation succeeded..!

Но, как показал мой предыдущий пример, метод доступа get, который должен видеть, действительно ли значение находится в списке, даже не был вызван. trueЗначение всегда было оставлено позади эффективно уничтожая любую логику я осуществил в моем ПОЛУЧИТЬ аксессор.

Мартин Андерссон
источник
Очень интересный ответ. И это заставило меня положительно задуматься о разработке через тестирование.
Эллиот
1
Хотя это интересно, это комментарий к ответу Эрика, а не ответ на вопрос ОП.
Джим Балтер,
Я бы не ожидал, что попытка выражения присваивания сначала получит значение целевого свойства. Если переменная изменяема и типы совпадают, какое значение имеет старое значение? Просто установите новое значение. Но я не фанат задания в тестах IF. Возвращает ли он истину, потому что присвоение было успешным, или это какое-то значение, например положительное целое число, которое принудительно установлено на истину, или потому что вы назначаете логическое значение? Вне зависимости от реализации логика неоднозначная.
Давос
6

Если присваивание не вернуло значение, строка a = b = c = 16тоже не будет работать.

Также while ((s = readLine()) != null)иногда может быть полезно умение писать такие вещи .

Итак, причина, по которой присваивание возвращает присвоенное значение, заключается в том, чтобы позволить вам делать эти вещи.

sepp2k
источник
Никто не упоминал о недостатках - я пришел сюда, задаваясь вопросом, почему присваивания не могут возвращать null, чтобы обычная ошибка присваивания и сравнения 'if (something = 1) {}' не могла компилироваться, а не всегда возвращала true. Теперь я вижу, что это может создать ... другие проблемы!
Джон
1
@Jon эту ошибку можно легко предотвратить, запретив или предупредив о =появлении в выражении, которое не заключено в скобки (не считая скобок в самом выражении if / while). gcc выдает такие предупреждения и тем самым по существу устранил этот класс ошибок из программ C / C ++, которые скомпилированы с ним. Жаль, что другие разработчики компиляторов уделили так мало внимания этой и нескольким другим хорошим идеям в gcc.
Джим Балтер,
4

Я думаю, вы не понимаете, как парсер будет интерпретировать этот синтаксис. Сначала будет оценено присвоение , а затем результат будет сравнен с NULL, т. Е. Оператор эквивалентен:

s = "Hello"; //s now contains the value "Hello"
(s != null) //returns true.

Как указывали другие, результатом присвоения является присвоенное значение. Мне трудно представить себе преимущества наличия

((s = "Hello") != null)

и

s = "Hello";
s != null;

не быть эквивалентным ...

Дэн Дж
источник
Хотя с практической точки зрения вы правы, что ваши две строки эквивалентны, ваше утверждение о том, что присвоение не возвращает значение, технически неверно. Насколько я понимаю, результатом присваивания является значение (в соответствии со спецификацией C #), и в этом смысле он ничем не отличается от, скажем, оператора сложения в выражении типа 1 + 2 + 3
Стив Эллинджер,
3

Я думаю, что основная причина - это (преднамеренное) сходство с C ++ и C. Приведение оператора присваивания (и множества других языковых конструкций) к поведению их аналогов на C ++ просто следует принципу наименьшего удивления, и любой программист, пришедший из другого фигурного- язык скобок может использовать их, не особо задумываясь. Быть понятным программистам на C ++ было одной из основных целей разработки C #.

tdammers
источник
2

По двум причинам, которые вы указываете в своем сообщении:
1) чтобы вы могли сделать a = b = c = 16
2) чтобы вы могли проверить, выполнено ли задание if ((s = openSomeHandle()) != null)

JamesMLV
источник
1

Тот факт, что 'a ++' или 'printf ("foo")' могут быть полезны либо как автономный оператор, либо как часть большего выражения, означает, что C должен учитывать возможность того, что результаты выражения могут быть или не быть используемый. Учитывая это, существует общее представление о том, что выражения, которые могут с пользой «возвращать» значение, также могут это делать. Цепочка присвоений может быть немного «интересной» в C и даже более интересной в C ++, если все рассматриваемые переменные не имеют в точности один и тот же тип. Такого использования, вероятно, лучше всего избегать.

Supercat
источник
1

Еще один отличный пример использования, я использую его все время:

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once
jazzcat
источник
0

Дополнительное преимущество, которое я не вижу здесь в ответах, заключается в том, что синтаксис для назначения основан на арифметике.

Теперь x = y = b = c = 2 + 3в арифметике означает нечто иное, чем язык C-стиля; в арифметике это утверждение, мы утверждаем, что x равно y и т. д., а в языке C-стиля это инструкция, которая делает x равным y и т. д. после ее выполнения.

Тем не менее, между арифметикой и кодом все еще достаточно взаимосвязи, поэтому нет смысла запрещать то, что является естественным в арифметике, если нет веской причины. (Другая вещь, которую языки C-стиля взяли из использования символа равенства, - это использование == для сравнения на равенство. Хотя здесь, поскольку крайний правый == возвращает значение, такое связывание было бы невозможно.)

Джон Ханна
источник
@JimBalter Интересно, сможете ли вы через пять лет выступить против.
Джон Ханна
@JimBalter нет, ваш второй комментарий на самом деле является контраргументом, причем здравым. Моя мыслящая реакция была вызвана тем, что вашего первого комментария не было. Тем не менее, изучая арифметику, мы учимся, это a = b = cозначает, что aи bи cявляются одним и тем же. Изучая язык в стиле C, мы изучаем это после a = b = c, aи bэто то же самое, что и c. Конечно, есть разница в семантике, как говорит сам мой ответ, но все же, когда в детстве я впервые научился программировать на языке, который действительно использовался =для назначения, но не позволял, a = b = cэто казалось мне неразумным, хотя…
Джон Ханна,
… Неизбежно, потому что этот язык также используется =для сравнения на равенство, поэтому в нем a = b = cдолжно быть означать, что a = b == cозначает в языках C-стиля. Я обнаружил, что создание цепочек, разрешенных в C, намного более интуитивно понятно, потому что я мог провести аналогию с арифметикой.
Джон Ханна
0

Мне нравится использовать возвращаемое значение присваивания, когда мне нужно обновить кучу вещей и вернуть, были ли изменения или нет:

bool hasChanged = false;

hasChanged |= thing.Property != (thing.Property = "Value");
hasChanged |= thing.AnotherProperty != (thing.AnotherProperty = 42);
hasChanged |= thing.OneMore != (thing.OneMore = "They get it");

return hasChanged;

Но будьте осторожны. Вы можете подумать, что можете сократить его до этого:

return thing.Property != (thing.Property = "Value") ||
    thing.AnotherProperty != (thing.AnotherProperty = 42) ||
    thing.OneMore != (thing.OneMore = "They get it");

Но на самом деле это перестанет оценивать операторы or после того, как обнаружит первое истинное. В данном случае это означает, что он перестает присваивать последующие значения после того, как присваивает первое другое значение.

См. Https://dotnetfiddle.net/e05Rh8, чтобы поиграть с этим

Люк Кубат
источник