Perché le istruzioni di assegnazione restituiscono un valore?


126

Questo è permesso:

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

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

A mio avviso, l'assegnazione s = ”Hello”;dovrebbe causare solo l' “Hello”assegnazione s, ma l'operazione non dovrebbe restituire alcun valore. Se ciò fosse vero, allora ((s = "Hello") != null)produrrebbe un errore, dal momento che nullsarebbe paragonato a nulla.

Qual è il ragionamento alla base del consentire alle dichiarazioni di assegnazione di restituire un valore?


44
Per riferimento, questo comportamento è definito nella specifica C # : "Il risultato di una semplice espressione di assegnazione è il valore assegnato all'operando di sinistra. Il risultato ha lo stesso tipo dell'operando di sinistra ed è sempre classificato come valore."
Michael Petrotta,

1
@Skurmedel Non sono il downvoter, sono d'accordo con te. Ma forse perché si pensava che fosse una domanda retorica?
Jsmith,

Nell'assegnazione pascal / delphi: = non restituisce nulla. Lo odio.
Andrey,

20
Standard per le lingue nel ramo C. Le assegnazioni sono espressioni.
Hans Passant,

"L'assegnazione ... dovrebbe causare l'assegnazione di" Hello "a s, ma l'operazione non dovrebbe restituire alcun valore" - Perché? "Qual è il ragionamento alla base del consentire alle dichiarazioni di assegnazione di restituire un valore?" - Perché è utile, come dimostrano i tuoi esempi. (Bene, il tuo secondo esempio è sciocco, ma while ((s = GetWord()) != null) Process(s);non lo è).
Jim Balter,

Risposte:


160

A mio avviso, assegnazione s = "Hello"; dovrebbe solo assegnare "Hello" a s, ma l'operazione non dovrebbe restituire alcun valore.

La tua comprensione è errata al 100%. Puoi spiegare perché credi a questa cosa falsa?

Qual è il ragionamento alla base del consentire alle dichiarazioni di assegnazione di restituire un valore?

Prima di tutto, le istruzioni di assegnazione non producono un valore. Le espressioni di assegnazione producono un valore. Un'espressione di assegnazione è una dichiarazione legale; ci sono solo una manciata di espressioni che sono dichiarazioni legali in C #: in attesa di un'espressione possono essere utilizzate espressioni di costruzione, istanza di costruzione, incremento, decremento, invocazione e assegnazione.

Esiste un solo tipo di espressione in C # che non produce alcun tipo di valore, vale a dire l'invocazione di qualcosa che viene digitato come vuoto di ritorno. (O, equivalentemente, in attesa di un'attività senza valore di risultato associato.) Ogni altro tipo di espressione produce un valore o una variabile o un riferimento o un accesso alla proprietà o un accesso all'evento e così via.

Nota che tutte le espressioni legali come dichiarazioni sono utili per i loro effetti collaterali . Questa è l'intuizione chiave qui, e penso che forse la causa della tua intuizione che gli incarichi dovrebbero essere dichiarazioni e non espressioni. Idealmente, avremmo esattamente un effetto collaterale per istruzione e nessun effetto collaterale in un'espressione. Esso è un po 'strano che codice lato-effettuando può essere utilizzato in un contesto espressione.

Il ragionamento alla base del consentire questa funzione è perché (1) è spesso conveniente e (2) è idiomatico in linguaggi simili al C.

Si potrebbe notare che la domanda è stata posta: perché questo è idiomatico in linguaggi simili al C?

Sfortunatamente Dennis Ritchie non è più disponibile a chiedere, ma la mia ipotesi è che un incarico lascia quasi sempre il valore che è stato appena assegnato in un registro. C è un linguaggio molto "vicino alla macchina". Sembra plausibile e in linea con la progettazione di C che ci sia una caratteristica del linguaggio che sostanzialmente significa "continuare ad usare il valore che ho appena assegnato". È molto semplice scrivere un generatore di codice per questa funzione; continui a utilizzare il registro che memorizza il valore che è stato assegnato.


1
Non ero a conoscenza del fatto che qualsiasi cosa restituisca un valore (anche la costruzione dell'istanza è considerata un'espressione)
user437291

9
@ user437291: se la costruzione dell'istanza non era un'espressione, non si poteva fare nulla con l'oggetto costruito - non si poteva assegnarlo a qualcosa, non si poteva passare a un metodo e non si potevano chiamare metodi su di essa. Sarebbe piuttosto inutile, no?
Timwi,

1
La modifica di una variabile è in realtà "solo" un effetto collaterale dell'operatore di assegnazione che, creando un'espressione, produce un valore come scopo principale.
mk12

