Why do assignment statements return a value?
Asked Answered
B

14

140

This is allowed:

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

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

To my understanding, assignment s = ”Hello”; should only cause “Hello” to be assigned to s, but the operation shouldn’t return any value. If that was true, then ((s = "Hello") != null) would produce an error, since null would be compared to nothing.

What is the reasoning behind allowing assignment statements to return a value?

Booty answered 27/9, 2010 at 19:36 Comment(5)
For reference, this behavior is defined in the C# spec: "The result of a simple assignment expression is the value assigned to the left operand. The result has the same type as the left operand and is always classified as a value."Goa
@Skurmedel I am not the downvoter, I agree with you. But maybe because it was thought to be a rhetorical question?Angilaangina
In pascal/delphi assignment := returns nothing. i hate it.Huberthuberto
Standard for languages in the C branch. Assignments are expressions.Alkalify
"assignment ... should only cause “Hello” to be assigned to s, but the operation shouldn’t return any value" -- Why? "What is the reasoning behind allowing assignment statements to return a value?" -- Because it's useful, as your own examples demonstrate. (Well, your second example is silly, but while ((s = GetWord()) != null) Process(s); is not).Alexanderalexandr
B
179

To my understanding, assignment s = "Hello"; should only cause "Hello" to be assigned to s, but the operation shouldn’t return any value.

Your understanding is 100% incorrect. Can you explain why you believe this false thing?

What is the reasoning behind allowing assignment statements to return a value?

First off, assignment statements do not produce a value. Assignment expressions produce a value. An assignment expression is a legal statement; there are only a handful of expressions which are legal statements in C#: awaits of an expression, instance construction, increment, decrement, invocation and assignment expressions may be used where a statement is expected.

There is only one kind of expression in C# which does not produce some sort of value, namely, an invocation of something that is typed as returning void. (Or, equivalently, an await of a task with no associated result value.) Every other kind of expression produces a value or variable or reference or property access or event access, and so on.

Notice that all the expressions which are legal as statements are useful for their side effects. That's the key insight here, and I think perhaps the cause of your intuition that assignments should be statements and not expressions. Ideally, we'd have exactly one side effect per statement, and no side effects in an expression. It is a bit odd that side-effecting code can be used in an expression context at all.

The reasoning behind allowing this feature is because (1) it is frequently convenient and (2) it is idiomatic in C-like languages.

One might note that the question has been begged: why is this idiomatic in C-like languages?

Dennis Ritchie is no longer available to ask, unfortunately, but my guess is that an assignment almost always leaves behind the value that was just assigned in a register. C is a very "close to the machine" sort of language. It seems plausible and in keeping with the design of C that there be a language feature which basically means "keep on using the value that I just assigned". It is very easy to write a code generator for this feature; you just keep on using the register that stored the value that was assigned.

