Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't a C# struct method return a reference to a field, but a non-member method can?

Tags:

c#

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.

like image 334
tg73 Avatar asked May 23 '18 13:05

tg73


2 Answers

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);
}
like image 75
Evk Avatar answered Nov 04 '22 15:11

Evk


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

like image 27
Vladimir Reshetnikov Avatar answered Nov 04 '22 16:11

Vladimir Reshetnikov