Рассмотрим следующий пример:
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
в этом контексте? Зачем это нужно, почему его можно опустить?
Возврат типа 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
- параметр - ссылаться на совершенно другую переменную, чем раньше. В основном это ничего не сделает, как написано. Исходная переменная componentData
ref
ed не будет затронута, так как componentData
больше не указывает на нее.
(b) (без ref
в присваивании) приведет к тому, что, вероятно, является ожидаемым результатом, а именно к тому, что переменная ref
ed 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>
, это невозможно - лучшее, что вы можете сделать, это присвоить переменной ref
ed componentData
копию struct
.
В целом, использование интерфейсов с struct
поможет вам обойти все это. Приведение экземпляра структуры к его типу интерфейса эквивалентно получению адреса, а приведение ссылки интерфейса к его типу структуры эквивалентно разыменованию указателя.
Это здорово, не могли бы вы также объяснить разницу между ними, когда переменная, которой присваивается значение ref/normal, сама является параметром ref?
«Нормальный возврат просто даст вам копию строки». - нет, не будет.
@GuruStron, конечно, будет. Что вы получаете в?
string
— это ссылочный тип, и создание копии строки произвольной длины при каждом возврате было бы кошмаром для производительности. Вы можете легко доказать, что возвращенный экземпляр строки будет таким же, либо с помощью отражения, либо с помощью небезопасной модификации строки.
@GuruStron хорошо, это явно не то, что я имел в виду. Конечно, нормальный возврат дает вам копию ссылки на эту конкретную строку, но не позволяет изменить исходную переменную.
Хотя это может быть очевидно для вас и меня, это может быть не так ясно для всех. Также еще раз - вы можете изменить «исходную переменную» через небезопасный API, и в случае «обычного» (неиммитируемого) типа ссылки разница будет немного более тонкой.
«Я полагаю, что это больше используется в более функциональных стилях программирования, чем в ООП» - это не имеет ничего общего с функциональным программированием, даже больше - я бы сказал, что это противоречит принципам FP, которые отдают предпочтение неизменяемым структурам данных и избегают мутаций. По моему опыту, обычно ref
используются из соображений производительности.
Давайте продолжим обсуждение в чате.
пожалуйста, посмотрите мой обновленный ответ, который касается вашего вопроса о разнице между
componentData = ref GetComponent<T>()
иcomponentData = GetComponent<T>();
.