Pourquoi les instructions d'affectation renvoient-elles une valeur?

126

Ceci est autorisé:

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

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

À ma connaissance, l'affectation s = ”Hello”;ne doit entraîner qu'une “Hello”affectation s, mais l'opération ne doit renvoyer aucune valeur. Si cela était vrai, alors ((s = "Hello") != null)produirait une erreur, car nullserait comparé à rien.

Quel est le raisonnement derrière l’autorisation des instructions d’affectation à renvoyer une valeur?

user437291
la source
44
Pour référence, ce comportement est défini dans la spécification C # : "Le résultat d'une expression d'affectation simple est la valeur affectée à l'opérande de gauche. Le résultat a le même type que l'opérande de gauche et est toujours classé comme une valeur."
Michael Petrotta
1
@Skurmedel Je ne suis pas le downvoter, je suis d'accord avec vous. Mais peut-être parce que c'était une question rhétorique?
jsmith
Dans l'affectation pascal / delphi: = ne renvoie rien. Je déteste ça.
Andrey le
20
Standard pour les langues de la branche C. Les affectations sont des expressions.
Hans Passant
"affectation ... ne devrait provoquer que" Hello "à être assigné à s, mais l'opération ne doit pas renvoyer de valeur" - Pourquoi? "Quel est le raisonnement derrière le fait d'autoriser les instructions d'affectation à renvoyer une valeur?" - Parce que c'est utile, comme le démontrent vos propres exemples. (Eh bien, votre deuxième exemple est idiot, mais while ((s = GetWord()) != null) Process(s);ne l'est pas).
Jim Balter

Réponses:

160

À ma connaissance, affectation s = "Bonjour"; ne devrait provoquer que "Hello" pour être assigné à s, mais l'opération ne doit renvoyer aucune valeur.

Votre compréhension est incorrecte à 100%. Pouvez-vous expliquer pourquoi vous croyez à cette fausse chose?

Quel est le raisonnement derrière l’autorisation des instructions d’affectation à renvoyer une valeur?

Tout d'abord, les instructions d' affectation ne produisent pas de valeur. Les expressions d' affectation produisent une valeur. Une expression d'affectation est une déclaration légale; il n'y a qu'une poignée d'expressions qui sont des déclarations légales en C #: l'attente d'une expression, la construction d'instance, l'incrémentation, la décrémentation, l'invocation et les expressions d'affectation peuvent être utilisées là où une instruction est attendue.

Il n'y a qu'un seul type d'expression en C # qui ne produit pas une sorte de valeur, à savoir, une invocation de quelque chose qui est typé comme retournant void. (Ou, de manière équivalente, une attente d'une tâche sans valeur de résultat associée.) Tout autre type d'expression produit une valeur ou une variable ou une référence ou un accès à une propriété ou un accès à un événement, et ainsi de suite.

Notez que toutes les expressions qui sont légales en tant que déclarations sont utiles pour leurs effets secondaires . C'est le point clé ici, et je pense que c'est peut-être la cause de votre intuition que les affectations devraient être des déclarations et non des expressions. Idéalement, nous aurions exactement un effet secondaire par déclaration, et aucun effet secondaire dans une expression. Il est un peu étrange que du code à effet secondaire puisse être utilisé dans un contexte d'expression.

Le raisonnement derrière l'autorisation de cette fonctionnalité est que (1) elle est souvent pratique et (2) elle est idiomatique dans les langages de type C.

On peut noter que la question a été posée: pourquoi est-ce idiomatique dans les langages de type C?

Dennis Ritchie n'est malheureusement plus disponible pour demander, mais je suppose qu'une affectation laisse presque toujours la valeur qui vient d'être attribuée dans un registre. C est un langage très "proche de la machine". Il semble plausible et conforme à la conception de C qu'il y ait une fonction de langage qui signifie essentiellement "continuez à utiliser la valeur que je viens d'attribuer". Il est très facile d'écrire un générateur de code pour cette fonctionnalité; vous continuez simplement à utiliser le registre qui a stocké la valeur qui a été attribuée.

