csharplang
csharplang copied to clipboard
[Proposal]: Recent changes to ref fields specification
Summarizing recent changes to the ref fields specification and related questions.
1. ref scoped removed
ref scoped has been removed.
scoped is now limited to:
- by-ref parameters and locals
- by-value
ref structparameters and locals
static void F1(scoped in int i) { } // scoped ref: ok
static void F2(scoped Span<int> s) { } // scoped ref struct: ok
static void F3(ref scoped Span<int> s) { } // ref scoped: not supported
ref scoped does not prevent assigning through the ref
static void F3(ref scoped Span<int> s)
{
s = stackalloc int[] { 1 }; // escapes to caller
}
2. ref field cannot refer to a ref struct type
Until now, the feature has codified existing C#10 rules, while adding support for ref fields.
Allowing ref fields to ref struct types would require additional escape rules.
First, a readonly ref struct could store ref state.
That means a readonly ref struct argument to a method is now a potential place that references can be captured.
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> s) { Span = s; }
}
Second, we'd need to track lifetime of nested refs.
ref struct Container
{
public ref Span<int> Nested;
}
Span<int> GetSpan(ref Container container) => container.Nested; // lifetime no shorter than container
3. Certain parameters are implicitly scoped
The recent change is to treat ref parameters that refer to ref struct types as implicitly scoped.
That addition means the set of parameters that are implicitly scoped is now:
thisforstructinstance methodsrefparameters that refer toref structtypesoutparameters
The first case, this for struct instance methods, is for compatibility with C#10 rules which considered this implicitly scoped.
The downside to implicitly scoping this is a struct instance method cannot return a field by reference.
struct S
{
public int F;
ref int GetRef() => ref F; // error: cannot return 'this' or members by ref
}
static ref int GetRef(ref S s) => ref s.F; // ok
Making this unscoped for ref struct types would be a significant breaking change.
It would allow instance members of a mutable ref struct to ref re-assign to fields in the ref struct, requiring additional escape rules:
ref struct Sneaky
{
int Field;
ref int RefField;
void SelfAssign()
{
RefField = ref Field; // illegal with existing rules
}
static Sneaky UseExample()
{
Sneaky local = default;
local.SelfAssign();
return local; // local.RefField escapes to caller
}
}
The second case, ref parameters of ref struct types, is similar to the previous case.
If not scoped, it would allow a mutable ref struct to ref re-assign to fields in the ref struct, adding escape rules, and potentially making APIs that include ref struct receivers or parameters as difficult to use.
static void DoSomething(ref Sneaky s) { ... }
static Sneaky UseExample()
{
Sneaky s = default; // safe-to-escape to caller
DoSomething(ref s); // compiler must assume self-assignment
return s; // error
}
The third case, out parameters, is to reduce the impact of considering ref parameters as returnable in ref fields.
In C#10, the scope of ref arguments including out arguments were considered in ref returning methods, but were not considered returnable in ref struct values.
In C#11, we need to consider that ref arguments may be returned in ref struct values.
Implicitly scoping out reduces the impact of that breaking change.
It also means out is intuitively treated as out only.
static ref int CreateRef(ref int x, out int y) { ... }
static Span<int> CreateSpan(ref int x, out int y) { ... }
static ref int F1(ref int x)
{
int y;
return ref CreateRef(ref x, out y); // C#10: cannot return 'y'; C#11: ok
}
static Span<int> F2(ref int x)
{
int y;
return CreateSpan(ref x, out y); // C#10: ok; C#11: ok
}
static Span<int> F3()
{
int x = 42;
int y;
return CreateSpan(ref x, out y); // C#10: ok; C#11: cannot return 'x'
}
See implicitly scoped, Unscoped this by default?
4. scoped is emitted as ScopedRefAttribute
Should the attribute be emitted for implicitly scoped parameters?
- For
outparameters that were considered unscoped in C#10? - Only for methods where a reference could escape (method has a
reforref structparameter and returns areforref struct)?
Should the attribute definition be added to the BCL or synthesized by the compiler?
5. UnscopedRefAttribute replaces proposed unscoped keyword
- Can be applied to any
refthat is implicitlyscoped - Can be applied to implicit
thisinstructinstance methods and properties - Cannot be applied to
initmembers or constructors, to avoid allowingreftoreadonlymembers - Member annotated with
[UnscopedRef]cannot implement interface - Considered in overrides, interface implementations, delegate conversions
See Provide unscoped
Relates to test plan https://github.com/dotnet/roslyn/issues/59194
Partially discussed in LDM: https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-03.md#ref-fields-specification-updates
Hey @cston, I saw this in the specs:
Member annotated with
[UnscopedRef]cannot implement interface
Does that mean that the following will not compile (while it currently compiles with latest 7 rc1)?
interface ITester {
ref int Test { get; }
}
struct Tester : ITester
{
private int _test;
[UnscopedRef]
public ref int Test => ref _test;
}
If that's the case, that's really unfortunate while interface with generics+structs are great for writing abstraction with high performance layout/code.
Is it because it is similar to the restrictions with default interface methods? (of the boxing that can happen to this in the default method, but I'm failing to see how that would be troublesome in that case)
Does that mean that the following will not compile (while it currently compiles with latest 7 rc1)?
Correct that should not compile. Will have a bug to track that shortly.
If that's the case, that's really unfortunate while interface with generics+structs are great for writing abstraction with high performance layout/code.
It's unfortunate but it's also extremely unsafe to allow it to do so. Consider it would allow the following to compile:
ref int M<T>(T value) where T : struct, ITester => ref value.Test;
The existing design can be extended to support this by requiring that the interface member have [UnscopedRef] as well. That wasn't done because feature was already big enough and we lacked the motivating scenarios to jsutify it. It can be considered for a future release though.
The existing design can be extended to support this by requiring that the
interfacemember have[UnscopedRef]as well. That wasn't done because feature was already big enough and we lacked the motivating scenarios to jsutify it. It can be considered for a future release though.
Thanks, understood, I was indeed assuming that we could put the attribute on the interface member as well to make such case possible. Glad to hear that it should be possible to bring that support. 🤞
This is a very exciting set of features, thank you to the team for the parts of this that have been implemented, and I'm looking forward to the future features in this area.
Massive thanks to whoever allowed me to do this with C# 11!
/// <summary>
/// Allows ignoring <see langword="scoped" /> and <see langword="in" />, use with care.
/// </summary>
public static ref T AsRef<T>(scoped in T val)
{
#if NET7_0_OR_GREATER
return ref Unsafe.AsRef(in val);
#else
unsafe
{
#pragma warning disable CS9088 // This returns a parameter by reference but it is scoped to the current method
return ref Unsafe.AsRef(in val);
#pragma warning restore CS9088 // This returns a parameter by reference but it is scoped to the current method
}
#endif
}
I do have to say I'm disappointed by the lack of unscoped keyword though.
@hamarb123 You can get unscoped behavior with [UnscopedRef] (iirc). I believe the reasoning for making that an attribute rather than a keyword is because it's much rarer needed.
Thanks, I know I can use [UnscopedRef], but it's sad that there isn't a keyword, because I have to manually make the attribute myself on older tfms (unlike scoped which gets autogenerated), and if we support things like scoped ref scoped Span<T> in the future or scoped ref scoped ref T, will we need more complex unscoped logic that will not be easily expressible with an user written attribute, or will unscoped just be the default for everything new (that would work too)? It would also good since it would be the opposite of the scoped keyword. But I accept that it's probably somewhat uncommon.