Перейти к содержимому
Теория · Массивы · урок 4 из 9

Массив — это ссылка

В теме о переменных присваивание копировало значение: после b = a и a = 7 в b оставалось старое число. С массивами то же присваивание ведёт себя иначе:

C#
int[] a = [1, 2, 3];
int[] b = a;
b[0] = 10;
Console.WriteLine(a[0]); // 10
Console.WriteLine(b[0]); // 10

Давайте разберём по строкам:

  1. int[] a = [1, 2, 3]; создаёт массив и даёт ему имя a.
  2. int[] b = a; второго массива не создаёт. Теперь у того же массива два имени: a и b.
  3. b[0] = 10; меняет первый элемент этого единственного массива.
  4. Поэтому a[0] тоже равен 10: через a и через b видно одно и то же.

Вернёмся к шкафчикам. Ряд шкафчиков стоит в раздевалке, а у вас в руке ключ от этого ряда. Переменная массива — это ключ, а не сами шкафчики. Строка int[] b = a; делает дубликат ключа: ключей стало два, а ряд по-прежнему один. Откройте шкафчик первым ключом и положите туда вещь: вторым ключом вы найдёте её на том же месте.

Переменная хранит ссылку

Пока программа работает, все её значения, от чисел до массивов, хранятся в памяти компьютера: это и есть раздевалка, где стоят шкафчики. Сам массив лежит в памяти отдельно от переменной. В переменной хранится ссылка — значение, по которому программа находит массив в памяти. Это и есть ключ от ряда шкафчиков.

Типы, переменные которых хранят ссылку, называют ссылочными, и массивы — ссылочный тип. А int, double и bool — значимые типы: их переменная хранит само значение.

Присваивание копирует то, что лежит в переменной. Для int это само число, для массива — ссылка. Поэтому b = a копирует ключ, а не шкафчики.

В виджете у переменной массива вместо значения стоит стрелка: → [1, 2, 3] значит «ведёт к массиву с такими элементами». Нажимайте «Следующий шаг» и читайте подпись: она говорит, общий ли массив у имён.

ВживуюДва имени — один массившаг 1 из 10 · рамок: 1

Создаётся массив, и в a кладётся ссылка на него.

Подпись к шагу говорит, общий ли массив у имён

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

Ключ можно поменять

Здесь картинка с ключами неточна. Настоящий ключ подходит к одному замку навсегда. А переменной-ссылке можно присвоить ссылку на другой массив, и дальше она ведёт уже к нему. Длина нового массива может быть любой: тип int[] длину не задаёт, она есть только у самого массива.

После b = [7, 8, 9] у b ключ от нового ряда, а ключ a по-прежнему открывает старый. Старый массив от этого не меняется: присваивание переменной не трогает элементы.

Массив в параметре метода

Параметр метода получает копию аргумента, поэтому метод не может изменить переданную переменную. Для массива правило то же, только копируется ссылка:

C#
int[] prices = [100, 250, 80];
Discount(prices);
Console.WriteLine(prices[1]); // 225
Replace(prices);
Console.WriteLine(prices[1]); // 225

void Discount(int[] nums)
{
    nums[1] = nums[1] - 25;
}

void Replace(int[] nums)
{
    nums = [0, 0, 0];
}

Разберём оба вызова:

  1. Discount(prices) копирует в параметр nums ссылку из prices: у метода свой ключ от того же массива.
  2. nums[1] = nums[1] - 25 меняет элемент общего массива, поэтому после вызова prices[1] равен 225.
  3. Replace(prices) тоже получает копию ссылки, но кладёт в nums ссылку на новый массив. Меняется только копия ключа.
  4. prices по-прежнему ведёт к старому массиву, и печатается снова 225.

Этот вопрос часто задают на собеседовании: изменит ли метод массив, который ему передали? Ответ из двух частей:

  • элементы — да: метод получает копию ссылки и меняет тот же массив;
  • саму переменную вызывающего кода — нет: присваивание параметру меняет только копию ссылки.

С ref int[] nums метод получил бы саму переменную prices и смог бы положить в неё ссылку на новый массив. Без ref массив, как и int, передаётся по значению, а не по ссылке: копируется ссылка, которая лежит в переменной.

Настоящая копия

Иногда нужен второй ряд шкафчиков с теми же вещами, чтобы менять копию и не трогать оригинал. Для этого создают новый массив той же длины и копируют в него элементы методом Array.Copy:

C#
int[] original = [1, 2, 3];
int[] copy = new int[original.Length];
Array.Copy(original, copy, original.Length);
copy[0] = 10;
Console.WriteLine(original[0]); // 1
Console.WriteLine(copy[0]);     // 10

Array.Copy(original, copy, original.Length) копирует из original в copy столько элементов, сколько указано третьим аргументом. После этого у массивов нет общих элементов и запись в copy не трогает original.

Частая ошибка

Два массива с одинаковыми элементами сравнили через ==:

C#
int[] a = [1, 2, 3];
int[] b = [1, 2, 3];
bool same = a == b; // ✗
Console.WriteLine(same); // False

Программа компилируется и печатает False, хотя элементы одинаковые. Для массивов == сравнивает ссылки, то есть проверяет, один ли это массив. Здесь массивов два, как два ряда шкафчиков с одинаковыми вещами. Чтобы сравнить содержимое, проверяют длины, а затем элементы по одному в цикле.

Главное

  • Переменная массива хранит ссылку на массив, а не сами элементы: массивы — ссылочный тип.
  • int[] b = a; копирует ссылку: у массива становится два имени, и запись через b видна через a.
  • Метод, получивший массив, может изменить его элементы, но присваивание параметру не меняет переменную вызывающего кода.
  • Настоящую копию делают через новый массив и Array.Copy; == для массивов проверяет, один ли это массив.