109 lines
5 KiB
Markdown
109 lines
5 KiB
Markdown
# 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
|
|
|
|
1. Conditional operator over refs (*Yes, but no decision on syntax*)
|
|
2. Async Main (*Allow Task-returning Main methods*)
|
|
|
|
|
|
# Conditional operator over refs
|
|
|
|
[Champion "conditional ref operator"](https://github.com/dotnet/csharplang/issues/223)
|
|
|
|
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`:
|
|
|
|
``` c#
|
|
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:
|
|
|
|
``` c#
|
|
(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:
|
|
|
|
``` c#
|
|
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:
|
|
|
|
``` c#
|
|
(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`:
|
|
|
|
``` c#
|
|
(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"](https://github.com/dotnet/csharplang/issues/38), 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:
|
|
|
|
1. require both or neither branch be readonly
|
|
2. 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
|
|
|
|
[Champion "Async Main"](https://github.com/dotnet/csharplang/issues/97)
|
|
|
|
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>`:
|
|
|
|
``` c#
|
|
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 new entry points are then entered as if being called like this:
|
|
|
|
``` c#
|
|
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()`.
|