5 KiB
C# Language Design Notes for Feb 28, 2017
Quote of the Day: "I don't have time to dislike your proposal, but I do!"
Agenda
- Conditional operator over refs (Yes, but no decision on syntax)
- Async Main (Allow Task-returning Main methods)
Conditional operator over refs
Champion "conditional ref operator"
Choice between two variables is not easily expressed, even with ref returns.
If array a1
is non-null, you want to modify its first element, otherwise you want to modify the first element of a2
:
Choose(a1 != null, ref a1[0], ref a2[0]) = value; // Null reference exception
It would be nice if the ternary conditional operator just worked with refs in the branches:
(a1 != null ? ref a1[0] : ref a2[0]) = value; // Right!
Both branches would have ref
, and would need to evaluate to variables of the same type.
Syntax
With the syntax as proposed, there's a concern that there will be an abundance of ref
keywords in common code, leading to confusion. For instance, to store the selected variable above in a ref local would be:
ref int r = ref (a1 != null ? ref a1[0] : ref a2[0]); // Four "ref"s
Alternative proposal: Do not add new syntax for this. Instead, if both branches of an existing conditional expression are variables of the same type, let the compiler treat the conditional expression as a whole as a variable:
(a1 != null ? a1[0] : a2[0]) = value;
ref int r = ref (a1 != null ? a1[0] : a2[0]); // Two "ref"s
This does have a problem that it would subtly change semantics of existing code. Think of a value type S
that has a mutating method M
:
(b ? v1 : v2).M();
This would now mutate the original v1
or v2
of type S
, instead of a copy! In practice such code today is probably buggy: why would you want to call a method to mutate a throw-away copy? But the fact remains that this would be a breaking change, unless we think up some very insidious mitigation.
An example of such a scheme would be to decide that a conditional is a variable, not based on what's inside it, but on what's around it. Whenever it's being used as a variable (
An approach is to define variable-ness of ternaries not from the inside out, but by "reinterpreting" it whenever it occurs in variable contexts not allowed today (ref, assignment).
Readonly and safe-to-return
This should also work for "ref readonly", if and when that goes in. For that to work, we'd either need to require further syntax (ref readonly
in the branches) or infer readonly-ness from the branches. The latter is more appetizing. We could:
- require both or neither branch be readonly
- infer that the conditional expression is readonly if at least one branch is readonly
Let's do 2. There seems to be no good reason for a stronger restriction.
Similarly we need to establish whether the resulting ref is safe-to-return. We can infer that the resulting ref is safe-to-return if both branches are safe-to-return.
Order of evaluation
There are subtle differences between the current spec and implementation when it comes to order of evaluation of an assignment. We need to make sure we fully understand how that plays in when the left hand side of an assignment is a conditional expression.
Conclusion
- Yes to doing it
- Syntax: leaning towards no refs inside (recursively), but worried about breaking changes
- Precedence not an issue -
ref
is not part of the expression - Field like events: sure, if it makes sense
Async Main
We want to allow program entry points (Main
methods) that contain await
. The temptation is to allow the async
keyword on existing entry points returning void
(or int
?). However, that makes it look like an async void
method, which is fire-and-forget, whereas we actually want program execution to wait for the main method to finish. So we'd have to make it so that if the method is invoked as an entry point we (somehow) wait for it to finish, whereas if it's invoked by another method, it is fire-and-forget.
That is not ideal. Instead, we should allow new entry points returning Task
and Task<int>
:
Task Main();
Task Main(string[] args);
Task<int> Main();
Task<int> Main(string[] args);
Since such methods can exist today, but are not entry points, we should give precedence to existing entry points for backwards compatibility.
These new entry points are then entered as if being called like this:
Main(...).GetAwaiter().GetResult();
In other words, they rely on the built-in capability of Task
awaiters to block on the getting the result.
C# 7.0 allows declaration of other "tasklike" types. We won't allow those in entry points yet, but that is easy to work around: Just await
your tasklike in an async Task Main
.
Conclusion
Allow Task
and Task<int>
returning Main
methods as entry points, invoking as Main(...).GetAwaiter().GetResult()
.