2
"La tua comprensione è errata al 100%." - Sebbene l'uso dell'inglese da parte dell'OP non sia il massimo, la sua "comprensione" riguarda ciò che dovrebbe essere il caso, rendendolo un'opinione, quindi non è il genere di cose che può essere "errato al 100%" . Alcuni progettisti linguistici concordano con il "dovere" dell'OP e fanno assegnazioni senza valore o vietano loro di essere sottoespressioni. Penso che sia una reazione eccessiva a =/ ==typo, che può essere facilmente risolto impedendo di usare il valore di a =meno che non sia tra parentesi. ad esempio, if ((x = y))o if ((x = y) == true)è consentito ma if (x = y)non lo è.
Jim Balter,

"Non sapevo che tutto ciò che restituisce un valore ... è considerato un'espressione" - Hmm ... tutto ciò che restituisce un valore è considerato un'espressione - i due sono praticamente sinonimi.
Jim Balter,

44

Non hai fornito la risposta? Serve per abilitare esattamente i tipi di costrutti che hai citato.

Un caso comune in cui viene utilizzata questa proprietà dell'operatore di assegnazione è la lettura di righe da un file ...

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

1
+1 Questo deve essere uno degli usi più pratici di questo costrutto
Mike Burton,

11
Mi piace molto per gli inizializzatori di proprietà davvero semplici, ma devi stare attento e tracciare la tua linea per un livello "semplice" basso per la leggibilità: return _myBackingField ?? (_myBackingField = CalculateBackingField());molto meno pula del controllo di null e assegnazione.
Tom Mayfield,

35

Il mio uso preferito delle espressioni di assegnazione è per proprietà pigramente inizializzate.

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

5
Ecco perché così tante persone hanno suggerito la ??=sintassi.
configuratore

27

Per uno, ti consente di concatenare i tuoi compiti, come nel tuo esempio:

a = b = c = 16;

Per un altro, ti consente di assegnare e verificare un risultato in una singola espressione:

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

Entrambi sono probabilmente motivi dubbi, ma ci sono sicuramente persone a cui piacciono questi costrutti.


5
+1 Il concatenamento non è una caratteristica fondamentale ma è bello vederlo "funziona" quando così tante cose non lo fanno.
Mike Burton,

+1 anche per il concatenamento; L'avevo sempre pensato come un caso speciale che è stato gestito dalla lingua piuttosto che dal naturale effetto collaterale dei "valori di ritorno delle espressioni".
Caleb Bell,

2
Ti consente anche di utilizzare l'assegnazione in cambio:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz

14

A parte le ragioni già menzionate (concatenamento di assegnazioni, set-and-test all'interno di cicli while, ecc.), Per utilizzare correttamente la usingdichiarazione è necessaria questa funzione:

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

MSDN scoraggia la dichiarazione dell'oggetto usa e getta al di fuori dell'istruzione using, poiché rimarrà nell'ambito anche dopo che è stato eliminato (vedere l'articolo di MSDN che ho collegato).


4
Non penso che questo sia davvero un concatenamento di compiti in sé.
Kenny,

1
@kenny: Uh ... No, ma non ho mai sostenuto che lo fosse. Come ho detto, a parte le ragioni già menzionate - che include il concatenamento delle assegnazioni - la dichiarazione di utilizzo sarebbe molto più difficile da usare se non fosse per il fatto che l'operatore di assegnazione restituisce il risultato dell'operazione di assegnazione. Questo non è affatto legato al concatenamento di incarichi.
Martin Törnwall,

1
Ma come ha osservato Eric sopra, "le dichiarazioni di assegnazione non restituiscono un valore". Questa è in realtà solo una sintassi del linguaggio dell'istruzione using, la prova è che non si può usare una "espressione di assegnazione" in un'istruzione using.
Kenny,

@kenny: mi scuso, la mia terminologia era chiaramente sbagliata. Mi rendo conto che l'istruzione using potrebbe essere implementata senza che la lingua avesse un supporto generale per le espressioni di assegnazione che restituivano un valore. A mio avviso, ciò sarebbe terribilmente incoerente.
Martin Törnwall,

2
@kenny: In realtà, si può usare un'istruzione di definizione e assegnazione o qualsiasi espressione in using. Tutti questi sono legali: using (X x = new X()), using (x = new X()), using (x). Tuttavia, in questo esempio, il contenuto dell'istruzione using è una sintassi speciale che non si basa affatto sull'assegnazione che restituisce un valore - Font font3 = new Font("Arial", 10.0f)non è un'espressione e non è valida in nessun luogo che si aspetta espressioni.
configuratore

10

Mi piacerebbe approfondire un punto specifico che Eric Lippert ha fatto nella sua risposta e mettere i riflettori in un'occasione particolare che non è stata affatto toccata da nessun altro. Eric ha detto:

[...] un incarico lascia quasi sempre il valore che è stato appena assegnato in un registro.

