Dlaczego instrukcje przypisania zwracają wartość?


126

Jest to dozwolone:

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

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

W moim rozumieniu przypisanie s = ”Hello”;powinno tylko powodować “Hello”przypisanie s, ale operacja nie powinna zwracać żadnej wartości. Gdyby to była prawda, ((s = "Hello") != null)spowodowałoby to błąd, ponieważ nullzostałoby porównane z niczym.

Jakie jest uzasadnienie zezwalania instrukcjom przypisania na zwracanie wartości?


44
Dla odniesienia to zachowanie jest zdefiniowane w specyfikacji C # : „Wynikiem prostego wyrażenia przypisania jest wartość przypisana do lewego operandu. Wynik ma ten sam typ co lewy operand i jest zawsze klasyfikowany jako wartość”.
Michael Petrotta

1
@Skurmedel Nie jestem zwolennikiem krytyki, zgadzam się z tobą. Ale może dlatego, że uznano to za pytanie retoryczne?
jsmith

W przypisaniu pascal / delphi: = nic nie zwraca. Nienawidzę tego.
Andrey

20
Standard dla języków w gałęzi C. Zadania są wyrażeniami.
Hans Passant

„przypisanie ... powinno tylko powodować przypisanie„ Hello ”do s, ale operacja nie powinna zwracać żadnej wartości” - Dlaczego? „Jakie jest uzasadnienie zezwalania instrukcjom przypisania na zwracanie wartości?” - Ponieważ jest to przydatne, jak pokazują twoje własne przykłady. (Cóż, twój drugi przykład jest głupi, ale nim while ((s = GetWord()) != null) Process(s);nie jest).
Jim Balter

Odpowiedzi:


160

W moim rozumieniu przypisanie s = "Witaj"; powinno tylko powodować przypisanie „Hello” do s, ale operacja nie powinna zwracać żadnej wartości.

Twoje rozumienie jest w 100% błędne. Czy możesz wyjaśnić, dlaczego wierzysz w tę fałszywą rzecz?

Jakie jest uzasadnienie zezwalania instrukcjom przypisania na zwracanie wartości?

Po pierwsze, instrukcje przypisania nie generują wartości. Wyrażenia przypisania tworzą wartość. Wyrażenie przypisania to oświadczenie prawne; istnieje tylko kilka wyrażeń, które są instrukcjami prawnymi w C #: awaits of a expression, konstruowanie wystąpienia, inkrementacja, dekrementacja, wywołanie i wyrażenia przypisania mogą być używane tam, gdzie oczekiwana jest instrukcja.

W języku C # jest tylko jeden rodzaj wyrażenia, który nie generuje wartości, a mianowicie wywołanie czegoś, co zostało wpisane jako zwracające void. (Lub, równoważnie, oczekiwanie na zadanie bez skojarzonej wartości wyniku). Każdy inny rodzaj wyrażenia tworzy wartość, zmienną, odwołanie, dostęp do właściwości lub dostęp do zdarzenia i tak dalej.

Zauważ, że wszystkie wyrażenia, które są legalne jako oświadczenia, są przydatne ze względu na ich skutki uboczne . To jest tutaj kluczowy wgląd i myślę, że być może przyczyna twojej intuicji, że zadania powinny być stwierdzeniami, a nie wyrażeniami. Idealnie byłoby, gdybyśmy mieli dokładnie jeden efekt uboczny na instrukcję i żadnych skutków ubocznych w wyrażeniu. Jest to nieco dziwne, że boczne dokonania kod może być użyty w kontekście wyrażenie w ogóle.

Powodem dopuszczenia tej funkcji jest to, że (1) jest ona często wygodna i (2) jest idiomatyczna w językach typu C.

Można zauważyć, że postawiono pytanie: dlaczego jest to idiomatyczne w językach C-podobnych?

Dennisa Ritchiego nie można już zadawać, niestety, ale przypuszczam, że przypisanie prawie zawsze pozostawia wartość, która została właśnie przypisana w rejestrze. C jest językiem bardzo zbliżonym do maszyny. Wydaje się prawdopodobne i zgodne z projektem C, że istnieje funkcja języka, która zasadniczo oznacza „nadal używaj wartości, którą właśnie przypisałem”. Bardzo łatwo jest napisać generator kodu dla tej funkcji; po prostu nadal używasz rejestru, który przechował przypisaną wartość.


