111 lines
4.2 KiB
Markdown
111 lines
4.2 KiB
Markdown
|
# C# Language Design Notes for Sep 27, 2017
|
||
|
|
||
|
|
||
|
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
|
||
|
|
||
|
QOTD: `in int i = in init(in it);`
|
||
|
|
||
|
We changed our minds to allow and require explicit `ref` for `ref readonly` arguments.
|
||
|
|
||
|
``` c#
|
||
|
const int x = 42;
|
||
|
foo(ref x);
|
||
|
```
|
||
|
|
||
|
In the compiler, the order of things change, which is the most churning fix.
|
||
|
|
||
|
Technically not hard to do.
|
||
|
|
||
|
## Concern 1
|
||
|
Is 'ref' the right keyword? Putting `ref` signals to the caller that mutation may be happening. You need to understand what you are passing it *to* in order to know whether it's safe from mutation.
|
||
|
|
||
|
In other cases, like `return ref x` or `ref readonly r = ref x` the `readonly` is nearby, and it doesn't cause the same concern.
|
||
|
|
||
|
## Concern 2
|
||
|
Also, the `ref` in front of rvalues feels wrong.
|
||
|
|
||
|
``` c#
|
||
|
Foo(ref 42);
|
||
|
Foo(ref await FooAsync());
|
||
|
```
|
||
|
|
||
|
There's an argument that `ref` is about *how* the value is passed, not about the guarantees of the callee. On the other hand we use both `out` and `ref`, where the only difference is the guarantee.
|
||
|
|
||
|
Compromise position:
|
||
|
|
||
|
Use keyword to pass by ref, no keyword to pass "by value" (really by creating a new local and putting the value in).
|
||
|
|
||
|
Warn (or error) when an lvalue is being passed without the keyword, that could have been passed by ref.
|
||
|
|
||
|
* Is it important to be able to see in the code if something is passed by ref?
|
||
|
* Is it important to be able to see in the code if something is copied?
|
||
|
* Is it important that the parameter passing mode reflects the contract?
|
||
|
* Is it important that the same keyword is used for passing and returning?
|
||
|
|
||
|
Other compromise:
|
||
|
- No modifier: Do you best
|
||
|
- Modifier: Require ref, don't copy
|
||
|
|
||
|
The keyword would be `in`.
|
||
|
|
||
|
Should it then also be `in` in parameter declarations? A danger is that people misunderstand it for "being explicit" about value parameters, whereas `ref readonly` leaves no such room for interpretation. On the other hand, this may be a case where we'd overoptimize for newcomers to the feature, leaving too much syntax for too little benefit. A bit of education, maybe some warnings, maybe analyzers...
|
||
|
|
||
|
## Conclusion
|
||
|
|
||
|
- At the parameter declaration site, use `in`. `ref readonly` is no longer allowed.
|
||
|
- At the call site, `in` is optional. If it is used, the argument has to an lvalue that can be passed directly by ref. If not, then the compiler does its best: copy only if necessary.
|
||
|
No change to `ref readonly` return signature, or to `return ref`.
|
||
|
No change to `ref readonly int r = ref ...`
|
||
|
|
||
|
We would consider allowing `ref readonly int r = 42;` in the future, but it is only useful once we have ref reassignment.
|
||
|
|
||
|
We could consider a warning for `in` parameters of small types (reference types and built-in value types, maybe), but sometimes you do need that. Better left to an analyzer.
|
||
|
|
||
|
What about ambiguity:
|
||
|
|
||
|
``` c#
|
||
|
M(in int x){}
|
||
|
M(int x){}
|
||
|
|
||
|
M(42); // how do you resolve in direction of value
|
||
|
```
|
||
|
|
||
|
We could deal with this through a tie breaker in overload resolution, but it's probably not worth it.
|
||
|
|
||
|
If you have an API like that, there's going to be a method overload you cannot call. We could always add it later.
|
||
|
|
||
|
This scenario isn't entirely esoteric: if I want to update my API from value-passing to in-passing, then either:
|
||
|
|
||
|
- I add an overload. Then all existing call sites are broken on recompilation.
|
||
|
- I replace the current overload. The all existing assemblies are broken until recompilation.
|
||
|
|
||
|
Instead, you can add an optional parameter, change parameter names etc. to give another means of distinguishing.
|
||
|
|
||
|
|
||
|
|
||
|
Delegate conversion: No contravariance in the type of parameters, unlike value parameters. THis is because the CLR doesn't know about it. It's similar to the restriction on out parameters.
|
||
|
|
||
|
|
||
|
Conditional: If one of the branches is readonly, the whole thing is readonly. That means that calling a mutating member on it would mutate a copy, *regardless* of whether the actual branch chosen was readonly or not:
|
||
|
|
||
|
``` c#
|
||
|
void M(bool b, in x, ref y)
|
||
|
{
|
||
|
(b ? ref x : ref y).M(); // M is always called on a copy, even if b is false
|
||
|
}
|
||
|
```
|
||
|
|
||
|
# ref structs
|
||
|
|
||
|
## Syntax
|
||
|
|
||
|
Partial has to keep being the last modifier; ref can for now only be right before `struct` or `partial struct`.
|
||
|
|
||
|
Long term we want `ref` to float freely as a modifier. Just may not get to do the work now.
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|