22

Here's an example of an instance method of struct attempting to return a readonly ref to an instance field of the struct:

struct Foo
{
    internal int _x;

    public ref readonly int MemberGetX() => ref _x;
    //                                          ^^^
    // Error CS8170: Struct members cannot return 'this' or other instance members by reference
}

This produces error CS8170 Struct members cannot return 'this' or other instance members by reference. However, doing the same thing using an extension method produces no error:

static class FooExtensions
{
    public static ref readonly int ExtensionGetX( this in Foo foo )
    {
        return ref foo._x;
    }
}

Answers to the related question Why can't a C# structure return a reference to its member field? discuss the reasons why the language does not permit the first scenario, but given those reasons, it's not clear to me why the second scenario is allowed.


Update:

Here's a full example that does not use readonly, and also shows a non-extension method, and that demonstrates usage:

struct Foo
{
    internal int _x;

    // Can't compile, error CS8170
    // public ref int GetXRefMember() => ref _x;

    public int X => _x;
}

static class FooExtensions
{
    public static ref int GetXRefExtension( this ref Foo foo )
    {
        return ref foo._x;
    }

    public static ref int GetXRef( ref Foo foo )
    {
        return ref foo._x;
    }
}

class Program
{
    static void Main( string[] args )
    {
        var f = new Foo();
        Console.WriteLine( f.X );
        f.GetXRefExtension() = 123;
        Console.WriteLine( f.X );

        // You can also do it without using an extension method, but the caller is required to specify ref:
        FooExtensions.GetXRef( ref f ) = 999;
        Console.WriteLine( f.X );

        /* Output:
         * 0
         * 123
         * 999
         */
    }
}

It's interesting that extension methods silently "add" ref, when normal calls require the caller to explicitly add ref to the argument, I presume to make it clear and prevent mistakes.

