Почему операторы присваивания возвращают значение? - PullRequest
122 голосов
/ 27 сентября 2010

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

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

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

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

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

Ответы [ 13 ]

149 голосов
/ 28 сентября 2010

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

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

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

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

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

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

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

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

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

43 голосов
/ 27 сентября 2010

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

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

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...
31 голосов
/ 30 сентября 2010

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

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}
27 голосов
/ 27 сентября 2010

Например, он позволяет вам связывать свои назначения, как в вашем примере:

a = b = c = 16;

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

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

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

14 голосов
/ 27 сентября 2010

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

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

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

8 голосов
/ 09 июня 2012

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

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

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

«Эффективно» да для всех примеров, построенных до сих пор в ответах этого потока.Но эффективны ли в случае свойств и индексаторов, которые используют методы доступа 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.

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

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

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

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

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

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

6 голосов
/ 27 сентября 2010

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

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

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

4 голосов
/ 27 сентября 2010

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

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

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

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

и

s = "Hello";
s != null;

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

3 голосов
/ 27 сентября 2010

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

2 голосов
/ 27 сентября 2010

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

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