По умолчанию аргумент копируется. Что именно копируется – значение или ссылка – зависит от типа, и на этом различии спотыкаются чаще всего. Разберём его до конца, а потом посмотрим на способы изменить поведение.
Значимый тип: копируется значение
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 нужны реже, чем кажется на первый взгляд.