Чому оператори присвоєння повертають значення?


126

Це дозволено:

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

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

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

Які міркування дозволяють операторам присвоєння повертати значення?


44
Для довідки така поведінка визначена у специфікації C # : "Результатом простого виразу присвоєння є значення, присвоєне лівому операнду. Результат має той же тип, що і лівий операнд, і завжди класифікується як значення."
Майкл Петротта

1
@Skurmedel Я не прихильник, я з вами згоден. Але, може, тому, що це вважалося риторичним питанням?
jsmith

У призначенні pascal / delphi: = нічого не повертає. я це ненавиджу.
Андрій

20
Стандартний для мов у галузі C. Призначення - це вирази.
Ганс Пасант

"присвоєння ... повинно спричинити присвоєння лише" Привіт "s, але операція не повинна повертати жодного значення" - Чому? "Які міркування дозволяють операторам присвоєння повертати значення?" - Тому що це корисно, як демонструють власні приклади. (Ну, ваш другий приклад - дурний, але while ((s = GetWord()) != null) Process(s);це не так).
Джим Балтер

Відповіді:


160

Наскільки я розумію, завдання s = "Привіт"; повинно спричинити призначення привіт лише s, але операція не повинна повертати жодне значення.

Ваше розуміння на 100% невірно. Чи можете ви пояснити, чому ви вважаєте цю помилкову річ?

Які міркування дозволяють операторам присвоєння повертати значення?

По-перше, заяви про призначення не приносять значення. Призначення вираз виробляє значення. Вираз призначення - це юридична заява; у C # є лише кілька виразів, які є юридичними твердженнями: очікування виразів, побудова екземпляра, приріст, декремент, виклики та призначення виразів можуть використовуватися там, де очікується заява.

У C # є лише один вид вираження, який не створює якогось значення, а саме викликання чогось, що набирається як повернення порожнечі. (Або, що еквівалентно, очікування завдання без пов'язаного значення результату.) Кожен інший вид вираження створює значення чи змінну, або посилання, доступ до властивостей чи доступ до події тощо.

Зауважте, що всі вирази, які є законними як заяви, корисні для їх побічних ефектів . Це ключове розуміння тут, і я думаю, що, можливо, причиною вашої інтуїції є те, що завдання повинні бути висловлюваннями, а не виразами. В ідеалі, у нас було б рівно один побічний ефект на висловлювання, а жодних побічних ефектів у виразі. Це є трохи дивним , що пліч здійснення код можуть бути використані в контексті вираження взагалі.

Міркування, що дозволяють використовувати цю функцію, полягає в тому, що (1) це часто зручно і (2) є ідіоматичним на мовах, подібних С.

Можна зауважити, що питання задається: чому це ідіоматика на мовах подібних С?

На жаль, Денніс Річі більше не доступний для запиту, але я здогадуюсь, що присвоєння майже завжди залишає після себе значення, яке було щойно призначене в реєстрі. C - це дуже близька до машини мова. Це здається правдоподібним і відповідно до дизайну C, що існує мовна функція, яка в основному означає "продовжувати використовувати значення, яке я щойно призначив". Написати генератор коду для цієї функції дуже просто; ви просто продовжуєте використовувати реєстр, який зберігав призначене значення.


1
Не знав, що все, що повертає значення (навіть побудова екземпляра вважається виразом)
user437291

9
@ user437291: Якщо побудова екземпляра не була виразом, ви нічого не могли зробити зі сконструйованим об'єктом - ви не могли призначити його чомусь, ви не могли передати його в метод і не могли викликати жодні методи на ньому. Було б досить марно, чи не так?
Тімві

1
Зміна змінної насправді є "лише" побічним ефектом оператора присвоєння, який, створюючи вираз, дає значення як його основне призначення.
mk12

2
"Ваше розуміння на 100% невірно". - Хоча використання англійською мовою в ОП не є найбільшим, його / її розуміння полягає в тому, що повинно бути так, що робить його думкою, тож це не така річ, як може бути "100% невірною". . Деякі мовні розробники погоджуються з "слід" ОП, і виконання завдань не має ніякого значення, або іншим чином забороняє їм бути субпрекспресіями. Я думаю, що це надмірна реакція на =/ ==typo, яку легко усунути, заборонивши використовувати значення, =якщо воно не поставлено в дужки. наприклад, if ((x = y))або if ((x = y) == true)дозволено, але if (x = y)це не так.
Джим Балтер