Belkisbelknap answered 27/9, 2010 at 20:28 Comment(11)
Wasn't aware that anything which returns a value ( even instance construction is considered an expression )Booty
@user437291: If instance construction wasn’t an expression, you couldn’t do anything with the constructed object — you couldn’t assign it to something, you couldn’t pass it into a method, and you couldn’t call any methods on it. Would be pretty useless, wouldn’t it?Araucania
Changing a variable is actually "just" a side effect of the assignment operator, which, creating an expression, yields a value as its primary purpose.Laurentium
"Your understanding is 100% incorrect." -- While the OP's use of English isn't the greatest, his/her "understanding" is about what should be the case, making it an opinion, so it isn't the sort of thing than can be "100% incorrect". Some language designers agree with the OP's "should", and make assignments have no value or otherwise ban them from being subexpressions. I think that's an overreaction to the =/== typo, which is easily addressed by disallowing using the value of = unless it is parenthesed. e.g., if ((x = y)) or if ((x = y) == true) is allowed but if (x = y) is not.Alexanderalexandr
"Wasn't aware that anything which returns a value ... is considered an expression" -- Hmm ... everything that returns a value is considered an expression -- the two are virtually synonymous.Alexanderalexandr
Maybe he believed such a thing because this is how VB.Net works. Thank you for correcting that in C# Eric.Luigiluigino
Confirmed for me that it is OK to do lazy initialization of member objects in C#6 using something like set => (FooObjectField ?? (FooObjectField = new FooObject())).FooObjectProperty = value; (because the assignment in the left-hand side of the null-coalescing operator returns the newly created object).Hearty
@Toby: That's legal, but I think it's a bit of a code smell to have an expression used for both its effects and its value. The code will be generated the same as { if (f == null) f = new F(); f.p = value;} which is only a few more keystrokes and easier to both read and debug.Belkisbelknap
D'oh, I meant right-hand side, but your point still applies I agree Eric.Hearty
Back in my 16-bit C programming days when every byte counted, I wrote code that depended on that "keep on using the value that I just assigned" behavior, as in int f() { x = 2 * 3; } deliberately omitting the return statement to conserve a few bytes of that precious 64k RAM. One could assume, albeit dangerously, that the result of the last expression remained in the accumulator, and int functions returned the value in the accumulator.Magistral
@BobKaufman: Fun fact, JavaScript still does that. Never do this, but in JavaScript you can say x=eval("if(whatever)y=2; else z=3;"); and x will be either 2 or 3. eval is pure unmitigated evil.Belkisbelknap
A
48

Haven’t you provided the answer? It’s to enable exactly the kinds of constructs you have mentioned.

A common case where this property of the assignment operator is used is reading lines from a file...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...
Araucania answered 27/9, 2010 at 19:40 Comment(2)
+1 This has to be one of the most practical uses of this constructMills
I really like it for really simple property initializers, but you have to be careful and draw your line for "simple" low for readability: return _myBackingField ?? (_myBackingField = CalculateBackingField()); A lot less chaff than checking for null and assigning.Replenish
A
43

