Most of these decisions logically follow from the general directions as discussed in LDM meetings. In many cases they were vetted in LDM meeting by quick checks, in email or in online spec discussions. They are collected here to make sure they are recorded in one place and to make sure once more that all stakeholders are ok with this going forward.
This could also be a good resource when writing a testplan or a spec for these features.
ref readonly
parameters and returns are allowed anywhere where a byval parameters are allowed. This includes indexers, operators (including conversions), delegates, lambdas, local functions.ref readonly
returns allowed anywhere whereref
returns are allowed. I.E. indexers, operators (including conversions), delegates, lambdas, local functions, but not in operators.- There are no
ref readonly
locals. - There are no warnings on
ref [readonly]
with reference types and primitives. It may be pointless in general, but in some cases user must/want to pass primitives asref readonly
. Examples - overriding a generic method likeMethod(ref readonly T param)
when T was substituted to beint
, or when having methods likeVolatile.Read(ref readonly int location)
Some cases where ref readonly
is not allowed can be allowed in the future if there is a need.
ref readonly
arguments are not required to be lvalues. When argument is not an lvalue, a temporary is used.ref readonly
parameter may have default values. When not specified by the call site, they are passed via a temporary.ref readonly
arguments may have implicit conversions, including those that do not preserve identity. A temporary is used in those cases.- the life time of the temporaries matches at least the closest encompassing scope. (in reality could be smaller if no observable difference).
- for the purpose of lambda/async capturing
in
parameters are still opaque references and as such cannot be lifted.
ref readonly
arguments look exactly the same as ordinary byval parameters - no modifiers. This includes arguments of operators, and receivers of extension methods.
void (ref readonly int arg){...};
M1(obj.field);
M1(42);
M1();
public static T1 operator +(ref readonly T1 x, ref readonly T1 y) {...};
a + b;
- returns in a
ref readonly
methods must specifyref
to signify that reference is taken. (Note we never return a ref of a copy)
ref readonly string M() => ref String.Empty;
// error - needs 'ref'
ref readonly string M() => "qq";
// error - not an lvalue
ref readonly string M() => ref "qq";
- since the user provides no modifiers at the call site, the signatures will equally match ordinary byval parameters and
ref readonly
parameters. If both are present, an ambiguity error is reported. - arguments to
ref readonly
parameters set lower bounds during method type inference. - in fact for the purpose of overload resolution
ref readonly
parameters behave as effectively byval parameters.
- since both delegates and lambdas can express RefKind of parameters, the parameter RefKinds of successful conversion candidates must match, similarly to how it works with
ref/out
parameters. - for the purpose of variance
ref readonly
is considered non-variant.
class Program
{
delegate void DRef<T>(ref T arg);
delegate void DIn<T>(ref readonly T arg);
static T Generic<T>(DIn<T> arg1, DIn<T> arg2) { return default; }
static void Main()
{
// error - parameter ref kind must match
DRef<Exception> d1 = (ref readonly Exception arg) => throw null;
// error - T inference is nonvariant/exact and therefore ambiguous
Generic((ref readonly Exception arg) => throw null, (ref readonly object arg) => throw null);
}
}
- async methods cannot have
ref readonly
parameters or returns - at call sites, unlike
ref
arguments,ref readonly
arguments never cause spilling related errors. User does not specifyref
orout
and as such spilling errors would be an unexpected nuisance. - when spilling by reference is possible (fields, array elements), we spill by reference and preserve aliasing.
- when spilling by reference is not possible (ref methods, ref ternary), we spill by value.
- NOTE: in rare cases this may cause an observable difference between calling with
await
in the signature and without. Ordinaryref
in these cases just produces an error.
void M1(ref readonly int arg1, ref readonly int arg2){...};
ref int M2() {...};
async Task<int> M3() {...};
// valid. first argument is spilled via a copy
M1(M2(), await M3()){...}
- the syntax is
condition? ref variable1: ref variable2
- ref ternary is an lvalue. Can be passed/returned by reference. Can be assigned to.
- consequence/alternative operands of ref ternary must be lvalue variables.
// errors
true? ref 42, ref 2 + 2
- readonly lvalue operands are allowed resulting in a whole expression being a readonly lvalue.
// pass by ref
Method1(ref condition? ref x: ref y);
// pass by out
Method3(out condition? ref x: ref y);
// assign
(condition ? ref x: ref y) = 123;
// assign
(condition ? ref x: ref y) = 123;
// return as readonly ref
ref readonly string MethodDecl() => ref condition?
ref string.Empty:
ref x;
- The syntax is
ref struct S1{ . .}
- A particular nuance here –
ref
is a contextual modifier. For historical reasonsref
in declarations can also be a ref-type-operator. Therefore it must be contextual to avoid syntactical ambiguities. - The disambiguating context here is "immediately preceeding
struct
keyword". - There is an interaction with another contextual keyword in this space -
partial
. We now allowpartial
to be used beforeref struct
.
// valid
public unsafe ref struct S1{}
// also valid
unsafe readonly public partial ref struct S1{}
// not valid - 'ref' must go immediately before 'struct'
unsafe ref readonly public partial struct S1{}
- NOTE: there is no requirement for modifiers to match between parts of a partial type. That is done to facilitate code-generator scenarios. For the eventual semantic meaning we consider the union of all modifiers across partials.
ref
is not an exception from the existing rules.
// effectively a 'public readonly ref struct S1{..}'
public partial struct S1{}
partial ref struct S1{}
readonly partial struct S1{}
- the syntax is
readonly struct
. this
is aref readonly
variable in all members except constructors.- the
readonly
here is a true modifier and can be specified in any order with other modifiers. readonly
structs cannot have writeable fields, autoprops or field-like events
- the syntax is
Span<int> sp = stackalloc int[100];
stackalloc int[100]
is now treated as a stack-allocated array literal and can be target-typed toSpan<T>
whereT
must match the type of the array.- we support target typing specifically to a well-known type
System.Span<T>
. - the StackAllocToSpan conversion is a standard conversion and can "stack" with user defined operators to form user-defined conversion, including implicit conversions. The following is valid:
// Span<T> has implicit conversion to ReadOnlySpan<T>
ReadOnlySpan<int> sp = stackallock int[10];
- safety rules for ref-like structs:
Doc: https://github.com/dotnet/csharplang/blob/master/proposals/span-safety.md
we now support the following 3 cases:
T this
– existing case, ok for any kind of receiver type.ref T this
– ok with structs or with generics constrained to structsref readonly T this
– ok with actual structs, but not ok with generic type parameter T regardless of constraints.
The purpose of ref readonly
is to avoid unnecessary copy, but with generic types, nearly all uses inside the extension will have to be done through interface methods and ref readonly
receiver will need to be copied every time. As a result the user will actually increase implicit copying, possibly dramatically.
It is never a good thing to use ref readonly
with generics. We do not want this to lead user on wrong path.
- receiver of a
ref
extension method must be an lvalue. Invoking aref
extension on a readonly field or an rvalue is an error. ref
receiver requires that receiver is identity-convertible to the type ofthis
parameter. We do not make copies when invoking a method whose purpose is to mutate.- invoking a
ref readonly
extension on a readonly field or an rvalue is not an error. ref readonly
receiver will permit implicit conversions in the same way as static method would permit.
NOTE: It is always possible to go from an extension method syntax to a static method invocation. This is preexisting design constraint since user could be forced to do the substitution in ambiguous situations.
- readonly struct in metadata is just a struct decorated with
[IsReadOnly]
attribute - ref struct is a struct with an [IsByRefLike]` attribute
- [ref readonly] parameters/returns are byref parameters decorated with
[IsReadOnly]
attributes. - attributes used by these features are not allowed to be used directly in source. This is breaking in compiler upgrade scenario, but was historically done to not having to rationalize numerous combinations of these attributes and features that use them if such use is allowed.
- attributes used by these feature are either found in the containing compilation or "embedded" by the compile as a private type in the containing assembly.
- embedded attributes are decorated with
[Embedded]
which itself is always embedded and visible/bindable from the source (so that user could not "embed" source types).
ref readonly
parameters are marked withmodreq(InAttribute)
, except for parameters of methods that cannot be "overriden" with a concrete implementation. This is done to prevent non-enlightened compiler to provide an implementation that does not respect the contract.- Delegate/interface methods are considered "overridable" for the purpose above.
ref readonly
returns are always marked withmodreq(InAttribute)
. This is done to prevent non-enlightened compiler writing through the reference.ref structs
are marked withObsoleteAttribute(“Types with embedded references are not supported in this version of your compiler.”, error=true)
. This is done to prevent non-enlightened compiler to use the types in unsafe ways.ref structs
are poisoned conditionally. If a method is already [Obsolete] or [Deprecated]. We honor the attribute supplied by the user and cannot emit ours without a clash. We may consider giving a warning for such cases, but none is given right now.- poisoning is optional when importing metadata to simplify the contract. When overriding "unpoisoned" signatures we will keep them "unpoisoned".