"Не усвідомлював, що все, що повертає значення ..., вважається виразом" - Хм ... все, що повертає значення, вважається виразом - вони є практично синонімами.
Джим Балтер

44

Ви не дали відповіді? Це вмикати саме ті конструкції, про які ви згадали.

Поширений випадок, коли використовується ця властивість оператора призначення - це зчитування рядків з файлу ...

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

1
+1 Це має бути одним із найбільш практичних застосувань цієї конструкції
Майк Бертон,

11
Мені це дуже подобається для дійсно простих ініціалізаторів властивостей, але ви повинні бути обережними та малювати лінію для "простого" низького для читабельності: return _myBackingField ?? (_myBackingField = CalculateBackingField());Набагато менший розбір, ніж перевірка на нуль та призначення.
Том Мейфілд,

34

Моє улюблене використання виразів призначення - для ліниво ініціалізованих властивостей.

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 відмовляє оголошувати об’єкт одноразового використання за межами використовуючого оператора, оскільки він залишатиметься в обсягах навіть після його вилучення (див. Статтю MSDN, яку я пов’язував).


4
Я не думаю, що це насправді таке завдання, як таке.
kenny

1
@kenny: Так ... Ні, але я ніколи не стверджував, що це було. Як я вже говорив, окрім уже згаданих причин - що включає ланцюжок присвоєння - використання оператора було б набагато складніше, якби не той факт, що оператор присвоєння повертає результат операції призначення. Це взагалі не пов’язано із ланцюжком завдань.
Мартін Торнволл

1
Але, як Ерік зазначив вище, "заяви про призначення не повертають значення". Це насправді просто синтаксис мови використовуючого оператора, доказом цього є те, що не можна використовувати "вираз призначення" в операторі, що використовує.
kenny

@kenny: Вибачте, моя термінологія була явно неправильною. Я розумію, що оператор use може бути реалізований без мови, яка має загальну підтримку для виразів призначення, що повертають значення. На моїх очах це було б дуже непослідовно.
Мартін Торнволл

2
@kenny: Насправді, можна використовувати або твердження про визначення, і призначення, або будь-який вираз у using. Всі вони є законними: using (X x = new X()), using (x = new X()), using (x). Однак у цьому прикладі вміст використовуючого оператора є спеціальним синтаксисом, який взагалі не покладається на призначення, що повертає значення - Font font3 = new Font("Arial", 10.0f)не є виразом і недійсний у будь-якому місці, яке очікує вирази.
конфігуратор

10

Я хотів би детальніше зупинитися на конкретному питанні Еріка Ліпперта у своїй відповіді та поставити прожектор на певний випадок, який взагалі ніхто не торкався. Ерік сказав:

[...] призначення майже завжди залишає після себе значення, яке було щойно призначене в регістрі.

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

"Ефективний" так для всіх прикладів, побудованих на даний момент у відповідях цієї теми. Але ефективні у випадку властивостей та індексаторів, які використовують аксесуари для отримання та встановлення? Зовсім ні. Розглянемо цей код:

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>), яку мій клас мав як приватну змінну.

Параметр, надісланий індексатору, являв собою рядок, який повинен розглядатися як значення в моїй колекції. Access access просто поверне значення true або false, якщо це значення існувало у списку чи ні. Таким чином, отримати аксесуар був ще одним способом використання List<T>.Containsметоду.

Якщо встановлений аксесуар набору індексатора був вказаний рядком як аргументом, а правильний операнд - bool true, він додасть цей параметр до списку. Але якби той самий параметр був надісланий аксесуару, а правий операнд - bool false, він замість цього видалить елемент зі списку. Таким чином, встановлений аксесуар використовувався як зручна альтернатива як List<T>.Addі List<T>.Remove.

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

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

Але, як показав мій попередній приклад, отримати аксесуар, який повинен бачити, чи є в списку значення справді, навіть не називався. trueЗначення завжди було залишено позаду ефективно знищуючи будь-яку логіку я здійснив в моєму ОТРИМАТИ аксессор.