1
Nie wiedziałem, że wszystko, co zwraca wartość (nawet konstrukcja instancji jest uważane za wyrażenie)
user437291

9
@ user437291: Jeśli konstrukcja instancji nie była wyrażeniem, nie można było nic zrobić ze skonstruowanym obiektem - nie można było go do czegoś przypisać, nie można było przekazać do metody ani wywołać żadnych metod na tym. Byłoby całkiem bezużyteczne, prawda?
Timwi

1
Zmiana zmiennej jest w rzeczywistości „tylko” efektem ubocznym operatora przypisania, który, tworząc wyrażenie, zwraca wartość jako główny cel.
mk12

2
„Twoje rozumienie jest w 100% błędne”. - Chociaż użycie języka angielskiego przez OP nie jest najlepsze, jego / jej "zrozumienie" dotyczy tego, co powinno być, wyrażania opinii, więc nie jest to coś, co może być "w 100% niepoprawne" . Niektórzy projektanci języków zgadzają się z zasadą „powinien” w PO i przypisują je bez wartości lub w inny sposób zabraniają im bycia podwyrażeniami. Myślę, że jest to przesadna reakcja na =/ ==literówka, którą można łatwo rozwiązać, uniemożliwiając użycie wartości, =chyba że jest ona umieszczona w nawiasach. np. if ((x = y))lub if ((x = y) == true)jest dozwolone, ale if (x = y)nie jest.
Jim Balter

„Nie wiedziałem, że wszystko, co zwraca wartość… jest uważane za wyrażenie” - Hmm… wszystko, co zwraca wartość, jest uważane za wyrażenie - te dwa elementy są praktycznie synonimami.
Jim Balter

44

Nie podałeś odpowiedzi? Ma to na celu włączenie dokładnie tego rodzaju konstrukcji, o których wspomniałeś.

Typowym przypadkiem, w którym używana jest ta właściwość operatora przypisania, jest odczytywanie wierszy z pliku ...

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

1
+1 To musi być jedno z najbardziej praktycznych zastosowań tej konstrukcji
Mike Burton

11
Naprawdę podoba mi się to w przypadku naprawdę prostych inicjatorów właściwości, ale musisz być ostrożny i narysować linię dla „prostego” niskiego dla czytelności: return _myBackingField ?? (_myBackingField = CalculateBackingField());Dużo mniej plewy niż sprawdzanie wartości null i przypisywanie.
Tom Mayfield,

34

Moim ulubionym zastosowaniem wyrażeń przypisania jest leniwie inicjowane właściwości.

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

5
Dlatego tak wiele osób zasugerowało ??=składnię.
konfigurator

27

Po pierwsze, pozwala łączyć zadania w łańcuchy, jak w przykładzie:

a = b = c = 16;

Po drugie, pozwala przypisać i sprawdzić wynik w jednym wyrażeniu:

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

Obydwa są prawdopodobnie wątpliwe, ale z pewnością są ludzie, którzy lubią te konstrukcje.


5
Tworzenie łańcuchów +1 nie jest kluczową funkcją, ale miło jest widzieć, że „po prostu działa”, gdy tak wiele rzeczy nie działa.
Mike Burton,

+1 do tworzenia łańcuchów również; Zawsze myślałem o tym jako o specjalnym przypadku obsługiwanym przez język, a nie o naturalnym efekcie ubocznym „wyrażeń zwracających wartości”.
Caleb Bell

2
Pozwala także na użycie przypisania w zwrotach:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz

14

Oprócz wspomnianych już powodów (łańcuch przypisania, ustawianie i testowanie w pętli while itp.), Aby poprawnie używać usinginstrukcji, potrzebujesz tej funkcji:

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

MSDN odradza deklarowanie jednorazowego obiektu poza instrukcją using, ponieważ pozostanie on w zakresie nawet po jego usunięciu (zobacz artykuł MSDN, do którego odsyłam).


4
Nie sądzę, że jest to naprawdę łańcuch zadań jako taki.
kenny

