ref и out
В прошлом уроке аппарат работал с копией ваших листов. Иногда нужно другое: чтобы аппарат поработал прямо с вашим листом и всё, что он сделал, на нём осталось. Тогда вы отдаёте ему сам оригинал.
Классический пример — метод, который меняет местами значения двух переменных. С копиями он бесполезен: поменяет местами копии, а переменные останутся прежними. Ключевое слово ref передаёт в метод саму переменную:
int a = 1;
int b = 2;
Swap(ref a, ref b);
Console.WriteLine(a + " " + b); // 2 1
void Swap(ref int first, ref int second)
{
int temp = first;
first = second;
second = temp;
}Ключевое слово ref
Давайте разберём пример:
- В объявлении
ref int firstзначит: параметрfirst— не копия, а другое имя той переменной, которую передадут. - В вызове
Swap(ref a, ref b)словоrefстоит ещё раз, и по вызову видно, что метод может изменитьaиb. - Внутри метода
first— это самаa, аsecond— самаb. Три присваивания черезtempменяют их значения местами. - После вызова
Console.WriteLineпечатает2 1.
Такой способ называют передачей по ссылке: метод получает доступ к самой переменной вызывающего кода.
У ref три правила:
- его пишут и в объявлении, и в вызове;
- аргументом может быть только переменная:
Swap(ref a, ref 2)не скомпилируется; - у переменной должно быть значение до вызова, ведь метод может его прочитать.
Здесь аналогия с оригиналом неточна. Отдав оригинал, вы остаётесь без него, пока аппарат его не вернёт. Переменная же никуда не уходит: first — второе имя той же переменной a.
Ключевое слово out
out тоже передаёт саму переменную, но работает в одну сторону: из метода наружу. Через него метод возвращает ещё одно значение, кроме обычного результата:
int quotient = Divide(17, 5, out int rest);
Console.WriteLine(quotient + " " + rest); // 3 2
int Divide(int a, int b, out int remainder)
{
remainder = a % b;
return a / b;
}Разберём, что здесь происходит:
out int restв вызове объявляет новую переменнуюrestпрямо на месте. Заводить её заранее и давать ей значение не нужно.- Метод обязан присвоить параметру
remainderзначение до того, как завершится. Без этого присваивания программа не скомпилируется. Divide(17, 5, out int rest)возвращает частное3, а остаток2попадает вrest.
Метод int.TryParse
Чаще всего out встречается в стандартном методе int.TryParse. Он пытается превратить строку в число. int.Parse на строке "forty" остановил бы программу с исключением FormatException, а int.TryParse сообщает о неудаче иначе:
- возвращает
trueилиfalse— получилось преобразование или нет; - само число отдаёт через
out-параметр, а при неудаче кладёт туда0.
Check("42"); // 43
Check("forty"); // Not a number
void Check(string text)
{
if (int.TryParse(text, out int number))
{
Console.WriteLine(number + 1);
}
else
{
Console.WriteLine("Not a number");
}
}Check("42") получает от int.TryParse значение true и число 42 в number, поэтому печатает 43. Для "forty" преобразование не удаётся, int.TryParse возвращает false, и выполняется ветка else. Проверка и преобразование в одном условии if (int.TryParse(text, out int number)) — обычный приём в коде на C#.
ref или out
На собеседовании часто спрашивают, чем ref отличается от out. Ответ из трёх частей:
- переменная для
refдолжна получить значение до вызова, а дляout— нет; - метод может не менять
ref-параметр, аout-параметру обязан присвоить значение; - оба передают саму переменную, а не копию.
Частая ошибка
В вызове забыли ref:
int a = 1;
int b = 2;
Swap(a, b); // ✗error CS1620: Argument 1 must be passed with the 'ref' keywordПрограмма не компилируется. Компилятор требует ref и в вызове, чтобы по нему было видно: метод может изменить a. Иначе читатель кода узнал бы об этом, только открыв сам метод. Правильный вызов — Swap(ref a, ref b);.
Главное
refпередаёт в метод саму переменную, и изменения внутри метода видны снаружи; его пишут и в объявлении, и в вызове.- Переменная для
refдолжна получить значение до вызова, аout-параметру метод обязан присвоить значение до завершения. int.TryParse(text, out int number)возвращаетtrueилиfalse, а число отдаёт черезout.