Vorrei dire che il compito lascerà sempre indietro il valore che abbiamo cercato di assegnare al nostro operando di sinistra. Non solo "quasi sempre". Ma non lo so perché non ho trovato questo problema commentato nella documentazione. In teoria potrebbe essere una procedura implementata molto efficace per "lasciarsi alle spalle" e non rivalutare l'operando di sinistra, ma è efficace?

Sì "efficiente" per tutti gli esempi finora costruiti nelle risposte di questo thread. Ma efficiente nel caso di proprietà e indicizzatori che utilizzano accessor get e set? Affatto. Considera questo codice:

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

Qui abbiamo una proprietà, che non è nemmeno un wrapper per una variabile privata. Ogni volta che viene chiamato, tornerà vero, ogni volta che si tenta di impostare il proprio valore, non farà nulla. Pertanto, ogni volta che questa proprietà viene valutata, deve essere sincero. Vediamo cosa succede:

Test test = new Test();

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

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

Indovina cosa stampa? Esso stampa Unexpected!!. A quanto pare, viene effettivamente chiamato l'accessor set, che non fa nulla. Ma da allora in poi, l'accessor get non viene mai chiamato. L'assegnazione lascia semplicemente il falsevalore che abbiamo cercato di assegnare alla nostra proprietà. E questo falsevalore è ciò che valuta l'istruzione if.

Concluderò con un esempio del mondo reale che mi ha portato a ricercare questo problema. Ho creato un indicizzatore che era un comodo wrapper per una raccolta ( List<string>) che una mia classe aveva come variabile privata.

Il parametro inviato all'indicizzatore era una stringa, che doveva essere trattata come un valore nella mia raccolta. L'accessorio get restituirebbe semplicemente vero o falso se quel valore esistesse o meno nell'elenco. Quindi l'accessor get era un altro modo di usare il List<T>.Containsmetodo.

Se l'accessor set dell'indicizzatore fosse chiamato con una stringa come argomento e l'operando giusto fosse un valore booleano true, aggiungerebbe quel parametro all'elenco. Ma se lo stesso parametro fosse stato inviato all'accessor e l'operando giusto fosse un valore booleano false, eliminerebbe invece l'elemento dall'elenco. Quindi l'accessor set è stato usato come una comoda alternativa a entrambi List<T>.Adde List<T>.Remove.

Pensavo di avere una "API" pulita e compatta che avvolgeva l'elenco con la mia logica implementata come gateway. Con l'aiuto di un solo indicizzatore ho potuto fare molte cose con alcune serie di tasti. Ad esempio, come posso provare ad aggiungere un valore al mio elenco e verificare che sia presente? Ho pensato che fosse l'unica riga di codice necessaria:

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

Ma come ha mostrato il mio esempio precedente, l'accessor get che dovrebbe vedere se il valore è davvero nell'elenco non è stato nemmeno chiamato. Il truevalore è stato sempre lasciato indietro, distruggendo efficacemente qualsiasi logica che avevo implementato nel mio get accessor.


Risposta molto interessante E mi ha fatto riflettere positivamente sullo sviluppo guidato dai test.
Elliot,

1
Sebbene ciò sia interessante, è un commento sulla risposta di Eric, non una risposta alla domanda del PO.
Jim Balter,

Non mi aspetto che un'espressione di assegnazione tentata ottenga prima il valore della proprietà target. Se la variabile è mutabile e i tipi corrispondono, perché dovrebbe importare quale era il vecchio valore? Basta impostare il nuovo valore. Tuttavia, non sono un fan degli incarichi nei test IF. Sta tornando vero perché il compito è andato a buon fine o è un valore, ad esempio un numero intero positivo che è costretto a vero o perché stai assegnando un valore booleano? Indipendentemente dall'implementazione, la logica è ambigua.
Davos,

6

Se l'assegnazione non restituisce un valore, a = b = c = 16neanche la riga funzionerebbe.

Anche essere in grado di scrivere cose come while ((s = readLine()) != null)può essere utile a volte.

Quindi il motivo dietro il lasciare che il compito restituisca il valore assegnato è quello di lasciarti fare quelle cose.


Nessuno ha menzionato gli svantaggi: sono venuto qui chiedendomi perché le assegnazioni non potessero restituire null, in modo che l'assegnazione comune rispetto al bug di confronto "if (qualcosa = 1) {}" non riuscisse a compilare anziché restituire sempre true. Ora vedo che ciò creerebbe ... altri problemi!
Jon

1
@Jon quel bug potrebbe essere facilmente prevenuto impedendo o avvertendo che si =verifichi in un'espressione che non è tra parentesi (senza contare le parentesi dell'istruzione if / while stessa). gcc fornisce tali avvertimenti e ha quindi sostanzialmente eliminato questa classe di bug dai programmi C / C ++ che vengono compilati con esso. È un peccato che altri autori di compilatori abbiano prestato così poca attenzione a questo e a molte altre buone idee in gcc.
Jim Balter,

