csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

[Proposal]: Recent changes to ref fields specification

Open cston opened this issue 3 years ago • 4 comments
trafficstars

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 struct parameters 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

See ref fields to ref struct

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:

  1. this for struct instance methods
  2. ref parameters that refer to ref struct types
  3. out parameters

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?

  1. For out parameters that were considered unscoped in C#10?
  2. Only for methods where a reference could escape (method has a ref or ref struct parameter and returns a ref or ref struct)?

Should the attribute definition be added to the BCL or synthesized by the compiler?

See ScopedRefAttribute

5. UnscopedRefAttribute replaces proposed unscoped keyword

  • Can be applied to any ref that is implicitly scoped
  • Can be applied to implicit this in struct instance methods and properties
  • Cannot be applied to init members or constructors, to avoid allowing ref to readonly members
  • 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

cston avatar Aug 02 '22 17:08 cston

Partially discussed in LDM: https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-03.md#ref-fields-specification-updates

333fred avatar Aug 03 '22 22:08 333fred

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)

xoofx avatar Oct 05 '22 15:10 xoofx

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.

jaredpar avatar Oct 05 '22 17:10 jaredpar

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.

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. 🤞

xoofx avatar Oct 05 '22 17:10 xoofx

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 avatar Nov 11 '22 02:11 hamarb123

@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.

Joe4evr avatar Nov 11 '22 06:11 Joe4evr

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.

hamarb123 avatar Nov 11 '22 07:11 hamarb123