My favorite use of assignment expressions is for lazily initialized properties.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}
Applewhite answered 30/9, 2010 at 6:1 Comment(3)
Which is why so many people have suggested the ??= syntax.Myelitis
Now that ??= is there, you can still use it with an assignment expression, makes it even easier: public string Name => this.name ??= ExpensiveNameGeneratorMethod();Conah
Sad that ??= is only available for .NET Core :(Carcinogen
B
28

For one, it allows you to chain your assignments, as in your example:

a = b = c = 16;

For another, it allows you to assign and check a result in a single expression:

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

Both are possibly dubious reasons, but there are definitely people who like these constructs.

Bushido answered 27/9, 2010 at 19:40 Comment(3)
+1 Chaining isn't a critical feature but it's nice to see it "just works" when so many things don't.Mills
+1 for chaining also; I'd always thought of that as a special case that was handled by the language rather than the natural side effect of "expressions return values".Sparse
It also allows you to use assignment in returns: return (HttpContext.Current.Items["x"] = myvar);Stere
G
15

Aside from the reasons already mentioned (assignment chaining, set-and-test within while loops, etc), to properly use the using statement you need this feature:

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

MSDN discourages declaring the disposable object outside of the using statement, as it will then remain in scope even after it has been disposed (see the MSDN article I linked).

Glucose answered 27/9, 2010 at 19:47 Comment(6)
I don't think this is really assignment chaining per se.Josiahjosias
@kenny: Uh... No, but I never claimed it was, either. As I said, aside from the reasons already mentioned -- which includes assignment chaining -- the using statement would be a lot more difficult to use were it not for the fact that the assignment operator returns the result of the assignment operation. This isn't really related to assignment chaining at all.Chu
But as Eric noted above, "assignment statements do not return a value". This is actually just language syntax of the using statement, proof to that is that one can't use an "assignment expression" in a using statement.Josiahjosias
@kenny: Apologies, my terminology was clearly wrong. I do realize that the using statement could be implemented without the language having general support for assignment expressions returning a value. That would, in my eyes, be terribly inconsistent though.Chu
@kenny: Actually, one can use either a definition-and-assignment statement, or any expression in using. All of these are legal: using (X x = new X()), using (x = new X()), using (x). However, in this example, the contents of the using statement is a special syntax that doesn't rely at all on assignment returning a value - Font font3 = new Font("Arial", 10.0f) is not an expression and is not valid in any place that expects expressions.Myelitis
" to properly use the using statement you need this feature" -- No, you most certainly do not. "without the language having general support for assignment expressions returning a value" -- why are you talking about assignment expressions? This is a variable definition with an initializer -- which is not an expression. "That would, in my eyes, be terribly inconsistent though." -- Your example of using (definition) completely contradicts you ... there's nowhere else such a construction can be used.Alexanderalexandr
H
14

I'd like to elaborate on a specific point Eric Lippert made in his answer and put the spotlight on a particular occasion that hasn't at all been touched upon by anyone else. Eric said:

[...] an assignment almost always leaves behind the value that was just assigned in a register.

I'd like to say that the assignment will always leave behind the value we tried to assign to our left operand. Not just "almost always". But I don't know because I haven't found this issue commented in the documentation. It might theoretically be a very effective implemented procedure to "leave behind" and not reevaluate the left operand, but is it efficient?

'Efficient' yes for all the examples so far constructed in the answers of this thread. But efficient in the case of properties and indexers that use get- and set accessors? Not at all. Consider this code:

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

Here we have a property, which isn't even a wrapper for a private variable. Whenever called upon he shall return true, whenever one tries to set his value he shall do nothing. Thus whenever this property is evaluated, he shall be truthy. Let's see what happens:

Test test = new Test();

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

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

Guess what it prints? It prints Unexpected!!. As it turns out, the set accessor is indeed called, which does nothing. But thereafter, the get accessor is never called at all. The assignment simply leaves behind the false value we tried to assign to our property. And this false value is what the if statement evaluates.

I'll finish off with a real world example that got me researching this issue. I made an indexer which was a convenient wrapper for a collection (List<string>) that a class of mine had as a private variable.

The parameter sent to the indexer was a string, which was to be treated as a value in my collection. The get accessor would simply return true or false if that value existed in the list or not. Thus the get accessor was another way to use the List<T>.Contains method.

If the indexer's set accessor was called with a string as an argument and the right operand was a bool true, he would add that parameter to the list. But if the same parameter was sent to the accessor and the right operand was a bool false, he would instead delete the element from the list. Thus the set accessor was used as a convenient alternative to both List<T>.Add and List<T>.Remove.

I thought I had a neat and compact "API" wrapping the list with my own logic implemented as a gateway. With the help of an indexer alone I could do many things with a few set of keystrokes. For instance, how can I try to add a value to my list and verify that it's in there? I thought this was the only line of code necessary:

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

But as my earlier example showed, the get accessor which is supposed to see if the value really is in the list wasn't even called. The true value was always left behind effectively destroying whatever logic I had implemented in my get accessor.

Hemato answered 9/6, 2012 at 10:48 Comment(3)
Very interesting answer. And it's made me think positively about test-driven development.Arty
While this is interesting, it's a comment upon Eric's answer, not an answer to the OP's question.Alexanderalexandr
I wouldn't expect an attempted assignment expression to first get the value of the target property. If the variable is mutable, and the types match, why would it matter what the old value was? Just set the new value. I'm not a fan of assignment in IF tests though. Is it returning true because the assignment was successful, or it's some value e.g. a positive integer that is coerced to true, or because you are assigning a boolean? Regardless of the implementation, the logic is ambiguous.Ayakoayala
S
7

If assignment didn't return a value, the line a = b = c = 16 wouldn't work either.

Also being able to write things like while ((s = readLine()) != null) can be useful sometimes.

So the reason behind letting assignment return the assigned value, is to let you do those things.

Shelter answered 27/9, 2010 at 19:40 Comment(2)
Nobody's mentioned disadvantages - I came here wondering why assignments couldn't return null, in order that the common assignment vs comparison bug 'if (something = 1){}' would fail to compile rather than always returning true. I now see that that would create... other issues!Decarbonate
@Decarbonate that bug could be easily prevented by disallowing or warning about = occurring in an expression that isn't parenthesized (not counting the parens of the if/while statement itself). gcc gives such warnings and has thereby essentially eliminated this class of bugs from C/C++ programs that are compiled with it. It's a shame that other compiler writers have paid so little attention to this and several other good ideas in gcc.Alexanderalexandr
F
4

I think you're misunderstanding how the parser is going to interpret that syntax. The assignment will be evaluated first, and the result will then be compared to NULL, i.e. the statement is equivalent to:

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

As others have pointed out, the result of an assignment is the assigned value. I find it hard to imagine the advantage to having

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

and

s = "Hello";
s != null;

not be equivalent...

Fillmore answered 27/9, 2010 at 19:42 Comment(1)
While you are right from a practical sense that your two lines are equivalent your statement that the assignment is not returning a value is not technically true. My understanding is that the result of an assignment is a value (per the C# spec), and in that sense is no different from say the addition operator in an expression like 1 + 2 + 3Fleece
T
3

I think the main reason is the (intentional) similarity with C++ and C. Making the assigment operator (and a lot of other language constructs) behave like their C++ counterparts just follows the principle of least surprise, and any programmer coming from another curly-bracket language can use them without spending much thought. Being easy to pick up for C++ programmers was one of the main design goals for C#.

Troupe answered 27/9, 2010 at 19:46 Comment(0)
F
2

For the two reasons you include in your post
1) so you can do a = b = c = 16
2) so you can test if an assignment succeeded if ((s = openSomeHandle()) != null)

