Перейти к основному содержимому

Передача параметров в C# — числа, объекты, ref, out, in

Смежные статьи

Передача параметров в C# — числа, объекты, ref, out, in

Разработчику

Дальше


Термины

Перед примерами полезно зафиксировать слова, которые встретятся дальше.

Аргумент и параметр

  • Параметр — имя в объявлении метода (void M(int x)x — параметр).
  • Аргумент — конкретное значение при вызове (M(5)5 — аргумент).

Значимый тип (value type)

  • В переменной лежит само значение (число, символ, байты структуры).
  • Примеры: int, bool, char, decimal, struct, enum.
  • При присваивании и при обычном вызове метода значение копируется целиком.

Ссылочный тип (reference type)

  • В переменной лежит адрес объекта в куче (управляемой области памяти CLR).
  • Сам объект хранится отдельно; переменная указывает на него.
  • Примеры: class, string, массив, List<T>, delegate.

Куча и стек

  • Стек — область для локальных переменных и параметров метода; доступ быстрый, lifetime привязан к вызову.
  • Куча — область для объектов ссылочных типов; память освобождает сборщик мусора (GC).

Инициализация

  • Переменной присвоено начальное значение до первого чтения. Подробнее — Переменные.

Модификатор параметра

  • Ключевое слово перед типом (ref, out, in), которое меняет способ передачи аргумента. Синтаксис вызова — в статье про переменные.

Как устроена передача по умолчанию

При вызове SomeMethod(argument) компилятор копирует аргумент в параметр метода. Содержимое копии зависит от категории типа.

КатегорияПримеры типовЧто оказывается в параметре
Значимый типint, bool, struct, enumКопия числа или всей структуры
Ссылочный типclass, string, массив, List<T>Копия адреса (ссылки) на объект в куче

После копирования параметр и исходная переменная живут отдельно. Исключение — обе переменные-ссылки могут указывать на один и тот же объект в куче, поэтому изменение полей через параметр видно снаружи.

Про слово "ссылка"

В C# у ссылочного типа переменная хранит адрес объекта. При обычном вызове метода копируется этот адрес. Модификатор ref передаёт адрес самой переменной в стеке вызывающего кода, чтобы метод мог переписать, что в ней лежит.

Два типичных сценария

  • Вызов void M(int x) { x = 10; } оставляет внешнюю переменную без изменений, потому что в метод попала копия числа.
  • Вызов void M(Person p) { p.Name = "Ivan"; } меняет имя у исходного объекта, потому что параметр и аргумент указывают на один объект в куче.

Схема при вызове метода


Числа и другие значимые типы

Обычный параметр

void ModifyNumber(int num)
{
num = 10;
}

int x = 5;
ModifyNumber(x);
Console.WriteLine(x); // 5

Разбор

  • num — отдельная локальная переменная с копией 5.
  • Строка num = 10 меняет только эту копию.
  • Переменная x в вызывающем коде остаётся 5.

Так же ведут себя bool, char, decimal, enum и любой struct.

Параметр с ref

void ModifyNumberRef(ref int num)
{
num = 10;
}

int x = 5;
ModifyNumberRef(ref x);
Console.WriteLine(x); // 10

Разбор

  • ref связывает параметр с переменной x в вызывающем коде (метод пишет прямо в неё).
  • Ключевое слово ref пишут и в объявлении метода, и в месте вызова.
  • Переменная должна быть инициализирована до вызова.

Структура (struct)

У структуры та же семантика, что у int: без ref в метод уходит полная копия всех полей.

public struct Point
{
public int X;
public int Y;
}

void MoveRight(Point p) => p.X++;

var origin = new Point { X = 0, Y = 0 };
MoveRight(origin);
Console.WriteLine(origin.X); // 0

void MoveRightRef(ref Point p) => p.X++;

MoveRightRef(ref origin);
Console.WriteLine(origin.X); // 1

Для небольших неизменяемых структур обычно возвращают новое значение из метода:

public readonly record struct Point(int X, int Y)
{
public Point MoveRight() => this with { X = X + 1 };
}

Когда выбирать struct или class — в Типах данных. Про копирование больших структур — производительность.


Объекты и другие ссылочные типы

Обычный параметр

class Person
{
public string Name { get; set; } = "";
}

void ModifyObj(Person p)
{
p.Name = "Ivan";
p = new Person { Name = "Anna" };
}

var person = new Person { Name = "Alex" };
ModifyObj(person);
Console.WriteLine(person.Name); // Ivan

Разбор

  • person и p — разные переменные в памяти.
  • До строки с new обе хранят один и тот же адрес объекта в куче.
  • p.Name = "Ivan" меняет поле общего объекта — результат виден через person.
  • p = new Person { ... } записывает новый адрес только в локальный параметр p. Переменная person по-прежнему указывает на первый объект (имя там стало Ivan).

Создание экземпляра new Person() и поля класса — в ООП в C#.

Параметр с ref

Если метод должен переназначить переменную вызывающего кода (другой объект, другой список), нужен ref.

void ReplacePerson(ref Person p)
{
p = new Person { Name = "Anna" };
}

var person = new Person { Name = "Alex" };
ReplacePerson(ref person);
Console.WriteLine(person.Name); // Anna

Разбор

  • ref Person p — ссылка на переменную person в стеке вызывающего кода.
  • Присваивание p = new ... меняет адрес, который хранит person.

Списки и массивы

List<T> и массивы — ссылочные типы. Логика та же, что у Person.