Дуже цікава відповідь. І це змусило мене позитивно думати про тестові розробки.
Елліот

1
Хоча це цікаво, це коментар на відповідь Еріка, а не відповідь на питання ОП.
Джим Балтер

Я не очікував, що спробу призначення виразу спочатку отримає значення цільового властивості. Якщо змінна є змінною, а типи відповідають, чому б це мало значення, яке було старе значення? Просто встановіть нове значення. Я не прихильник присвоювання тестів на ІФ. Чи повертається справжнє тому, що призначення було успішним, або це якесь значення, наприклад, додатне ціле число, примусове до істинного, або тому, що ви призначаєте булеве значення? Незалежно від реалізації логіка неоднозначна.
Давос

6

Якщо призначення не поверне значення, рядок a = b = c = 16також не працюватиме.

Також вміти писати такі речі while ((s = readLine()) != null)Іноді може бути корисним іноді.

Отже, причина, за якою дозволити призначенню повернути призначене значення, полягає в тому, щоб ви могли робити це.


Ніхто не згадував недоліки - я прийшов сюди, цікаво, чому присвоєння не можуть повернути нуль для того, щоб загальна присвоєння та порівняння помилка "якщо (щось = 1) {}" не вдалося скласти, а не завжди повертала істину. Тепер я бачу, що це створило б ... інші питання!
Jon

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 #.


2

З двох причин, які ви включаєте у свою посаду
1), щоб ви могли зробити це a = b = c = 16
2), щоб ви могли перевірити, чи доручення вдалося if ((s = openSomeHandle()) != null)


1

Той факт, що 'a ++' або 'printf ("foo") "може бути корисним або як автономний вислів, або як частина більшого виразу, означає, що C повинен передбачати можливість того, що результати вираження можуть бути, а можуть і не бути б / в. Враховуючи це, існує загальне уявлення про те, що вирази, які можуть корисно "повернути" значення, можуть це зробити. Ланцюг присвоєння може бути трохи "цікавим" в C, а ще цікавіше в C ++, якщо всі розглянуті змінні не мають точно одного типу. Таких звичаїв, мабуть, найкраще уникати.


1

Ще один чудовий приклад використання, я використовую це постійно:

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

0

Додатковою перевагою, яку я не бачу у наведених тут відповідях, є те, що синтаксис присвоєння базується на арифметиці.

Тепер x = y = b = c = 2 + 3означає щось інше в арифметиці, ніж мова в стилі С; в арифметиці його твердження ми констатуємо, що x дорівнює y і т. д., а мовою стилю С це інструкція, яка робить x рівним y і т.д. після її виконання.

Це означає, що між арифметикою та кодом все ще достатньо співвідношення, щоб не було сенсу забороняти те, що є природним у арифметиці, якщо немає вагомих причин. (Інша річ, яку мови С-стилю взяли за використання символу рівності, - це використання == для порівняння рівності. Хоча, тому що право-більш == повертає значення, такий тип ланцюга був би неможливим.)


@JimBalter Цікаво, чи за 5 років ви насправді можете аргументувати проти.
Джон Ханна

@JimBalter ні, ваш другий коментар насправді є протилежним аргументом, і здоровим. Мій роздум був тому, що ваш перший коментар не був. Тим НЕ менше, при вивченні арифметичного ми дізнаємося , a = b = cозначає , що aі bта cодне і те ж. Вивчаючи мову стилю С, ми дізнаємось про це після a = b = c, aі bце те саме, що і c. У семантиці, безумовно, є різниця, як говорить сама моя відповідь, але все ж таки, коли в дитинстві я вперше навчився програмувати мовою, якою я користувався =для виконання завдань, але не допустив a = b = cцього, мені здалося нерозумним…
Джон Ханна,

... неминуче, оскільки ця мова також використовується =для порівняння рівності, тому це a = b = cповинно означати, що a = b == 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");

Але це фактично перестане оцінювати або твердження після того, як воно знайде перше правдиве. У цьому випадку це означає, що він припиняє присвоювати наступні значення, як тільки присвоює перше значення, яке є іншим.

Дивіться https://dotnetfiddle.net/e05Rh8, щоб пограти з цим

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.