csharplang/meetings/2017/LDM-2017-02-22.md
2017-04-04 15:17:33 -07:00

6.1 KiB

C# Language Design Notes for Feb 22, 2017

Agenda

We went over the proposal for ref readonly: Champion "Readonly ref".

readonly refs

Passing and returning by ref addresses the copying and out parameter nightmare of performance critical code over structs. However, the use of it comes with risk: the recipient of a passed or returned ref can freely modify it.

This proposal introduces a readonly modifier on ref parameters and returns that incurs restrictions on the recipient of the ref similar to those incurred by a readonly modifier on a field. Thus, a ref can safely be passed or returned without risk of the recipient mutating it. This eliminates one common source of (deliberate, defensive) struct copying in C# code.

Conclusion

We want to support this.

readonly struct types

In and of itself, though, readonly refs contribute to another source of struct copying: when a method or property is invoked on a readonly struct (including now a readonly ref parameter or local), the C# compiler implicitly copies the struct, as a defense against the method or property invocation mutating the original struct. In the common case where the member is not actually mutating the struct, this is pure waste.

To counter this, the proposal also allows the readonly modifier on struct type declarations themselves. This signifies that no function member is mutating, and therefore calling them does not need to incur a copy. Inside struct members, this would be a readonly ref instead of a ref.

Once we have this feature, we could start warning on the compiler's defensive copying (with a warning wave for back compat).

More detailed versions are possible, where readonly-ness can be per struct member. This may not be necessary in practice, based on experience from the Midori project. (Or we can make it not strictly enforced, or allow exceptions that are somehow marked.)

Conclusion

Support readonly just on whole struct types for now, until and unless evidence shows the need for per-member decisions.

Syntax

We've been calling this feature "readonly ref", but it is really not the ref that is readonly; rather it is the thing that is referenced. For that reason is more correct to call it ref readonly, as in:

public ref readonly int Choose(ref readonly int i1, ref readonly int i2) { ... }

Currently, refs themselves are always single-assignment in C#, but if we decide at some point to make them reassignable by default (which we could do without breaking), then you might want to be able to explicitly put a readonly modifier on the refs themselves. That would be readonly ref. And of course you could have readonly ref readonly where both the ref and the target are readonly.

in parameters

ref readonly is a bit of a mouthful. For parameters, ref readonly is in fact the exact opposite of out: something that comes in but cannot be modified. It would make total sense to call them in parameters. However, it would probably be confusing to call a return value in, so in should be allowed as a shorthand for ref readonly exclusively on parameters.

public ref readonly int Choose(in int i1, in int i2) { ... } // Equivalent to the above

Call site annotations

With today's refs you need a call site ref annotation. This is to warn the caller of potential side effects and make sure they buy into them. Do we need it for ref readonly parameters, where there are no side effects by definition? We believe not.

int x = 1, y = 2;
int z = Choose(x, y); // implicitly passed by readonly ref

We didn't question the ref annotation on return statements in ref readonly returning methods, but maybe we should.

Values as in arguments?

The proposal allows literals to be passed as an in argument. A variable is created under the hood, assigned the value and passed along. Is that bad? On the one hand, it makes for less predictable performance, in that some arguments cause copying (into a fresh variable), and others do not. On the other hand, since no ref is required, the arguments look like value arguments, that are already copied.

At least initially, as we prototype this, we will allow any expression of the right type, and we will just copy into a shadow variable when necessary. Even when a variable of a different but convertible type is passed. If this turns out to be a problem, or a cause of confusion or overly defensive programming, then we'll reevaluate.

Extension methods

We want extension methods on structs to be able to work by ref, so that they don't copy. VB already allows this. We should allow both ref and ref readonly extension methods on value types.

Ref extension methods on classes could be controversial - like an EnsureNotNull method that replaces the object reference in the variable. We are not necessarily opposed to this in principle, but we don't need it for this scenario, and we'd have to track down weird consequences of it. So for now, extension methods with ref and in this-parameters must extend a value type.

Readonly parameters and locals

A separate feature, but seems to be part of the same package, and is a long standing request. Let's try to do these at the same time.

Versioning

What happens when older code references newer code with ref readonly return types? The old compiler might not understand the readonly annotation, and would happily allow mutation. Upgrade the compiler, and you're broken. Can we encode it in metadata in such a way that it breaks downlevel? modreqs? Possibly, but then putting it on existing libraries is a library breaking change.

The CLR already does not protect readonly-ness, and there are therefore already ways you can circumvent it and mutate anyway. You can't avoid bad actors (reflection etc), but we would like to avoid accidentally breaking guarantees, and also breaking code.

Conclusion

We'll build the first version using attributes, and therefore leaving us open to breaking on upgrade. We'll consider if there are mitigations, but poisoning downlevel code, e.g. with modreqs, seems like too big of a hammer.