Eric Lippert
la source
1
Je ne savais
9
@ user437291: Si la construction d'instance n'était pas une expression, vous ne pouviez rien faire avec l'objet construit - vous ne pouviez pas l'affecter à quelque chose, vous ne pouviez pas le passer dans une méthode et vous ne pouviez appeler aucune méthode dessus. Ce serait assez inutile, non?
Timwi
1
La modification d'une variable est en fait "juste" un effet secondaire de l'opérateur d'affectation, qui, en créant une expression, produit une valeur comme objectif principal.
mk12
2
"Votre compréhension est à 100% incorrecte." - Bien que l'utilisation de l'anglais par l'OP ne soit pas la meilleure, sa "compréhension" concerne ce qui devrait être le cas, en faisant une opinion, donc ce n'est pas le genre de chose qui peut être "100% incorrect" . Certains concepteurs de langage sont d'accord avec le «devrait» du PO, et font que les affectations n'ont aucune valeur ou les interdisent autrement d'être des sous-expressions. Je pense que c'est une réaction excessive à la =/ ==typo, qui est facilement résolue en interdisant l'utilisation de la valeur de =sauf si elle est entre parenthèses. par exemple, if ((x = y))ou if ((x = y) == true)est autorisé mais if (x = y)ne l'est pas.
Jim Balter
"Je ne savais pas que tout ce qui renvoie une valeur ... est considéré comme une expression" - Hmm ... tout ce qui renvoie une valeur est considéré comme une expression - les deux sont pratiquement synonymes.
Jim Balter
44

N'avez-vous pas fourni la réponse? C'est pour activer exactement les types de constructions que vous avez mentionnés.

Un cas courant où cette propriété de l'opérateur d'affectation est utilisée est la lecture de lignes à partir d'un fichier ...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...
Timwi
la source
1
+1 Cela doit être l'une des utilisations les plus pratiques de cette construction
Mike Burton
11
Je l'aime vraiment pour les initialiseurs de propriété vraiment simples, mais vous devez faire attention et tracer votre ligne pour "simple" faible pour la lisibilité: return _myBackingField ?? (_myBackingField = CalculateBackingField());beaucoup moins de paillettes que de vérifier la valeur null et d'assigner.
Tom Mayfield du
35

Mon utilisation préférée des expressions d'affectation est pour les propriétés initialisées paresseusement.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}
Nathan Baulch
la source
5
C'est pourquoi tant de personnes ont suggéré la ??=syntaxe.
configurateur
27

D'une part, il vous permet d'enchaîner vos affectations, comme dans votre exemple:

a = b = c = 16;

Pour un autre, il vous permet d'assigner et de vérifier un résultat dans une seule expression:

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

Les deux sont peut-être des raisons douteuses, mais il y a certainement des gens qui aiment ces constructions.

Michael Burr
la source
5
Le chaînage +1 n'est pas une fonctionnalité critique, mais il est agréable de voir qu'il "fonctionne juste" quand tant de choses ne le font pas.
Mike Burton du
+1 pour le chaînage également; J'avais toujours pensé à cela comme un cas particulier qui était traité par le langage plutôt que comme l'effet secondaire naturel des "valeurs de retour d'expressions".
Caleb Bell
2
Il vous permet également d'utiliser l'affectation dans les retours:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz
14

Outre les raisons déjà mentionnées (chaînage des affectations, set-and-test dans les boucles while, etc.), pour utiliser correctement l' usinginstruction, vous avez besoin de cette fonctionnalité:

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

MSDN déconseille de déclarer l'objet jetable en dehors de l'instruction using, car il restera alors dans la portée même après avoir été supprimé (voir l'article MSDN que j'ai lié).