tg73
  • 415
  • 3
  • 10
  • 1
    Your `struct` itself isn't `readonly`. [you should never pass a non-readonly struct as in parameter](https://blogs.msdn.microsoft.com/seteplia/2018/03/07/the-in-modifier-and-the-readonly-structs-in-c/) (towards bottom of that article, but read the whole thing if it's not immediately clear) - "... once the parameter is **used**, the defensive copy will nullify the benefits or will make the performance worse..." (My **emphasis**) – Damien_The_Unbeliever May 23 '18 at 13:54
  • 2
    what's even more fun... can you even call `ExtensionGetX()`? I'm getting a lot of `CS8329` – Marc Gravell May 23 '18 at 14:05
  • @MarcGravell [SharpLab](https://sharplab.io/#v2:EYLgtghgzgLgpgJwD4AEAMACFBGA3AWAChYEBXAYxgwDEB7WogbyI1awGYMBLAOyoH0AHgUIBfIkRwA2LACYa9AKKD4PKF1pqmLNik7SMCOADNDcCABNNAGwCe3PhmWr1mgOJwYADQAUGGAAWXFAOCrQYxvQYAJQ6rMyEbElYAOxmppG0AHRCIkniYpL68gDCGAlJelgALBgAsj7R5XHJRqZGljb2AG4QCBi91hgAvOkYPHAA7mGNWc5waho8Ht6NeWwFokA) – xanatos May 23 '18 at 14:09
  • Note that it doesn't *have* to be an extension method - you can remove the `this` to turn it into a normal method, and it will still compile. I think the extension method part of this question is irrelevant. – Matthew Watson May 23 '18 at 14:13
  • "Documentation" (https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.2/readonly-ref.md) states that "instance struct fields are safe to return as long as the receiver is safe to return". Here receiver (`foo` parameter) is safe to return (because `in`), so seems works as intended. While first case is covered by "'this' is not safe to return from struct members". – Evk May 23 '18 at 14:19
  • @MatthewWatson - that's true of any extension method, that it's also callable without using the extension functionality and if you're going to always do that, you can omit making it an extension method. – Damien_The_Unbeliever May 23 '18 at 14:20
  • @Damien_The_Unbeliever Exactly - so it would be better to omit any mention of extension methods from the question (since it just adds complexity to it for no advantage). – Matthew Watson May 23 '18 at 14:24
  • 2
    @MatthewWatson @Servy Indeed, the example works without `readonly` or `this`, but the example is intended to highlight two scenarios that have the same semantic intent, but the compiler allows one but not the other. To reverse the question, given that the compiler allows the extension method form, why does it not allow the member form? Is this an actual inconsistency in the language, or is there some significant difference about the context in which the two forms can be invoked that explains the behaviour? – tg73 May 23 '18 at 14:28
  • @tg73 The point is to get an example down to a minimal complete example, removing anything not needed to get to that core question. Don't force every reader of the question to have to sift through what parts of the example are relevant to the question and what isn't. – Servy May 23 '18 at 14:48
  • @Servy I agree. I've updated the question. However, on reflection, there may be some relevance to the behaviour that extension methods automagically add `ref` to the `this` argument at the callsite, where non-extension methods require explicit use of `ref` at the callsite. – tg73 May 23 '18 at 14:52

2 Answers2

6

I think this is covered in ref-readonly proposal, Safe To Return Rules section.

'this' is not safe to return from struct members

This one you already know. You can read more about why it's not allowed here. In short - allowing it "contaminates any ref-returning method called on a local value type", so making all ref returns from methods on structs not "safe to return", because they might potentially contain ref to this.

ref/in parameters are safe to return

instance struct fields are safe to return as long as the receiver is safe to return

This covers static method case. foo parameter is safe to return (because in), and therefore, foo._x is safe to return, being field of a struct instance which itself is safe to return.

a ref, returned from another method is safe to return if all refs/outs passed to that method as formal parameters were safe to return.

This one prevents problems with the above static method. It makes the following invalid:

public static ref readonly int ExtensionGetX(in Foo foo) {
    return ref foo._x;
}

static ref readonly int Test() {
    var s = new Foo();
    s._x = 2;
    // fails to compile
    return ref ExtensionGetX(s);
}

Because s is not safe to return, ref we got from ExtensionGetX is not safe to return either, so we cannot leak pointer to local variable outside of scope.

So in short - it's allowed because it's safe, and does not have a specific drawback which forbid returning ref to "this" from struct member method.

Update. I don't think update to your question changes my answer. "safe to return" rules mentioned above stay the same. You changed in to ref, but ref parameter is also safe to return. So is its instance field. But if you make parameter not safe to return:

public static ref int GetXRef(Foo foo)
{
    return ref foo._x;
}

Then it won't compile.

You also think (in comment) that "you can't store the returned reference as a local variable", but that's not true:

ref int y = ref FooExtensions.GetXRef(ref f);
y = 10;
// print 10
Console.WriteLine(f.X);

So, readonly or not, in or ref - safe to return rules ensure it's safe to return ref struct member in that situation, while allowing to return reference to this from struct local method has undesirable consequences, forcing to treat all ref values returned by all struct members as not safe to return.

Small example of what would not be possible if struct member can return ref to this:

public ref int GetXRefMember(ref int y) => ref y;

static ref int Test(ref int y) {
    var x = new Foo();
    // safe to return, but won't be if ref to `this` can
    // ever be returned from struct local method
    return ref x.GetXRefMember(ref y);
}
Evk
  • 98,527
  • 8
  • 141
  • 191
  • I've updated the question to include an example that does not use `in` or `readonly`, this might affect your answer. I think you have some valid points, but I *think* it boils down to that you can't store the returned reference as a local variable (`var r = foo.GetXRef()` will make `r` a local `int` copy, not an `int&`). But I don't think this addresses the headline question. – tg73 May 23 '18 at 14:58
  • @tg73 I've updated answer with my thoughts about this – Evk May 23 '18 at 15:30
1

Another answer already explains why this compiler error is needed, and how it prevents you from accidentally keeping a dangling reference.

As a quick and dirty workaround, you can suppress this error by using unsafe fixed construct:

struct Foo
{
    internal int _x;

    public unsafe ref readonly int MemberGetX()
    {
        fixed (int* ptr = &_x)
            return ref *ptr;
    }
}

But now it is your responsibility to make sure that references to fields of local or temporary instances of Foo do not escape their scope. The compiler is no longer looking after you, and you are on your own.

But this approach only works for fields of an unmanaged type (i.e. primitive types and structs constructed entirely from them).

Vladimir Reshetnikov
  • 11,750
  • 4
  • 30
  • 51