web-dev-qa-db-fra.com

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

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 devrait entraîner que l'attribution de “Hello” à s, mais l'opération ne devrait pas renvoyer de valeur. Si cela était vrai, alors ((s = "Hello") != null) produirait une erreur, puisque null serait comparé à rien. 

Quel est le raisonnement derrière permettre aux instructions d'affectation de renvoyer une valeur?

118
user437291

À ma connaissance, l'affectation s = "Bonjour"; Cela devrait uniquement entraîner l'affectation de "Hello" à s, mais l'opération ne doit pas renvoyer de valeur.

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

Quel est le raisonnement derrière permettre aux instructions d'affectation de renvoyer une valeur?

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

Il n'y a qu'un seul type d'expression dans C # qui ne génère pas de valeur, à savoir l'invocation de quelque chose qui est tapé comme renvoyant vide. (Ou, de manière équivalente, l'attente d'une tâche sans valeur de résultat associée.) Tout autre type d'expression produit une valeur ou une variable ou un accès à la propriété ou à la propriété ou un accès à un événement, etc.

Notez que toutes les expressions qui sont légales comme déclarations sont utiles pour leurs effets secondaires . C’est là l’essentiel, et je pense que votre intuition est peut-être d’affirmer que les tâches assignées devraient être des déclarations et non des expressions. Idéalement, nous aurions exactement un effet secondaire par énoncé et aucun effet secondaire dans une expression. Il est un peu étrange que du code secondaire puisse être utilisé dans un contexte d'expression.

Le fait de permettre cette fonctionnalité est principalement dû au fait que (1) elle est fréquemment pratique et (2) qu’elle est idiomatique dans les langues de type C.

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

Dennis Ritchie n’est malheureusement plus disponible pour demander, mais j’imagine qu’une affectation laisse presque toujours derrière 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 existe une fonctionnalité de langage qui signifie fondamentalement "continue à 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 stockait la valeur attribuée.

151
Eric Lippert

N'avez-vous pas fourni la réponse? C’est pour activer exactement le type de construction que vous avez mentionné.

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

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

Mon utilisation préférée des expressions d’affectation concerne les propriétés initialisées tardivement.

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

D'une part, il vous permet de chaîner vos tâches, comme dans votre exemple:

a = b = c = 16;

D'autre part, il vous permet d'attribuer 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.

26
Michael Burr

En plus des raisons déjà mentionnées (chaînage des assignations, set-and-test dans les boucles while, etc.), pour correctement / utilisez l'instruction using, 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 donc dans le champ d'application même après sa suppression (voir l'article MSDN lié à l'article I).

14
Martin Törnwall

J'aimerais élaborer sur un point précis évoqué par Eric Lippert et mettre en lumière une occasion particulière qui n'a pas du tout été abordée par quiconque. Eric a dit:

[...] une assignation laisse presque toujours derrière la valeur qui vient d'être assignée dans un registre.

Je voudrais dire que la cession laissera toujours derrière la valeur que nous avons essayé d'attribuer à notre opérande de 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 très efficace mise en œuvre pour "laisser derrière" et ne pas réévaluer l'opérande de gauche, mais est-ce efficace?

"Efficace" oui pour tous les exemples jusqu'ici construits dans les réponses de ce fil. Mais efficace dans le cas de propriétés et d’indexeurs utilisant des accesseurs get et set? Pas du tout. Considérons 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 est appelé, il revient vrai, chaque fois que l’on essaie de fixer sa valeur, il ne fait rien. Ainsi, chaque fois que cette propriété sera é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 que ça imprime? Il imprime Unexpected!!. En fin de compte, l’accesseur de set est appelé, ce qui ne fait rien. Mais par la suite, l'accesseur get n'est jamais appelé. La cession laisse simplement derrière la valeur false que nous avons essayé d'attribuer à notre propriété. Et cette valeur false correspond à l'évaluation de l'instruction if.

Je terminerai avec un exemple concret qui m'a permis de faire des recherches sur ce problème. J'ai créé un indexeur qui était un wrapper pratique pour une collection (List<string>) qu'une classe de la mienne 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 un autre moyen d'utiliser la méthode List<T>.Contains.

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

Je pensais avoir une "API" soignée et compacte enveloppant la liste avec ma propre logique mise en œuvre en tant que passerelle. Avec l’aide d’un indexeur seul, je pouvais faire beaucoup de choses avec quelques touches. Par exemple, comment puis-je essayer d'ajouter une valeur à ma liste et vérifier qu'elle est là? 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 réellement dans la liste n’a même pas été appelé. La valeur true a toujours été laissée de côté, détruisant efficacement la logique que j'avais implémentée dans mon accesseur get.

8
Martin Andersson

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

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

Ainsi, la raison pour laquelle l'affectation renvoie la valeur attribuée est de vous permettre de faire ces choses.

6
sepp2k

Je pense que vous comprenez mal 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 est équivalente à:

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

4
Dan J

Je pense que la raison principale est la similitude (intentionnelle) avec C++ et C. Faire en sorte que l'opérateur d'assigment (et beaucoup d'autres constructions de langage) se comporte comme si leurs homologues C++ suivaient simplement le principe de moindre surprise, le langage entre parenthèses peut les utiliser sans trop y penser. L'un des principaux objectifs de conception de C # était de faciliter la tâche des programmeurs C++.

3
tdammers

Pour les deux raisons que vous incluez dans votre message
1) afin que vous puissiez faire a = b = c = 16
2) afin que vous puissiez vérifier si une affectation a réussi if ((s = openSomeHandle()) != null)

2
JamesMLV

Le fait que 'a ++' ou 'printf ("foo") "puisse être utile en tant qu'instruction autonome ou en tant que partie d'une expression plus grande signifie que C doit permettre à la possibilité que les résultats de l'expression soient utilisé. Compte tenu de cela, il existe une notion générale selon laquelle les expressions qui pourraient utilement «renvoyer» une valeur peuvent également 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 exactement le même type. De tels usages sont probablement mieux évités.

1
supercat

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

Maintenant, x = y = b = c = 2 + 3 signifie quelque chose de différent en arithmétique par rapport à 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 ne soit pas logique de rejeter ce qui est naturel dans l'arithmétique, sauf pour de bonnes raisons. (L'autre chose que les langages de style C ont empruntée à l'utilisation du symbole égal est l'utilisation de == pour la comparaison d'égalité. Ici, étant donné que == le plus à droite renvoie une valeur, ce type d'enchaînement serait impossible.)

0
Jon Hanna

Un autre excellent exemple de cas d'utilisation, je l'utilise tout le temps:

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