csharplang/meetings/2017/LDM-2017-09-27.md
2017-10-05 20:15:39 -07:00

4.2 KiB

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.

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.

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:

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:

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.