Перейти к содержимому
Теория · Методы · урок 5 из 8

ref и out

В прошлом уроке аппарат работал с копией ваших листов. Иногда нужно другое: чтобы аппарат поработал прямо с вашим листом и всё, что он сделал, на нём осталось. Тогда вы отдаёте ему сам оригинал.

Классический пример — метод, который меняет местами значения двух переменных. С копиями он бесполезен: поменяет местами копии, а переменные останутся прежними. Ключевое слово ref передаёт в метод саму переменную:

C#
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

Давайте разберём пример:

  1. В объявлении ref int first значит: параметр first — не копия, а другое имя той переменной, которую передадут.
  2. В вызове Swap(ref a, ref b) слово ref стоит ещё раз, и по вызову видно, что метод может изменить a и b.
  3. Внутри метода first — это сама a, а second — сама b. Три присваивания через temp меняют их значения местами.
  4. После вызова Console.WriteLine печатает 2 1.

Такой способ называют передачей по ссылке: метод получает доступ к самой переменной вызывающего кода.

У ref три правила:

  • его пишут и в объявлении, и в вызове;
  • аргументом может быть только переменная: Swap(ref a, ref 2) не скомпилируется;
  • у переменной должно быть значение до вызова, ведь метод может его прочитать.

Здесь аналогия с оригиналом неточна. Отдав оригинал, вы остаётесь без него, пока аппарат его не вернёт. Переменная же никуда не уходит: first — второе имя той же переменной a.

Ключевое слово out

out тоже передаёт саму переменную, но работает в одну сторону: из метода наружу. Через него метод возвращает ещё одно значение, кроме обычного результата:

C#
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;
}

Разберём, что здесь происходит:

  1. out int rest в вызове объявляет новую переменную rest прямо на месте. Заводить её заранее и давать ей значение не нужно.
  2. Метод обязан присвоить параметру remainder значение до того, как завершится. Без этого присваивания программа не скомпилируется.
  3. Divide(17, 5, out int rest) возвращает частное 3, а остаток 2 попадает в rest.

Метод int.TryParse

Чаще всего out встречается в стандартном методе int.TryParse. Он пытается превратить строку в число. int.Parse на строке "forty" остановил бы программу с исключением FormatException, а int.TryParse сообщает о неудаче иначе:

  • возвращает true или false — получилось преобразование или нет;
  • само число отдаёт через out-параметр, а при неудаче кладёт туда 0.
C#
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:

C#
int a = 1;
int b = 2;
Swap(a, b); // ✗
TEXT
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.