Почему операторы присваивания возвращают значение?
Это разрешено:
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