4

Penso che tu abbia frainteso il modo in cui il parser interpreterà quella sintassi. L'assegnazione verrà valutata per prima e il risultato verrà quindi confrontato con NULL, ovvero l'affermazione equivale a:

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

Come altri hanno sottolineato, il risultato di un'assegnazione è il valore assegnato. Trovo difficile immaginare il vantaggio di avere

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

e

s = "Hello";
s != null;

non essere equivalente ...


Mentre hai ragione dal punto di vista pratico che le tue due righe sono equivalenti, la tua affermazione che l'assegnazione non sta restituendo un valore non è tecnicamente vera. La mia comprensione è che il risultato di un incarico è un valore (secondo la specifica C #), e in tal senso non è diverso dal dire l'operatore addizione in un'espressione come 1 + 2 + 3
Steve Ellinger

3

Penso che il motivo principale sia la somiglianza (intenzionale) con C ++ e C. Far sì che l'operatore di assigmentazione (e molti altri costrutti del linguaggio) si comporti come se le loro controparti in C ++ seguissero il principio della minima sorpresa, e qualsiasi programmatore proveniente da un altro riccio- il linguaggio di parentesi può usarli senza spendere molto pensiero. Essere facili da raccogliere per i programmatori C ++ è stato uno dei principali obiettivi di progettazione per C #.


2

Per i due motivi che includi nel tuo post
1) in modo da poter fare a = b = c = 16
2) in modo da poter verificare se un compito è riuscito if ((s = openSomeHandle()) != null)


1

Il fatto che 'a ++' o 'printf ("pippo")' può essere utile come affermazione autonoma o come parte di un'espressione più grande significa che C deve consentire la possibilità che i risultati dell'espressione possano o meno essere Usato. Detto questo, c'è una nozione generale secondo cui le espressioni che potrebbero utilmente "restituire" un valore possono anche farlo. Il concatenamento di assegnazioni può essere leggermente "interessante" in C, e ancora più interessante in C ++, se tutte le variabili in questione non hanno esattamente lo stesso tipo. È preferibile evitare tali usi.


1

Un altro ottimo caso d'uso, lo uso sempre:

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

0

Un ulteriore vantaggio che non vedo nelle risposte qui è che la sintassi dell'assegnazione si basa sull'aritmetica.

Ora x = y = b = c = 2 + 3significa qualcosa di diverso in aritmetica di un linguaggio in stile C; in aritmetica sua asserzione, abbiamo affermare che x è uguale ay ecc e in un linguaggio C-stile è un'istruzione che marche x uguale ay ecc dopo che viene eseguita.

Detto questo, c'è ancora abbastanza relazione tra l'aritmetica e il codice che non ha senso rifiutare ciò che è naturale nell'aritmetica a meno che non ci sia una buona ragione. (L'altra cosa che i linguaggi di tipo C hanno preso dall'uso del simbolo uguale è l'uso di == per il confronto di uguaglianza. Qui però, poiché l'estrema destra == restituisce un valore questo tipo di concatenamento sarebbe impossibile.)


@JimBalter Mi chiedo se, tra cinque anni, potresti davvero discutere.
Jon Hanna,

@JimBalter no, il tuo secondo commento è in realtà un argomento contrario e valido. La mia replica era perché il tuo primo commento non lo era. Tuttavia, quando impariamo l'aritmetica, apprendiamo a = b = csignifica che aed be csono la stessa cosa. Quando si impara una lingua stile C si apprende che dopo a = b = c, ae bsiamo la stessa cosa c. C'è sicuramente una differenza nella semantica, come dice la mia stessa risposta, ma ancora quando da bambina ho imparato per la prima volta a programmare in una lingua che usava =per il compito ma che non permetteva a = b = cquesto mi sembrava irragionevole, però ...
Jon Hanna,

... inevitabile perché quel linguaggio usato anche =per il confronto di uguaglianza, quindi in questo a = b = cdovrebbe significare cosa a = b == csignifica nei linguaggi in stile C. Ho trovato il concatenamento consentito in C molto più intuitivo perché potevo disegnare un'analogia con l'aritmetica.
Jon Hanna,

0

Mi piace usare il valore di ritorno dell'assegnazione quando devo aggiornare un sacco di cose e restituire se ci sono state o meno delle modifiche:

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;

Stai attento però. Potresti pensare di accorciarlo a questo:

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

Ma questo smetterà effettivamente di valutare le dichiarazioni o dopo aver trovato il primo vero. In questo caso ciò significa che smette di assegnare valori successivi una volta assegnato il primo valore diverso.

Vedi https://dotnetfiddle.net/e05Rh8 per giocare con questo

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.