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

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

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

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

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

В чем заключается причина, позволяющая операторам присваивания возвращать значение?

Ответ 1

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

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

В чем заключается причина, позволяющая операторам присваивания возвращать значение?

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

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

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

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

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

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

Ответ 2

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

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

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...

Ответ 3

Мое любимое использование выражений присваивания для лениво инициализированных свойств.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}

Ответ 4

Во-первых, он позволяет вам цепочки ваших назначений, как в вашем примере:

a = b = c = 16;

Для другого, он позволяет назначать и проверять результат в одном выражении:

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

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

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!!. Как оказалось, набор аксессуаров действительно называется, что ничего не делает. Но после этого приемник доступа никогда не называется вообще. Назначение просто оставляет значение false, которое мы пытались присвоить нашему свойству. И это значение false - это то, что оценивает оператор if.

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

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

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

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

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

Но, как показал мой предыдущий пример, get accessor, который должен видеть, действительно ли значение действительно находится в списке, даже не вызывался. Значение true всегда оставалось позади, эффективно уничтожая любую логику, которую я реализовал в своем get accessor.

Ответ 7

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

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

Таким образом, причина того, что присвоение присваивания возвращает назначенное значение, - это позволить вам делать эти вещи.

Ответ 8

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

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

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

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

и

s = "Hello";
s != null;

не эквивалентен...

Ответ 9

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

Ответ 10

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

Ответ 11

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

Ответ 12

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

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

Это говорит о том, что между арифметикой и кодом все еще достаточно отношения, что нет смысла отрицать, что естественно в арифметике, если нет веской причины. (Другое дело, что языки C-стиля, взятые из использования символа равенства, - это использование == для сравнения равенства. Здесь, хотя из-за того, что right-most == возвращает значение, такая цепочка будет невозможна.)

Ответ 13

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

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once