1
@kenny: Uh ... Nie, ale ja też nigdy nie twierdziłem, że tak było. Jak powiedziałem, poza wspomnianymi już powodami - które obejmują łańcuch przypisań - instrukcja using byłaby znacznie trudniejsza w użyciu, gdyby nie fakt, że operator przypisania zwraca wynik operacji przypisania. To wcale nie jest związane z łańcuchem przydziałów.
Martin Törnwall

1
Ale jak zauważył Eric powyżej, „instrukcje przypisania nie zwracają wartości”. W rzeczywistości jest to po prostu składnia językowa instrukcji using, dowodem na to jest to, że w instrukcji using nie można używać „wyrażenia przypisania”.
kenny

@kenny: Przepraszam, moja terminologia była wyraźnie błędna. Zdaję sobie sprawę, że instrukcja using mogłaby zostać zaimplementowana bez ogólnego wsparcia dla wyrażeń przypisania zwracających wartość. W moich oczach byłoby to jednak strasznie niekonsekwentne.
Martin Törnwall

2
@kenny: Właściwie można użyć instrukcji definicji i przypisania lub dowolnego wyrażenia w using. Wszystkie one są legalne: using (X x = new X()), using (x = new X()), using (x). Jednak w tym przykładzie zawartość instrukcji using to specjalna składnia, która w ogóle nie polega na przypisaniu zwracającym wartość - Font font3 = new Font("Arial", 10.0f)nie jest wyrażeniem i nie jest poprawna w żadnym miejscu, w którym oczekuje się wyrażeń.
konfigurator

10

Chciałbym rozwinąć konkretną kwestię, którą Eric Lippert poruszył w swojej odpowiedzi i skierować uwagę na konkretną okazję, która w ogóle nie została poruszona przez nikogo innego. Eric powiedział:

[...] przypisanie prawie zawsze pozostawia wartość, która została właśnie przypisana w rejestrze.

Chciałbym powiedzieć, że przypisanie zawsze pozostawi wartość, którą próbowaliśmy przypisać do naszego lewego operandu. Nie tylko „prawie zawsze”. Ale nie wiem, ponieważ nie znalazłem tego problemu skomentowanego w dokumentacji. Teoretycznie może to być bardzo skuteczna zaimplementowana procedura „pozostawienia” i nie ponownej oceny lewego operandu, ale czy jest wydajna?

„Wydajne” tak dla wszystkich przykładów, które zostały do ​​tej pory skonstruowane w odpowiedziach w tym wątku. Ale wydajne w przypadku właściwości i indeksatorów, które używają metod dostępu get i set? Ani trochę. Rozważ ten kod:

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

Tutaj mamy właściwość, która nie jest nawet opakowaniem dla zmiennej prywatnej. Za każdym razem, gdy zostanie wezwany, zwróci prawdę, gdy ktoś spróbuje ustalić swoją wartość, nie powinien nic zrobić. Zatem ilekroć ta właściwość zostanie oceniona, będzie prawdomówny. Zobaczmy co się stanie:

Test test = new Test();

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

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

Zgadnij, co to drukuje? To drukuje Unexpected!!. Jak się okazuje, faktycznie wywoływany jest set accessor, który nic nie robi. Ale potem metoda get nigdy nie jest w ogóle wywoływana. Przypisanie to po prostu pozostawia falsewartość, którą próbowaliśmy przypisać naszej własności. Ta falsewartość jest obliczana przez instrukcję if.

Skończę na przykładzie z prawdziwego świata , dzięki któremu zbadałem ten problem. Zrobiłem indeksator, który był wygodnym opakowaniem dla kolekcji ( List<string>), którą moja klasa miała jako zmienną prywatną.

Parametr wysyłany do indeksatora był łańcuchem, który miał być traktowany jako wartość w mojej kolekcji. Metoda dostępu get zwróciłaby po prostu prawdę lub fałsz, gdyby ta wartość istniała na liście, czy nie. Zatem List<T>.Containsmetoda get była kolejnym sposobem wykorzystania metody.