void ClearList(List<int> items) => items.Clear();

void ReplaceList(List<int> items) => items = new List<int>();

var numbers = new List<int> { 1, 2, 3 };
ClearList(numbers);
// numbers пуст — Clear() изменил тот же объект в куче

ReplaceList(numbers);
// numbers по-прежнему пуст — ReplaceList переписал только локальную копию ссылки

Чтобы метод подменил саму переменную-список

  • объявите ref List<int> items, или
  • верните новый список из метода через return.

Строки

string — ссылочный неизменяемый тип. Переприсваивание параметра s внутри метода не затрагивает переменную вызывающего кода.

void TryChange(string s) => s = "changed";

string text = "hello";
TryChange(text);
Console.WriteLine(text); // hello

Любая операция вроде Replace или конкатенации создаёт новую строку в куче. Исходная переменная text остаётся прежней, пока ей явно не присвоить результат.


Модификаторы ref, out, in

Краткая таблица. Полный список ключевых слов — Справочник C#.

МодификаторЧто передаётсяИнициализация до вызоваОбязанности методаГде встречается
(нет)Копия значения или копии адресаЛюбаяРабота с локальной копиейБольшинство методов
refПеременная вызывающего кодаОбязательнаМожно читать и писатьОбновление аргумента на месте
outПеременная вызывающего кодаНе требуетсяПрисвоить значение перед выходомTryParse, TryGetValue
inПеременная только для чтенияЛюбаяЧитать можно, менять нельзяКрупные struct

out — дополнительный результат через параметр

Паттерн Try в стандартной библиотеке .NET (BCL)

  • метод возвращает bool (успех или неудача);
  • полезное значение отдаёт через out-параметр.
bool TryDivide(int a, int b, out double result)
{
if (b == 0)
{
result = 0;
return false;
}

result = (double)a / b;
return true;
}

if (TryDivide(10, 4, out double quotient))
Console.WriteLine(quotient); // 2.5

if (int.TryParse("42", out int value))
Console.WriteLine(value); // 42

Разбор

  • Компилятор требует присвоить out-параметр на каждой ветке метода.
  • В вызове можно объявить переменную сразу: out int value.
  • Примеры в BCL: int.TryParse, Dictionary.TryGetValue, Guid.TryParse.

Вернуть несколько значений можно и через кортежreturn (true, quotient) — без out.

in — передать структуру без полного копирования

Модификатор in (C# 7.2+) передаёт адрес struct, но запрещает менять поля параметра.

public readonly struct Matrix3x3
{
public double M11, M12, M13;
public double M21, M22, M23;
public double M31, M32, M33;
}

double Trace(in Matrix3x3 m)
{
// m.M11 = 0; // ошибка компиляции
return m.M11 + m.M22 + m.M33;
}

Разбор

  • Девять полей double — 72 байта; при обычном параметре структура копируется целиком на каждый вызов.
  • in экономит копирование и фиксирует контракт "только чтение".
  • Для class выигрыш обычно незаметен — копируется только адрес (8 байт на 64-битной платформе).

Как выбрать способ передачи

ЗадачаПодход
Прочитать данные и вернуть результатОбычный параметр + return
Изменить поля существующего объектаОбычный параметр ссылочного типа
Изменить число или struct у вызывающегоref или return нового значения
Сообщить успех и значение (как TryParse)bool + out или кортеж
Принять большую struct без копииin
Подменить ссылку (другой объект, другой список)ref или return
Стиль в новых проектах

Для нескольких результатов часто используют return, кортежи и record. out остаётся стандартом для API в духе TryParse. ref уместен в буферах, Span<T> и горячих участках — см. справочник и производительность.


Частые ошибки

СимптомПричинаРешение
void M(int x) { x++; } не увеличивает аргументВ метод попала копия intref int x или return x + 1
p = new Person() внутри метода не меняет переменную снаружиСкопирован только адрес; new пишет в локальный параметрref Person p или вернуть объект
void M(Point p) { p.X++; } не двигает точку снаружиСкопирована вся структураref Point p, readonly struct + return
Ошибка компиляции на out-параметреНе все ветки if присваивают outПрисвоить во всех ветках или result = default
Лишний ref вездеУсложняется чтение и сопровождение APIref только там, где аргумент должен измениться

Типичные вопросы на собеседовании — 474.


Все варианты в одном примере

class Counter
{
public int Value;
}

void A(int n) => n = 0; // копия int
void B(ref int n) => n = 0; // переменная int снаружи
void C(Counter c) => c.Value = 0; // общий объект в куче
void D(Counter c) => c = new(); // новый адрес только в параметре
void E(ref Counter c) => c = new(); // переменная-ссылка снаружи

int num = 42;
A(num); Console.WriteLine(num); // 42
B(ref num); Console.WriteLine(num); // 0

var ctr = new Counter { Value = 42 };
C(ctr); Console.WriteLine(ctr.Value); // 0
D(ctr); Console.WriteLine(ctr.Value); // 0, тот же объект
E(ref ctr); Console.WriteLine(ctr.Value); // 0, уже другой объект

Шпаргалка

  • Без модификатора — копия значения (int, struct) или копия адреса (class, массив, список).
  • ref — метод работает с исходной переменной; она должна быть инициализирована.
  • out — метод обязан записать значение в параметр; удобно для Try*-методов.
  • in — адрес struct без права изменения; для тяжёлых структур.

Документация Microsoft