Martin Törnwall
la source
4
Je ne pense pas que ce soit vraiment le chaînage des affectations en soi.
kenny le
1
@kenny: Euh ... Non, mais je n'ai jamais prétendu que c'était le cas non plus. Comme je l'ai dit, outre les raisons déjà mentionnées - qui incluent le chaînage des affectations - l'instruction using serait beaucoup plus difficile à utiliser si ce n'était du fait que l'opérateur d'affectation renvoie le résultat de l'opération d'affectation. Ce n'est pas du tout lié au chaînage des affectations.
Martin Törnwall
1
Mais comme Eric l'a noté ci-dessus, "les instructions d'affectation ne renvoient pas de valeur". C'est en fait juste la syntaxe de langage de l'instruction using, la preuve en est qu'on ne peut pas utiliser une "expression d'affectation" dans une instruction using.
kenny
@kenny: Toutes mes excuses, ma terminologie était clairement erronée. Je me rends compte que l'instruction using pourrait être implémentée sans que le langage n'ait un support général pour les expressions d'affectation renvoyant une valeur. Ce serait, à mes yeux, terriblement incohérent.
Martin Törnwall
2
@kenny: En fait, on peut utiliser soit une instruction de définition et d'affectation, soit n'importe quelle expression dans using. Tous ces éléments sont légaux: using (X x = new X()), using (x = new X()), using (x). Cependant, dans cet exemple, le contenu de l'instruction using est une syntaxe spéciale qui ne repose pas du tout sur l'affectation renvoyant une valeur - Font font3 = new Font("Arial", 10.0f)n'est pas une expression et n'est valide à aucun endroit qui attend des expressions.
configurateur
10

Je voudrais développer un point précis qu'Eric Lippert a soulevé dans sa réponse et mettre en lumière une occasion particulière qui n'a été du tout évoquée par personne d'autre. Eric a dit:

[...] une affectation laisse presque toujours la valeur qui vient d'être affectée dans un registre.

Je voudrais dire que l'affectation laissera toujours derrière la valeur que nous avons essayé d'attribuer à notre opérande gauche. Pas seulement «presque toujours». Mais je ne sais pas car je n'ai pas trouvé ce problème commenté dans la documentation. Cela pourrait théoriquement être une procédure mise en œuvre très efficace pour «laisser derrière» et ne pas réévaluer l'opérande gauche, mais est-ce efficace?

«Efficace» oui pour tous les exemples construits jusqu'à présent dans les réponses de ce fil. Mais efficace dans le cas des propriétés et des indexeurs qui utilisent des accesseurs get et set? Pas du tout. Considérez ce code:

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

Ici, nous avons une propriété, qui n'est même pas un wrapper pour une variable privée. Chaque fois qu'il sera appelé, il retournera vrai, chaque fois que l'on essayera de fixer sa valeur, il ne fera rien. Ainsi, chaque fois que cette propriété est évaluée, il sera véridique. Voyons ce qui se passe:

Test test = new Test();

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

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

Devinez ce qu'il imprime? Il imprime Unexpected!!. En fait, l'accesseur set est en effet appelé, ce qui ne fait rien. Mais par la suite, l'accesseur get n'est jamais appelé du tout. L'affectation laisse simplement derrière la falsevaleur que nous avons essayé d'attribuer à notre propriété. Et cette falsevaleur est ce que l'instruction if évalue.

Je terminerai par un exemple concret qui m'a amené à faire des recherches sur ce problème. J'ai créé un indexeur qui était un wrapper pratique pour une collection ( List<string>) qu'une de mes classes avait comme variable privée.

Le paramètre envoyé à l'indexeur était une chaîne, qui devait être traitée comme une valeur dans ma collection. L'accesseur get renverrait simplement true ou false si cette valeur existait ou non dans la liste. Ainsi, l'accesseur get était une autre façon d'utiliser la List<T>.Containsméthode.

Si l'accesseur set de l'indexeur était appelé avec une chaîne comme argument et que l'opérande de droite était un booléen true, il ajouterait ce paramètre à la liste. Mais si le même paramètre était envoyé à l'accesseur et que l'opérande de droite était un booléen false, il supprimerait à la place l'élément de la liste. Ainsi, l'accesseur set a été utilisé comme une alternative pratique à la fois List<T>.Addet List<T>.Remove.