Jeśli metoda akcesora set indeksatora została wywołana z łańcuchem znaków jako argumentem, a prawy operand truebyłby bool , dodałby ten parametr do listy. Ale jeśli ten sam parametr falsezostałby wysłany do metody dostępu, a właściwy operand byłby wartością logiczną , zamiast tego usunąłby element z listy. W ten sposób zestaw akcesorów został użyty jako wygodna alternatywa dla obu List<T>.Addi List<T>.Remove.

Wydawało mi się, że mam zgrabne i kompaktowe „API” opakowujące listę moją własną logiką zaimplementowaną jako brama. Z pomocą samego indeksatora mogłem zrobić wiele rzeczy za pomocą kilku zestawów naciśnięć klawiszy. Na przykład, jak mogę spróbować dodać wartość do mojej listy i sprawdzić, czy się tam znajduje? Myślałem, że to jedyna niezbędna linia kodu:

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

Ale jak pokazał mój wcześniejszy przykład, metoda get, która powinna sprawdzić, czy wartość rzeczywiście znajduje się na liście, nie została nawet wywołana. trueWartość zawsze pozostawione skutecznie niszczyć cokolwiek logika miałem realizowany w moim akcesor get.


Bardzo ciekawa odpowiedź. I to sprawiło, że pomyślałem pozytywnie o rozwoju opartym na testach.
Elliot

1
Chociaż jest to interesujące, jest to komentarz do odpowiedzi Erica, a nie odpowiedź na pytanie PO.
Jim Balter

Nie spodziewałbym się, że próba wyrażenia przypisania najpierw uzyska wartość właściwości docelowej. Jeśli zmienna jest zmienna, a typy są zgodne, dlaczego miałoby to mieć znaczenie, jaka była stara wartość? Po prostu ustaw nową wartość. Nie jestem jednak fanem przydziału w testach IF. Czy zwraca prawdę, ponieważ przypisanie się powiodło, czy jest to jakaś wartość, np. Dodatnia liczba całkowita, która jest wymuszana na prawdę, czy też dlatego, że przypisujesz wartość logiczną? Niezależnie od implementacji logika jest niejednoznaczna.
Davos

6

Gdyby przypisanie nie zwróciło wartości, linia a = b = c = 16również nie zadziała.

while ((s = readLine()) != null)Czasami przydatna może być również umiejętność pisania takich rzeczy .

Więc powodem, dla którego przypisanie zwraca przypisaną wartość, jest umożliwienie ci zrobienia tych rzeczy.


Nikt nie wspomniał o wadach - przyszedłem tutaj, zastanawiając się, dlaczego przypisania nie mogą zwracać wartości null, aby typowy błąd przypisania i porównania „if (something = 1) {}” nie mógł się skompilować, a nie zawsze zwracał true. Teraz widzę, że spowodowałoby to ... inne problemy!
Jon

1
@Jon temu błędowi można łatwo zapobiec, blokując zezwolenie lub ostrzegając o =wystąpieniu w wyrażeniu, które nie jest ujęte w nawiasach (nie licząc parens samej instrukcji if / while). gcc daje takie ostrzeżenia i tym samym zasadniczo wyeliminował tę klasę błędów z programów C / C ++, które są z nim kompilowane. Szkoda, że ​​inni twórcy kompilatorów poświęcili tak mało uwagi temu i kilku innym dobrym pomysłom w gcc.
Jim Balter

4

Myślę, że nie rozumiesz, jak parser będzie interpretował tę składnię. Przypisanie zostanie ocenione jako pierwsze , a wynik zostanie następnie porównany z wartością NULL, tj. Instrukcja jest równoważna z:

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

Jak wskazywali inni, wynikiem przypisania jest przypisana wartość. Trudno mi sobie wyobrazić korzyści z posiadania

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

i

s = "Hello";
s != null;

nie są równoważne ...


