Что делает ключевое слово ref в случае присвоения возвращаемого типа ref параметру ref

Рассмотрим следующий пример:

public ref T GetComponent<T>() where T : struct, IComponentData
{
    return ref componentAnymapReference.GetComponent<T>(Entity).ComponentData;
}

public bool TryGetComponent<T>(ref T componentData) where T : struct, IComponentData
{
    if (!Entity.HasComponent<T>())
        return false;

    componentData = ref GetComponent<T>();
    return true;
}

Строка componentData = ref GetComponent<T>(); также компилируется как componentData = GetComponent<T>(); без ошибок и предупреждений.

Что делает ref в этом контексте? Зачем это нужно, почему его можно опустить?

пожалуйста, посмотрите мой обновленный ответ, который касается вашего вопроса о разнице между componentData = ref GetComponent<T>() и componentData = GetComponent<T>();.

Emperor Eto 02.04.2023 21:48
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
1
94
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Рассмотрим этот пример:

class Person
{
    string _name;
    public string Name
    {
        get => _name;
        init => _name = value;
    }

    public ref string GetNameRef()
    {
        return ref _name;
    }

    public static void Example()
    {
        var person = new Person { Name = "Joe" };

        string normalReturn = person.Name;
        normalReturn = "New Name";
        Console.WriteLine($"Person Name = {person.Name}");  // expecting "Joe"

        ref string refReturn = ref person.GetNameRef();
        refReturn = "New Name";
        Console.WriteLine($"Person Name = {person.Name}");  // expecting "New Name"
    }
}

Изменение refReturn позволяет изменить частное значение Person._name. Обычный возврат просто вернет экземпляр той же строки, что и _name, но не даст вам возможности изменить _name в противном случае.

Варианты использования для этого, по общему признанию, ограничены, если не сомнительны. Мой пример хоть и показателен, но, конечно, нереалистичен. Я предполагаю, что это больше используется в более функциональных стилях программирования, а не в ООП, где в основном ожидается, что объекты будут управлять своим собственным состоянием.

Одна ситуация, которая, как мне кажется, может быть полезна, — это если метод принимает несколько параметров ref в качестве входных данных и возвращает один из них в зависимости от некоторого условия. Например:

    public static ref int MaxRef (ref int a, ref int b)
    {
        if (a >= b)
            return ref a;
        else 
            return ref b;
    }

    public static void Example2()
    {
        int a = 10;
        int b = 20;

        ref int max = ref MaxRef(ref a, ref b);
        max += 10;  
        Console.WriteLine($"b = {b}"); // expect 30
    }

Изучение конкретного вопроса ОП о разнице между

 componentData = ref GetComponent<T>(); // (a)

и

 componentData = GetComponent<T>(); // (b)

приводит к любопытному результату.

(a) заставляет componentData - параметр - ссылаться на совершенно другую переменную, чем раньше. В основном это ничего не сделает, как написано. Исходная переменная componentDatarefed не будет затронута, так как componentData больше не указывает на нее.

(b) (без ref в присваивании) приведет к тому, что, вероятно, является ожидаемым результатом, а именно к тому, что переменная refed by componentData теперь будет иметь значение, возвращаемое GetComponent<T>.

Хотя это сбивает с толку, потому что ref используется в C# как в качестве оператора косвенного обращения, так и в качестве оператора адреса, в зависимости от контекста имеет смысл написать тот же код на квази-C++:

// public ref T GetComponent<T>() 
public T* GetComponent<T>() 
{
    // return ref componentAnymapReference.GetComponent<T>.ComponentData;
    return &(componentAnymapReference.GetComponent<T>.ComponentData);
}

// public bool TryGetComponent<T>(ref T componentData) where T : struct, IComponentData
public bool TryGetComponent<T>(T* componentData) 
{    
    // componentData = ref GetComponent<T>(); 
    componentData = GetComponent<T>();  // changes the local value of componentData, which basically does nothing

    // --or--

    // componentData = GetComponent<T>();
    *componentData = *GetComponent<T>();  // more likely what you want
    return true;
}

И для примера, который ясно иллюстрирует это, основываясь на моем предыдущем примере:

    public static void Example2()
    {
        int a = 10;
        int b = 20;
        int max = 0, max2 = 0;

        GetMax1(ref a, ref b, ref max);
        Debug.WriteLine($"max = {max}");   // max is chagned by GetMax1 (without the ref)

        GetMax2(ref a, ref b, ref max2);
        Debug.WriteLine($"max2 = {max2}");   // max2 isn't changed by GetMax2 (with the ref)
    }

    private static void GetMax1(ref int a, ref int b, ref int max)
    {
        max = MaxRef(ref a, ref b);
    }

    private static void GetMax2(ref int a, ref int b, ref int max)
    {
        max = ref MaxRef(ref a, ref b);
    }

Обратите внимание, что невозможно заставить max2 в конечном итоге стать ref из b, передав его в качестве параметра GetMax2. Это потребует, чтобы GetMax2 принимал параметр ref ref int max, что не разрешено (хотя это и есть в C++). Итак, в вашем примере, если вы надеялись получить ref базовой переменной, возвращаемой GetComponent<T>, это невозможно - лучшее, что вы можете сделать, это присвоить переменной refed componentData копию struct.

В целом, использование интерфейсов с struct поможет вам обойти все это. Приведение экземпляра структуры к его типу интерфейса эквивалентно получению адреса, а приведение ссылки интерфейса к его типу структуры эквивалентно разыменованию указателя.

Это здорово, не могли бы вы также объяснить разницу между ними, когда переменная, которой присваивается значение ref/normal, сама является параметром ref?

Jay 02.04.2023 20:50

«Нормальный возврат просто даст вам копию строки». - нет, не будет.

Guru Stron 02.04.2023 20:59

@GuruStron, конечно, будет. Что вы получаете в?

Emperor Eto 02.04.2023 21:02
string — это ссылочный тип, и создание копии строки произвольной длины при каждом возврате было бы кошмаром для производительности. Вы можете легко доказать, что возвращенный экземпляр строки будет таким же, либо с помощью отражения, либо с помощью небезопасной модификации строки.
Guru Stron 02.04.2023 21:04

@GuruStron хорошо, это явно не то, что я имел в виду. Конечно, нормальный возврат дает вам копию ссылки на эту конкретную строку, но не позволяет изменить исходную переменную.

Emperor Eto 02.04.2023 21:08

Хотя это может быть очевидно для вас и меня, это может быть не так ясно для всех. Также еще раз - вы можете изменить «исходную переменную» через небезопасный API, и в случае «обычного» (неиммитируемого) типа ссылки разница будет немного более тонкой.

Guru Stron 02.04.2023 21:14

«Я полагаю, что это больше используется в более функциональных стилях программирования, чем в ООП» - это не имеет ничего общего с функциональным программированием, даже больше - я бы сказал, что это противоречит принципам FP, которые отдают предпочтение неизменяемым структурам данных и избегают мутаций. По моему опыту, обычно ref используются из соображений производительности.

Guru Stron 02.04.2023 22:17

Давайте продолжим обсуждение в чате.

Emperor Eto 02.04.2023 22:21

Другие вопросы по теме