Программа курса← C#: Базовый курс
1Знакомство с C# и .NET
2Данные и типы
3Ветвления и циклы
4Методы и ошибки
5Коллекции и строки
6Классы и объекты

Передача по значению и по ссылке

По умолчанию аргумент копируется. Что именно копируется – значение или ссылка – зависит от типа, и на этом различии спотыкаются чаще всего. Разберём его до конца, а потом посмотрим на способы изменить поведение.

Значимый тип: копируется значение

static void TryChange(int number)
{
    number = 100;
}

int x = 5;
TryChange(x);

Console.WriteLine(x);    // 5

Метод получил свою копию пятёрки и изменил её. Оригинал остался нетронутым: связи между x и number нет.

Ссылочный тип: копируется ссылка

static void FillFirst(int[] items)
{
    items[0] = 99;
}

int[] numbers = { 1, 2, 3 };
FillFirst(numbers);

Console.WriteLine(numbers[0]);    // 99

Копия тоже была – скопировалась ссылка. Но объект, на который она указывает, один на двоих, поэтому изменение содержимого видно снаружи.

Разница станет очевидной, если метод попробует подменить не содержимое, а сам массив:

static void Replace(int[] items)
{
    items = new int[] { 7, 8, 9 };    // меняем локальную ссылку
}

int[] numbers = { 1, 2, 3 };
Replace(numbers);

Console.WriteLine(numbers[0]);    // 1 – снаружи ничего не изменилось

Отсюда точная формулировка: в C# аргументы всегда передаются по значению. Просто у ссылочного типа значением является ссылка. Фраза «объекты передаются по ссылке» – удобное упрощение, которое перестаёт работать ровно в примере выше.

ref: работать с самой переменной

Слово ref в объявлении и при вызове связывает параметр с переменной снаружи напрямую:

static void Increase(ref int number)
{
    number += 10;
}

int x = 5;
Increase(ref x);

Console.WriteLine(x);    // 15

ref пишется дважды – в объявлении и на месте вызова. Второе обязательно: читатель кода видит, что переменную сейчас изменят, а не просто прочитают.

Переменная обязана быть инициализирована до вызова: метод вправе её прочитать.

Классический пример – обмен значениями:

static void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

out: вернуть второе значение

out похож на ref, но с обратными обязанностями: метод обязан присвоить значение, а вызывающий может передать неинициализированную переменную.

static void Divide(int a, int b, out int quotient, out int remainder)
{
    quotient = a / b;
    remainder = a % b;
}

Divide(17, 5, out int q, out int r);

Console.WriteLine($"{q} и {r}");    // 3 и 2

Переменную разрешено объявлять прямо в аргументе – out int q. Именно так устроен знакомый вам int.TryParse:

if (int.TryParse(input, out int number))
{
    Console.WriteLine(number * 2);
}

Метод возвращает bool – удалось или нет, – а само число отдаёт через out. Одним значением тут не обойтись: нужно и то, и другое.

Если out-параметров становится больше одного, обычно это знак, что метод должен возвращать кортеж или объект. out хорош для пары «получилось ли» плюс «результат», а не как способ вернуть пять значений сразу.

Кортеж вместо out

Часто вместо out удобнее вернуть несколько значений сразу:

static (int Quotient, int Remainder) Divide(int a, int b)
{
    return (a / b, a % b);
}

var result = Divide(17, 5);
Console.WriteLine($"{result.Quotient} и {result.Remainder}");

Результат раскладывается по переменным одной строкой:

var (quotient, remainder) = Divide(17, 5);

Читается лучше, чем out, и не требует объявлять переменные заранее. Именно так пишут в новом коде; out остаётся там, где нужен именно шаблон TryXxx.

in: только чтение

in передаёт значимый тип по ссылке, но запрещает менять его внутри метода:

static double Distance(in Point a, in Point b) { ... }

Нужен он редко – только когда структура большая и копировать её накладно. Для int и double выигрыша нет: ссылка занимает не меньше самого числа.

Итог одной таблицей

Как объявлен Копируется Метод может подменить переменную снаружи Инициализировать до вызова
int x значение нет да
int[] x ссылка нет, но содержимое менять может да
ref int x ничего, работа с оригиналом да да
out int x ничего, работа с оригиналом обязан присвоить нет
in int x ничего, работа с оригиналом нет, только чтение да

Метод, который меняет свои аргументы, тяжелее читать: результат вызова перестаёт быть виден на месте вызова. По возможности возвращайте новое значение вместо изменения переданного – ref и out нужны реже, чем кажется на первый взгляд.