Je pensais avoir une "API" soignée et compacte enveloppant la liste avec ma propre logique implémentée en tant que passerelle. Avec l'aide d'un indexeur seul, je pouvais faire beaucoup de choses avec quelques combinaisons de touches. Par exemple, comment puis-je essayer d'ajouter une valeur à ma liste et vérifier qu'elle s'y trouve? Je pensais que c'était la seule ligne de code nécessaire:

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

Mais comme mon exemple précédent l'a montré, l'accesseur get qui est censé voir si la valeur est vraiment dans la liste n'a même pas été appelé. La truevaleur était toujours laissée pour détruire efficacement la logique que j'avais implémentée dans mon accesseur get.

Martin Andersson
la source
Réponse très intéressante. Et cela m'a fait penser positivement au développement piloté par les tests.
Elliot
1
Bien que cela soit intéressant, c'est un commentaire sur la réponse d'Eric, pas une réponse à la question du PO.
Jim Balter
Je ne m'attendrais pas à ce qu'une tentative d'expression d'affectation obtienne d'abord la valeur de la propriété cible. Si la variable est modifiable et que les types correspondent, pourquoi la valeur de l'ancienne valeur importerait-elle? Définissez simplement la nouvelle valeur. Je ne suis pas fan de l'affectation dans les tests IF. Est-ce que cela renvoie true parce que l'affectation a réussi, ou c'est une valeur, par exemple un entier positif qui est forcé à true, ou parce que vous attribuez un booléen? Quelle que soit la mise en œuvre, la logique est ambiguë.
Davos le
6

Si l'affectation ne retournait pas de valeur, la ligne a = b = c = 16ne fonctionnerait pas non plus.

Etre capable d'écrire des choses comme while ((s = readLine()) != null)peut parfois être utile.

Donc, la raison pour laquelle l'affectation renvoie la valeur assignée est de vous laisser faire ces choses.

sepp2k
la source
Personne n'a mentionné les inconvénients - je suis venu ici en me demandant pourquoi les affectations ne pouvaient pas renvoyer null, afin que le bogue d'affectation commun contre comparaison 'if (something = 1) {}' échoue à se compiler plutôt que de toujours renvoyer true. Je vois maintenant que cela créerait ... d'autres problèmes!
Jon
1
@Jon ce bug pourrait être facilement évité en interdisant ou en avertissant de =se produire dans une expression qui n'est pas entre parenthèses (sans compter les parenthèses de l'instruction if / while elle-même). gcc donne de tels avertissements et a ainsi essentiellement éliminé cette classe de bogues des programmes C / C ++ compilés avec lui. C'est dommage que d'autres auteurs de compilateurs aient prêté si peu d'attention à cela et à plusieurs autres bonnes idées dans gcc.
Jim Balter du
4

Je pense que vous ne comprenez pas comment l'analyseur va interpréter cette syntaxe. L'affectation sera évaluée en premier , et le résultat sera ensuite comparé à NULL, c'est-à-dire que l'instruction équivaut à:

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

Comme d'autres l'ont souligné, le résultat d'une affectation est la valeur attribuée. J'ai du mal à imaginer l'avantage d'avoir

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

et

s = "Hello";
s != null;

pas être équivalent ...

Dan J
la source
Bien que vous ayez raison d'un sens pratique que vos deux lignes sont équivalentes, votre déclaration selon laquelle l'affectation ne renvoie pas de valeur n'est pas techniquement vraie. Je crois comprendre que le résultat d'une affectation est une valeur (selon la spécification C #), et en ce sens n'est pas différent de l'opérateur d'addition dans une expression comme 1 + 2 + 3
Steve Ellinger
3

Je pense que la raison principale est la similitude (intentionnelle) avec C ++ et C.Faire en sorte que l'opérateur d'assignation (et beaucoup d'autres constructions de langage) se comportent comme leurs homologues C ++ suit simplement le principe de la moindre surprise, et tout programmeur venant d'un autre bouclé- le langage entre parenthèses peut les utiliser sans trop y penser. Être facile à comprendre pour les programmeurs C ++ était l'un des principaux objectifs de conception pour C #.

tdammers
la source
2

Pour les deux raisons que vous incluez dans votre message
1) vous pouvez donc faire a = b = c = 16
2) afin que vous puissiez tester si un devoir a réussi if ((s = openSomeHandle()) != null)