Chociaż masz rację z praktycznego punktu widzenia, że ​​twoje dwa wiersze są równoważne, twoje stwierdzenie, że przypisanie nie zwraca wartości, nie jest technicznie prawdziwe. Rozumiem, że wynikiem przypisania jest wartość (zgodnie ze specyfikacją C #) iw tym sensie nie różni się od, powiedzmy, operatora dodawania w wyrażeniu takim jak 1 + 2 + 3
Steve Ellinger

3

Myślę, że głównym powodem jest (celowe) podobieństwo z C ++ i C.Sprawdzenie, że operator przypisania (i wiele innych konstrukcji językowych) zachowuje się tak, jak ich odpowiedniki w C ++, po prostu kieruje się zasadą najmniejszego zaskoczenia, a każdy programista pochodzący z innego curly- język nawiasów może ich używać bez poświęcania dużo uwagi. Łatwość przyswojenia przez programistów C ++ była jednym z głównych celów projektowych dla C #.


2

Z dwóch powodów, które umieścisz w swoim poście
1), więc możesz zrobić a = b = c = 16
2), abyś mógł sprawdzić, czy zadanie się powiodło if ((s = openSomeHandle()) != null)


1

Fakt, że „a ++” lub „printf („ foo ”)” może być użyteczny jako samodzielna instrukcja lub jako część większego wyrażenia oznacza, że ​​C musi uwzględniać możliwość, że wynik wyrażenia może, ale nie musi, być używany. Biorąc to pod uwagę, istnieje ogólne przekonanie, że wyrażenia, które mogą pożytecznie „zwracać” wartość, równie dobrze mogą to robić. Tworzenie łańcuchów przypisań może być nieco „interesujące” w C, a nawet bardziej interesujące w C ++, jeśli wszystkie dane zmienne nie mają dokładnie tego samego typu. Takich zwyczajów prawdopodobnie najlepiej unikać.


1

Kolejny świetny przykład użycia, używam tego cały czas:

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

0

Dodatkową zaletą, której nie widzę w odpowiedziach tutaj, jest to, że składnia przypisania jest oparta na arytmetyce.

Teraz x = y = b = c = 2 + 3oznacza coś innego w arytmetyce niż język w stylu C; w arytmetycznej jego twierdzenia, możemy stwierdzić, że x jest równa y itd. oraz w języku C-stylu jest to instrukcja, która sprawia, że x równe y itd. Po to jest wykonywany.

To powiedziawszy, wciąż istnieje wystarczająca relacja między arytmetyką a kodem, że nie ma sensu blokować tego, co jest naturalne w arytmetyce, chyba że istnieje dobry powód. (Inną rzeczą, którą języki w stylu C przejęły z użycia symbolu równości, jest użycie == do porównania równości. Tutaj jednak, ponieważ prawy most == zwraca wartość, tego rodzaju tworzenie łańcucha byłoby niemożliwe.)


@JimBalter Zastanawiam się, czy za pięć lat mógłbyś faktycznie argumentować przeciwko.
Jon Hanna

@JimBalter nie, Twój drugi komentarz jest w rzeczywistości kontrargumentem i to rozsądnym. Moja myślowa riposta była taka, że ​​twój pierwszy komentarz nie był. Mimo to, podczas nauki arytmetyki uczymy a = b = coznacza, że aa bi cto samo. Ucząc się języka C stylu dowiadujemy się, że po a = b = c, ai bto samo jak c. Z pewnością istnieje różnica w semantyce, jak sama moja odpowiedź mówi, ale mimo to, gdy jako dziecko po raz pierwszy nauczyłem się programować w języku, który używał =do przydziału, ale nie pozwalałem na a = b = cto, wydawało mi się to nierozsądne…
Jon Hanna

… Nieuniknione, ponieważ ten język był również używany =do porównywania równości, więc a = b = cmusiałoby to oznaczać, co a = b == coznacza w językach w stylu C. Okazało się, że tworzenie łańcuchów dozwolone w C jest o wiele bardziej intuicyjne, ponieważ mogłem narysować analogię do arytmetyki.
Jon Hanna

0

Lubię używać wartości zwracanej przypisania, gdy muszę zaktualizować kilka rzeczy i zwrócić, czy były jakieś zmiany, czy nie:

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;

Uważaj jednak. Możesz pomyśleć, że możesz to skrócić do tego:

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

Ale to faktycznie przestanie oceniać instrukcje lub po znalezieniu pierwszej prawdziwej. W tym przypadku oznacza to, że przestaje przypisywać kolejne wartości, gdy przypisze pierwszą inną wartość.

Zobacz https://dotnetfiddle.net/e05Rh8, aby się z tym pobawić

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.