Fai answered 27/9, 2010 at 19:41 Comment(0)
B
1

The fact that 'a++' or 'printf("foo")' may be useful either as a self-contained statement or as a part of a larger expression means that C has to allow for the possibility that expression results may or may not be used. Given that, there's a general notion that expressions which might usefully 'return' a value may as well do so. Assignment chaining can be slightly "interesting" in C, and even more interesting in C++, if all the variables in question do not have precisely the same type. Such usages are probably best avoided.

Bracteate answered 27/9, 2010 at 22:48 Comment(0)
G
1

An extra advantage I don't see given in the answers here, is that the syntax for assignment is based on arithmetic.

Now x = y = b = c = 2 + 3 means something different in arithmetic than a C-style language; in arithmetic its an assertion, we state that x is equal to y etc. and in a C-style language it's an instruction that makes x equal to y etc. after it is executed.

This said, there's still enough relation between the arithmetic and the code that it doesn't make sense to disallow what is natural in the arithmetic unless there's a good reason. (The other thing that C-style languages took from the use of the equals symbol is the use of == for equality comparison. Here though because the right-most == returns a value this sort of chaining would be impossible.)

Grethel answered 2/10, 2010 at 17:50 Comment(3)
@JimBalter I wonder whether, in five years time, you might actually make an argument against.Grethel
@JimBalter no, your second comment is actually a counter argument, and a sound one. My thinking retort was because your first comment was not. Still, when learning arithmetic we learn a = b = c means that a and b and c are the same thing. When learning a C style language we learn that after a = b = c, a and b are the same thing as c. There's certainly a difference in semantics, as my answer itself says, but still when as a child I first learned to program in a language which did use = for assignment but didn't allow a = b = c this seemed unreasonable to me, though …Grethel
… unavoidable because that language also used = for equality comparison, so in that a = b = c would have to mean what a = b == c means in C-style languages. I found the chaining allowed in C a lot more intuitive because I could draw an analogy to arithmetic.Grethel
C
1

Another great example use-case, I use this all the time:

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once
Cauca answered 21/1, 2017 at 15:41 Comment(0)
M
1

I like using the assignment return value when I need to update a bunch of stuff and return whether or not there were any changes:

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;

Be careful though. You might think you can shorten it to this:

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

But this will actually stop evaluating the or statements after it finds the first true. In this case that means it stops assigning subsequent values once it assigns the first value that is different.

See https://dotnetfiddle.net/e05Rh8 to play around with this

Malignity answered 15/6, 2020 at 15:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.