JamesMLV
la source
1

Le fait que 'a ++' ou 'printf ("foo")' puisse être utile soit en tant qu'instruction autonome soit en tant que partie d'une expression plus large signifie que C doit permettre la possibilité que les résultats de l'expression soient ou non utilisé. Compte tenu de cela, il existe une notion générale selon laquelle les expressions qui pourraient utilement «renvoyer» une valeur peuvent aussi bien le faire. Le chaînage des affectations peut être légèrement "intéressant" en C, et encore plus intéressant en C ++, si toutes les variables en question n'ont pas précisément le même type. Il vaut probablement mieux éviter de tels usages.

supercat
la source
1

Un autre excellent exemple d'utilisation, j'utilise ceci tout le temps:

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

Un avantage supplémentaire que je ne vois pas dans les réponses ici, est que la syntaxe de l'affectation est basée sur l'arithmétique.

Désormais, cela x = y = b = c = 2 + 3signifie quelque chose de différent en arithmétique d'un langage de style C; en arithmétique, c'est une assertion, nous déclarons que x est égal à y etc. et dans un langage de style C, c'est une instruction qui rend x égal à y etc. après son exécution.

Cela dit, il y a encore suffisamment de relations entre l'arithmétique et le code pour qu'il n'y ait pas de sens d'interdire ce qui est naturel dans l'arithmétique à moins qu'il n'y ait une bonne raison. (L'autre chose que les langages de style C ont tirée de l'utilisation du symbole égal est l'utilisation de == pour la comparaison d'égalité. Ici, cependant, parce que le plus à droite == renvoie une valeur, ce type de chaînage serait impossible.)

Jon Hanna
la source
@JimBalter Je me demande si, dans cinq ans, vous pourriez réellement vous disputer.
Jon Hanna
@JimBalter non, votre deuxième commentaire est en fait un contre-argument, et un argument solide. Ma réponse était que votre premier commentaire ne l'était pas. Pourtant, lors de l'apprentissage de l'arithmétique, nous apprenons a = b = csignifie que aet bet csont la même chose. Lors de l'apprentissage d'un langage de style C, nous apprenons cela après a = b = c, aet bc'est la même chose que c. Il y a certainement une différence de sémantique, comme le dit ma réponse elle-même, mais quand j'étais enfant, j'ai appris pour la première fois à programmer dans une langue qui utilisait =pour les devoirs mais qui ne le permettait pas, a = b = ccela me semblait déraisonnable, cependant…
Jon Hanna
… Inévitable parce que ce langage est également utilisé =pour la comparaison d'égalité, donc cela a = b = cdevrait signifier ce que a = b == csignifie les langages de style C. J'ai trouvé le chaînage autorisé en C beaucoup plus intuitif car je pouvais faire une analogie avec l'arithmétique.
Jon Hanna
0

J'aime utiliser la valeur de retour d'affectation lorsque j'ai besoin de mettre à jour un tas de choses et de retourner s'il y a eu ou non des changements:

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;

Soyez prudent cependant. Vous pourriez penser que vous pouvez le raccourcir à ceci:

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

Mais cela arrêtera en fait d'évaluer les instructions ou après avoir trouvé la première vraie. Dans ce cas, cela signifie qu'il arrête d'attribuer les valeurs suivantes une fois qu'il affecte la première valeur différente.

Voir https://dotnetfiddle.net/e05Rh8 pour jouer avec ça

Luke Kubat
la source