Почему метод С# struct не возвращает ссылку на поле, но метод, не являющийся членом, может?

Здесь приведен пример метода экземпляра struct, пытающегося вернуть refonly ref в поле экземпляра структуры:

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
}

Это приводит к ошибке. Члены CS8170 Struct не могут возвращать "этот" или другие члены экземпляра по ссылке. Тем не менее, выполнение одной и той же функции с использованием метода расширения не вызывает ошибки:

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

Ответы на связанный вопрос Почему структура С# не может ссылаться на поле своего члена? обсудите причины, по которым язык не разрешает первый сценарий, но с учетом этих причин мне непонятно, почему разрешен второй сценарий.


Обновить:

Здесь приведен полный пример, который не использует readonly, а также показывает метод без расширения и демонстрирует использование:

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
         */
    }
}

Интересно, что методы расширения молча добавляют "add" ref, когда нормальные вызовы требуют, чтобы вызывающий ящик явно добавлял ref в аргумент, я предполагаю, что он станет ясным и предотвратит ошибки.

Ответы

Ответ 1

Я думаю, что это описано в разделе ref-readonly, Safe To Return Rules.

"это" небезопасно возвращать из членов структуры

Это ты уже знаешь. Вы можете прочитать больше о том, почему он не имеет права здесь. Короче говоря, это позволяет "загрязнять любой метод ref-return, вызванный локальным типом значения", поэтому включение всех возвратов ref из методов в структурах не "безопасно для возврата", потому что они могут потенциально содержать ref для этого.

ref/in параметры безопасны для возврата

поля структуры экземпляра безопасны для возврата, если приемник безопасен для возврата

Это охватывает случай статического метода. Параметр foo безопасен для возврата (потому что in), и, следовательно, foo._x безопасно возвращать, будучи полем экземпляра структуры, который сам по себе безопасен для возврата.

ref, возвращенный из другого метода, безопасно возвращать, если все refs/outs переданы этому методу, так как формальные параметры были бы безопасны для возврата.

Это предотвращает проблемы с указанным выше статическим методом. Это делает следующие недопустимые:

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);
}

Поскольку s небезопасно возвращаться, ref, полученный нами из ExtensionGetX небезопасно возвращать, поэтому мы не можем течь указатель на локальную переменную за пределами области видимости.

Короче говоря, это разрешено, потому что оно безопасно и не имеет определенного недостатка, который запрещает возвращать ref к "этому" из метода struct member.

Обновить. Я не думаю, что обновление вашего вопроса меняет мой ответ. Правила "безопасного возвращения", упомянутые выше, остаются неизменными. Вы изменили in к ref, но ref параметр также безопасен для возврата. Так что это поле экземпляра. Но если вы возвращаете параметр небезопасным:

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

Тогда он не будет компилироваться.

Вы также думаете (в комментарии), что "вы не можете сохранить возвращаемую ссылку как локальную переменную", но это не так:

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

Таким образом, readonly или нет, in или ref safe для возврата правил гарантируют, что он безопасно будет возвращать член ref struct в этой ситуации, позволяя возвращать ссылку на this из struct local method имеет нежелательные последствия, заставляя обрабатывать все значения ref, возвращаемые всеми как небезопасные для возврата.

Небольшой пример того, что не было бы возможным, если член структуры может вернуть реф в 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);
}