The "readonly references" feature is actually a group of features that leverage the efficiency of passing variables by reference, but without exposing the data to modifications:
There is an existing proposal that touches this topic https://github.com/dotnet/roslyn/issues/115 as a special case of readonly parameters without going into many details.
Here I just want to acknowledge that the idea by itself is not very new.
## Motivation
C# lacks an efficient way of expressing a desire to pass struct variables into method calls for readonly purposes with no intention of modifying. Regular by-value argument passing implies copying, which adds unnecessary costs. That drives users to use by-ref argument passing and rely on comments/documentation to indicate that the data is not supposed to be mutated by the callee. It is not a good solution for many reasons.
The examples are numerous - vector/matrix math operators in graphics libraries like [XNA](https://msdn.microsoft.com/en-us/library/bb194944.aspx) are known to have ref operands purely because of performance considerations. There is code in Roslyn compiler itself that uses structs to avoid allocations and then passes them by reference to avoid copying costs.
Similarly to the `out` parameters, `in` parameters are passed as managed references with additional guarantee from the callee.
Unlike `out` parameters that _must_ be assigned by the callee before any other use, `in` parameters cannot be assigned at all.
`in` parameters allow for effectiveness of indirect argument passing without exposing arguments to mutations by the calee.
## Declaring `in` parameters
The proposed syntax is to use existing `in` keyword at declaration site.
For all purposes the `in` parameter is treated as a `readonly` variable. Most of the restrictions on the use of `in` parameter inside the method are the same as with `readonly` fields.
> Indeed an `in` parameter may represent a `readonly` field. Similarity of restrictions is not a coincidence.
-`in` parameters and returns are allowed anywhere where ordinary byval parameters are allowed. This includes indexers, operators (including conversions), delegates, lambdas, local functions.
> ```C#
> (in int x) => x // lambda expression
> TValue this[in TKey index]; // indexer
> public static Vector3 operator +(in Vector3 x, in Vector3 y) => ... // operator
- For the purpose of delegate/lambda/method group conversions, `in` will behave similarly to an `out` parameter.
Lambdas and applicable method group conversion candidates will have to match `in` parameters of the target delegate with `in` parameters of an identity-convertible type.
> NOTE: There are no warnings on `in` parameters that are reference types and primitives.
It may be pointless in general, but in some cases user must/want to pass primitives as `in`. Examples - overriding a generic method like `Method(in T param)` when `T` was substituted to be `int`, or when having methods like `Volatile.Read(in int location)`
>
> It is conceivable to have an analyzer that warns in cases of inefficient use of `in` parameters, but the rules for such analysis would be too fuzzy to be a part of a language specification.
The motivation for the above is that `in` arguments guarantee _aliasing_ of the argument variable. The callee always receives a direct reference to the same location as represented by the argument.
- in rare situations when `in` arguments must be stack-spilled due to mixed use with `await`, the behavior is the same as with `out` and `ref` arguments - if the variable cannot be spilled in referentially-transparent manner, an error is reported.
`staticField` is a static field which can be accessed more than once without observable sideeffects. Therefore both the order of sideeffects and aliasing requirements can be provided.
2) `M1(in RefReturningMethod(), await SomethingAsync())` will produce an error.
`RefReturningMethod()` is a `ref` returning method. Accessing method may have observable sideeffects, therefore it must be evaluated before `await`. However the result of the invocation is a reference that cannot be preserved across `await` suspension point which make the direct reference requirement impossible.
> NOTE: the stack spilling errors are considered to be implementation-specific limitations. Therefore they do not have effect on overload resolution or lambda inference.
Ordinary byval arguments can match `in` parameters. In such case the arguments have the same relaxed constraints as ordinary byval arguments would have.
The motivation for this is that `in` parameters in APIs may result in inconveniences for the user when arguments cannot be passed as a direct reference - ex: literals, computed or `await`-ed results or arguments that happen to have more specific types.
All these cases have a trivial solution of storing the argument value in a temporary local or appropriate type and passing that local as an `in` argument.
To reduce the need for such boilerplate code compiler can perform the same transformation, if needed, when `in` modifier is not present at the call site.
In addition, in some cases, such as invocation of operators, or `in` extensions methods, there is no syntactical way to specify `in` at all. That alone requires specifying the behavior of an ordinary byval arguments when they match `in` parameters.
- in a case of ordinary parameters, implicit conversions are allowed.
A reference to a temporary holding converted value is passed in such case.
Example:
```C#
Print<int>(Short.MaxValue) // not an error.
```
- in a case of a receiver of an `in` extension method, implicit _this-argument-conversions_ are allowed.
- cases of argument spilling due to `async` are allowed.
If order of sideeffects requires creating an argument copy, then a copy is created.
Example:
```C#
M1(RefReturningMethod(), await SomethingAsync()) // not an error.
```
Since the result of a sideeffecting invocation is a reference that cannot be preserved across `await` suspension, a temporary containing the actual value will be preserved instead.
### omitted optional arguments
It is permitted for an `in` parameter to specify default value. That make the corresponding argument optional.
Omitting optional argument results in passing the default value via a temporary.
In addition, if the method is *abstract* or *virtual*, then the signature of such parameters (and only such parameters) must have `modreq[System.Runtime.CompilerServices.IsReadOnlyAttribute]`.
The motivation for this sub-feature is roughly symmetrical to the reasons for the `in` parameters - avoiding copying, but on the returning side. A method or an indexer has currently two options: 1) return by reference and be exposed to possible mutations or 2) return by value which results in copying.
(a more concise single modifier like `in` has been elusive for this case).
The feature will allow a member to return variables by reference without exposing them to mutations.
## Declaring `ref readonly` returning members
`ref readonly` will be used to modify member signatures to indicate the return is passed as a readonly reference.
For all purposes a `ref readonly` member is treated as a `readonly` variable - similar to `readonly` fields and `in` parameters.
For example fields of `ref readonly` member which has a struct type are all recursively classified as `readonly` variables. - It is permitted to pass them as `in` arguments, but not as `ref` or `out` arguments.
- For the purpose of delegate/lambda/method group conversions, `ref readonly` is similar but distinct from `ref`.
Lambdas and applicable method group conversion candidates will have to match `ref readonly` return of the target delegate with `ref readonly` return of the type that is identity-convertible.
> NOTE: There are no warnings on `in` parameters that are reference types and primitives.
It may be pointless in general, but in some cases user must/want to pass primitives as `in`. Examples - overriding a generic method like `ref readonly T Method()` when `T` was substituted to be `int`.
>
>It is conceivable to have an analyzer that warns in cases of inefficient use of `ref readonly` parameters, but the rules for such analysis would be too fuzzy to be a part of a language specification.
The motivation is that `return ref readonly <expression>` is unnecessary long and only allows for mismatches on `readonly` part that would always be errors.
The `ref` is, however, required for consistency with other scenarios where something is passed via strict aliasing vs. by value.
> Unlike the case with `in` parameters, `ref readonly` returns never return via a copy. Considering that the copy would cease to exist immediately upon returning such practice would be pointless and dangerous. Therefore `ref readonly` returns always pass direct references.
Note that a `ref readonly` can be obtained through a regular ref, but not the other way around. Otherwise the safety of `ref readonly`s is inferred the same way as for the regular refs.
We also must consider the situation of RValues passed as `in` parameters via a copy and then coming back in a form of a `ref readonly` and thus the result of the invocation is clearly unsafe to return.
*Specifically RValues are safe to pass as in parameters.*
> NOTE: There are additional rules regarding returnability that come into play when ref-like types and ref-reassignments are involved.
> The rules equally apply to `ref` and `ref readonly` members and therefore are not mentioned here.
## Aliasing behavior.
`ref readonly` members provide the same aliasing behavior as ordinary `ref` members (except for being readonly).
Therefore for the purpose of capturing in lambdas, async, iterators, stack spilling etc... the same restrictions apply. - I.E. due to inability to capture the actual references and due to sideeffecting nature of member evaluation such scenarios would be disallowed.
> It is permitted and required to make a copy when `ref readonly` return is a receiver of regular struct methods, which take `this` as an ordinary writeable ref. Historically we would make an invocation on a copy in such scenario.
## Metadata representation.
When `System.Runtime.CompilerServices.IsReadOnlyAttribute` is applied to the return of a byref returning method, it means that the method returns a readonly reference.
In addition, the result signature of such methods (and only those methods) must have `modreq[System.Runtime.CompilerServices.IsReadOnlyAttribute]`.
In short - a feature that makes `this` parameter of all instance members of a struct, except for constructors, an `in` parameter.
## Motivation
Compiler must assume that any method call on a struct instance may modify the instance. Indeed a writeable reference is passed to the method as `this` parameter and fully enables this behavior. To allow such invocations on `readonly` variables, the invocations are applied to temp copies. That could be unintuitive and sometimes forces people to abandon `readonly` for performance reasons.
After adding support for `in` parameters and `ref redonly` returns the problem of defensive copying will get worse since readonly variables will become more common.
## Solution
Allow `readonly` modifier on struct declarations which would result in `this` being an `in` parameter on all struct instance methods except for constructors.
```C#
static void Test(in Vector3 v1)
{
// no need to make a copy of v1 since Vector3 is a readonly struct
System.Console.WriteLine(v1.ToString());
}
readonly struct Vector3
{
. . .
public override string ToString()
{
// not OK!! "this" is an `in` parameter
foo(ref this.X);
// OK
return $"X: {X}, Y: {Y}, Z: {Z}";
}
}
```
## Restrictions on members of readonly struct
- instance fields of a readonly struct must be readonly.
**Motivation:** can only be written to externally, but not through members.
- instance autoproperties of a readonly struct must be get-only.
**Motivation:** consequence of restriction on instance fields.
- readonly struct may not declare events.
**Motivation:** consequence of restriction on instance fields.
- The identity of the `IsReadOnlyAttribute` type is unimportant. In fact it can be embedded by the compiler in the containing assembly if needed.
- Applying `IsReadOnlyAttribute` to targets not listed here - to byval local slots, byval returns/parameters, reference types, etc... is reserved for the future use and in scope of this proposal results in verification errors.
# `ref`/`in` extension methods
There is actually an existing proposal (https://github.com/dotnet/roslyn/issues/165) and corresponding prototype PR (https://github.com/dotnet/roslyn/pull/15650).
I just want to acknowledge that this idea is not entirely new. It is, however, relevant here since `ref readonly` elegantly removes the most contentious issue about such methods - what to do with RValue receivers.
The general idea of the proposal is to allow extension methods to take the `this` parameter by reference, as long as the type is known to be a struct type.
`obj.stringField?.RefExtension(...)` - need to capture a copy of `stringField` to make the null check meaningful, but then assignments to `this` inside RefExtension would not be reflected back to the field.
An ability to declare extension methods on **structs** that take the first argument by reference was a long-standing request. One of the blocking consideration was "what happens if receiver is not an LValue?".
- There is a precedent that any extension method could also be called as a static method (sometimes it is the only way to resolve ambiguity). It would dictate that RValue receivers would be disallowed.
- On the other hand there is a practice of making invocation on a copy in similar situations when struct instance methods are involved.
The reason why the "implicit copying" exists is because the majority of struct methods do not actually modify the struct while not being able to indicate that. Therefore the most practical solution was to just make the invocation on a copy, but this practice is known for harming performance and causing bugs.
Now, with availability of `in` parameters, it is possible for an extension to signal the intent. Therefore the conundrum can be resolved by requiring `ref` extensions to be called with writeable receivers while `in` extensions would permit implicit copying if necessary.
The purpose of `ref` extension methods is to mutate the receiver directly or by invoking mutating members. Therefore `ref this T` extensions are allowed as long as `T` is constrained to be a struct.
On the other hand `in` extension methods exist specifically to reduce implicit copying. However any use of an `in T` parameter will have to be done through an interface member. Since all interface members are considered mutating, any such use would require a copy. - Instead of reducing copying, the effect would be the opposite. Therefore `in this T` are not allowed when `T` is a generic type parameter regardless of constraints.
Once `ref readonly` members were introduced, it was clear from the use that they need to be paired with appropriate kind of local. Evaluation of a member may produce or observe sideeffects, therefore if the result must be used more than once, it needs to be stored. Ordinary `ref` locals do not help here since they cannot be assigned a `readonly` reference.
Allow declaring `ref readonly` locals. This is a new kind of byref locals that is not writeable. As a result `ref readonly` locals can accept references to readonly variables without exposing the variables to writes.
## Declaring and using `ref readonly` locals.
The proposed syntax is to use `ref readonly` at declaration site. Similarly to ordinary `ref` locals, `ref readonly` locals must be ref-initialized at declaration. Unlike regular `ref` locals, `ref readonly` locals can refer to `readonly` LValues - `in` parameters, `readonly` fields, `ref readonly` methods.
For all purposes the `ref readonly` local is treated as a `readonly` variable. Most of the restrictions on the use are same as with `readonly` fields or `in` parameters.
For example fields of an `in` parameter which has a struct type are all recursively classified as `readonly` variables .
Except for their `readonly` nature, `ref readonly` locals behave like ordinary `ref` locals and are subject to exactly same restrictions.
For example restrictions related to capturing in closures, declaring in `async` methods or the `safe-to-return` tracking equally applies to `ref readonly` locals.
Note that `Choice` is not an exact replacement of a ternary since _all_ arguments must be evaluated at the call site, which was leading to unintuitive behavior and bugs.
// will crash with NRE because 'arr[0]' will be executed unconditionally
ref var r = ref Choice(arr != null, ref arr[0], ref otherArr[0]);
```
## Solution
Allow special kind of conditional expression that evaluates to a reference to one or another LValue argument based on a condition.
## Using `ref` ternary expression.
The proposed syntax for the `ref` flavor of a conditional expression is ` <condition> ? ref <consequence> : ref <alternative>;`
Just like with the ordinary conditional expression only `<consequence>` or `<alternative>` is evaluated based on result of the boolean condition expression.