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

Массивы, списки и диапазоны

Как выбрать структуру под задачу

Практический ориентир:

  • T[] - фиксированный объём и быстрый доступ по индексу;
  • List<T> - универсальный выбор для большинства бизнес-задач;
  • Span<T> - высокопроизводительная обработка без лишних аллокаций;
  • Memory<T> - когда диапазон нужно хранить дольше и передавать асинхронно.

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

  • Использование Array.Resize в частом цикле (каждый раз создаётся новый массив).
  • Выбор многомерного массива, когда удобнее и проще jagged.
  • Непонимание, что срез arr[a..b] создаёт новый массив для T[].

Мини-паттерн "безопасный доступ к диапазону"

ReadOnlySpan<int> window = data.AsSpan(start, length);
foreach (var item in window)
{
// обработка без копирования исходного массива
}

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

Массивы, списки и диапазоны

Разработчику Архитектору

Play ITЗагрузка интерактивного демо…


Массивы, списки и диапазоны

Массивы и диапазоны

Массив — это фиксированный по размеру контейнер, хранящий элементы одного типа, доступ к которым осуществляется по индексу (целое число, начиная с 0).

int[] numbers = new int[5]; // массив из 5 целых чисел
numbers[0] = 10; // доступ по индексу

Разбор:

  • int[] объявляет массив целых чисел фиксированной длины.
  • new int[5] выделяет память под 5 элементов, все стартуют со значением 0 по умолчанию.
  • Индексация начинается с 0, поэтому numbers[0] - первый элемент.
  • Запись numbers[0] = 10 изменяет значение по конкретному индексу.

Массивы в C# — объекты, наследуемые от System.Array. Это ссылочный тип, даже если хранит значимые типы.

Одномерные массивы - самый простой и часто используемый тип.

int[] numbers = new int[3];
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;

Разбор:

  • Массив создаётся с фиксированным размером 3, изменить длину после этого нельзя.
  • Каждое присваивание обращается к конкретной ячейке по индексу.
  • Такой стиль удобен, когда данные заполняются пошагово.
  • Попытка обратиться к numbers[3] вызовет IndexOutOfRangeException. Соответственно, инициализация:
int[] arr1 = new int[3] {1, 2, 3};
int[] arr2 = new int[] {1, 2, 3};
int[] arr3 = {1, 2, 3}; // краткая форма

Разбор:

  • Все три строки создают один и тот же тип данных: int[] из трёх элементов.
  • arr1 явно задаёт размер и список инициализации одновременно.
  • arr2 позволяет компилятору вывести длину по количеству элементов.
  • arr3 - наиболее краткая форма, часто используемая в прикладном коде. Многомерные массивы (прямоугольные) это массивы с двумя и более измерениями. Все строки имеют одинаковую длину. Двумерный массив (матрица):
int[,] matrix = new int[2, 3]
{
{1, 2, 3},
{4, 5, 6}
};

Разбор:

  • int[,] - прямоугольный двумерный массив (матрица).
  • Размер new int[2, 3] означает 2 строки и 3 столбца.
  • Вложенные фигурные скобки инициализируют значения построчно.
  • Все строки обязаны иметь одинаковую длину, в отличие от jagged-массива. Доступ:
int value = matrix[0, 1]; // 2
matrix[1, 2] = 99;

Разбор:

  • Для двумерного массива индекс указывается парой: [row, column].
  • matrix[0, 1] читает элемент первой строки и второго столбца.
  • matrix[1, 2] = 99 изменяет значение в указанной ячейке.
  • Такой доступ удобен для матриц, таблиц и игровых карт. Размеры:
int rows = matrix.GetLength(0); // 2
int cols = matrix.GetLength(1); // 3

Разбор:

  • GetLength(dimension) возвращает размер по конкретному измерению.
  • Измерение 0 соответствует строкам, измерение 1 - столбцам.
  • Этот способ безопаснее "магических чисел", особенно в универсальных циклах.
  • Подходит для кода, который должен работать с матрицами разного размера. Трёхмерный массив:
int[,,] cube = new int[2, 3, 4];

Разбор:

  • int[,,] объявляет трёхмерный массив (3 измерения).
  • Размеры 2, 3, 4 можно трактовать как "слой-строка-столбец".
  • Такой тип полезен для 3D-данных, вокселей, временных срезов и т.п.
  • Доступ к элементам будет происходить через три индекса — cube[x, y, z]. Ступенчатые массивы (jagged arrays) это массив массивов. Каждый "вложенный" массив может иметь разную длину.
int[][] jagged = new int[3][];
jagged[0] = new int[] {1, 2};
jagged[1] = new int[] {3, 4, 5, 6};
jagged[2] = new int[] {7};

Разбор:

  • int[][] - ступенчатый массив: массив, где каждый элемент сам является массивом.
  • Внешний массив имеет длину 3, но внутренние массивы могут быть разной длины.
  • Это гибче прямоугольной матрицы, когда строки неравномерны.
  • Инициализация в несколько шагов удобна, если длины строк вычисляются динамически. Инициализация:
int[][] jagged = new int[][]
{
new int[] {1, 2},
new int[] {3, 4, 5},
new int[] {6}
};

Разбор:

  • Это компактная литеральная инициализация ступенчатого массива.
  • Каждая "строка" задаётся отдельным int[] внутри общего массива.
  • По структуре данные становятся похожи на список строк переменной длины.
  • Читаемость выше, когда исходные значения известны заранее. Доступ:
int value = jagged[1][2]; // 5

Разбор:

  • Двойная индексация отражает два уровня вложенности: сначала строка, потом элемент в строке.
  • jagged[1] выбирает второй внутренний массив {3,4,5,...}.
  • [2] берёт третий элемент этой строки, то есть 5.
  • Перед доступом важно учитывать фактическую длину каждой внутренней строки. То есть, [][] это ступенчатый массив, а [] массив.

Если тип неявный, то соответственно используется var имя = new[] {элементы}:

var arr = new[] {1, 2, 3};

Разбор:

  • var включает вывод типа компилятором по правой части.
  • new[] {1, 2, 3} создаёт массив с автоматически выведенным типом int[].
  • Запись удобна, когда тип очевиден из литерала.
  • Если элементы одного типа, компилятор выбирает этот тип без явного указания. Если элементы, как выше - однородные, допустим, 1, 2, 3 - то тип будет int[]. Но если они будут неоднородными:
var arr = new[] {1, "hello", true};

Разбор:

  • В массиве смешаны int, string и bool, общего узкого типа у них нет.
  • Компилятор поднимает тип до общего предка object.
  • В итоге arr имеет тип object[].
  • Такой массив менее удобен: для типобезопасной работы часто нужны проверки и приведения. …то тип будет object[].

Доступ к элементам по индексу - индексация начинается с нуля, соответственно для получения доступа к первому элементу, используется arr[0]. И если мы попытались выйти за границы массива - получаем IndexOutOfRangeException.

int[] numbers = {10, 20, 30};
Console.WriteLine(numbers[0]); // 10
numbers[1] = 99;

Разбор:

  • Литерал {10, 20, 30} создаёт и инициализирует массив в одну строку.
  • numbers[0] читает первый элемент и выводит его в консоль.
  • numbers[1] = 99 демонстрирует изменяемость содержимого массива.
  • Размер остаётся фиксированным, но значения элементов можно менять. Массивы всегда проверяют границы при доступе (в обычном режиме). Это безопасно, но немного замедляет выполнение.

C# 8.0 ввёл современные операторы для работы с подмассивами и индексами:

^ — индекс от конца

string[] names = { "Alice", "Bob", "Charlie" };
string last = names[^1]; // "Charlie"
string secondLast = names[^2]; // "Bob"

Разбор:

  • Оператор ^ задаёт индекс с конца массива.
  • ^1 - последний элемент, ^2 - предпоследний.
  • Такой синтаксис избавляет от вычислений вида names[names.Length - 1].
  • Повышает читаемость при работе с хвостом коллекции.
    • ^0 — за последним элементом (недопустимо для доступа)
    • ^1 — последний элемент
    • ^n — n-й элемент с конца

.. (две точки)— диапазон (range), который создаёт диапазон индексов: start..end:

string[] slice = names[1..3]; // элементы с индекса 1 по 3 (не включая 3)
// → {"Bob", "Charlie"}

Разбор:

  • 1..3 формирует диапазон: начало включительно, конец не включается.
  • Результат - новый массив-срез, а не "вид" исходного массива.
  • В срез попадут элементы с индексами 1 и 2.
  • Диапазоны делают подмассивы компактнее и легче читаются, чем ручные циклы копирования.
    • 0..^0 — весь массив
    • 1..^1 — всё, кроме первого и последнего
    • ..5 — от начала до 5 (эквивалент 0..5)
    • ^3.. — последние 3 элемента
int[] numbers = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

var firstThree = numbers[..3]; // {0,1,2}
var lastTwo = numbers[^2..]; // {8,9}
var middle = numbers[2..^2]; // {2,3,4,5,6,7}
var all = numbers[..]; // копия всего массива

Разбор:

  • [..3] берёт элементы с начала до индекса 3 (не включая его).
  • [^2..] выбирает диапазон от предпоследнего элемента до конца.
  • [2..^2] вырезает "середину", отбрасывая два элемента справа.
  • [..] создаёт копию всего массива, что полезно для безопасной передачи без изменения оригинала.

Диапазоны работают не только с массивами, но и с string, Span<T>, Memory<T>, List<T> (через .ToArray() или .AsSpan()).

Существует и особый инструмент для безопасной работы с памятью - это стековый и управляемый диапазоны.

Span<T> — стековый диапазон.

Span<T> — ref-структура, позволяющая безопасно работать с непрерывным участком памяти без выделения в куче.

int[] data = {1, 2, 3, 4, 5};
Span<int> span = data.AsSpan();

span[0] = 99;
var subset = span[1..3]; // {2, 3}

Разбор:

  • AsSpan() создаёт Span<int> поверх существующего массива без новой аллокации.
  • Изменение span[0] меняет и исходный массив data, так как это представление тех же данных.
  • span[1..3] создаёт под-диапазон без копирования памяти.
  • Такой подход важен для высокопроизводительных участков: минимум копий и минимум GC-нагрузки. Очень эффективно, используется в высокопроизводительных сценариях (например, парсинг). Span<T> нельзя хранить в полях класса (только в стеке или ref struct).

Memory<T> — управляемый диапазон в куче. Memory<T> — похож на Span<T>, но может быть сохранён в куче (например, в поле класса).

Memory<int> memory = data.AsMemory();
Span<int> stackSpan = memory.Span; // можно получить Span

Разбор:

  • Memory<int> - heap-friendly обёртка диапазона, которую можно хранить и передавать дольше.
  • Свойство Span даёт быстрый доступ к текущему участку памяти для синхронной обработки.
  • Пара Memory<T> + Span<T> полезна, когда нужно совместить асинхронные API и производительность.
  • Шаблон особенно частый в I/O, сетевом коде и пайплайнах обработки данных. Используется, когда нужно передавать диапазон между методами или асинхронно.

Класс System.Array предоставляет статические методы для работы с массивами.

МетодОписаниеПример
array.LengthВозвращает количество элементов в массивеint len = arr.Length;
array[index]Получает или устанавливает элемент по указанному индексуarr[0] = 10;
Array.Sort(arr)Сортирует элементы массива по возрастаниюArray.Sort(numbers);
Array.Reverse(arr)Изменяет порядок элементов в массиве на обратныйArray.Reverse(names);
Array.IndexOf(arr, value)Возвращает индекс первого вхождения указанного значения; если не найдено — -1int index = Array.IndexOf(arr, "target");
Array.LastIndexOf(arr, value)Возвращает индекс последнего вхождения указанного значенияint i = Array.LastIndexOf(arr, 5);
Array.Find(arr, predicate)Находит первый элемент, удовлетворяющий условиюvar first = Array.Find(arr, x => x > 10);
Array.Clear(arr, start, count)Устанавливает элементы в диапазоне [start, start + count) в значение по умолчанию (null, 0, false)Array.Clear(arr, 0, 2);
Array.Resize(ref arr, newSize)Изменяет размер массива (создаёт новый массив и копирует данные)Array.Resize(ref arr, 10);
LINQ: .Where(), .Select()Расширяет возможности обработки массивов с использованием LINQ-запросовvar filtered = arr.Where(x => x > 5).ToArray();

Массивы неизменяемы по размеру, поэтому Resize создаёт новый массив.

Имеется также особый тип работы с массивами через LINQ. LINQ (Language Integrated Query) позволяет писать выражения, похожие на SQL, для фильтрации, преобразования и анализа данных. Подключается через using System.Linq;

Основные методы:

МетодОписаниеПример
.Where()Фильтрует элементы коллекции по заданному условиюarr.Where(x => x > 5)
.Select()Преобразует каждый элемент коллекции по заданному выражениюarr.Select(x => x * 2)
.Any()Возвращает true, если хотя бы один элемент удовлетворяет условиюarr.Any(x => x < 0)
.All()Возвращает true, если все элементы удовлетворяют условиюarr.All(x => x >= 0)
.Count()Возвращает количество элементов, удовлетворяющих условиюarr.Count(x => x % 2 == 0)
.First(), .FirstOrDefault()Возвращает первый элемент, удовлетворяющий условию; First() выбрасывает исключение, если элемент не найден, FirstOrDefault() возвращает default(T)arr.FirstOrDefault(x => x > 100)
.OrderBy(), .ThenBy()Сортирует коллекцию по ключу; ThenBy() добавляет дополнительный уровень сортировкиarr.OrderBy(x => x)
.ToArray(), .ToList()Преобразует результат запроса в массив или списокvar result = query.ToArray();

int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

var evenSquares = numbers
.Where(x => x % 2 == 0)
.Select(x => x * x)
.OrderBy(x => x)
.ToArray();

// Результат: {4, 16, 36, 64, 100}

Разбор:

  • .Where(x => x % 2 == 0) фильтрует только чётные числа.
  • .Select(x => x * x) преобразует каждый отфильтрованный элемент в квадрат.
  • .OrderBy(x => x) сортирует результат по возрастанию (здесь порядок и так уже возрастающий, но оператор показан как часть конвейера).
  • .ToArray() материализует ленивый LINQ-запрос в конкретный массив.
  • Цепочка демонстрирует декларативный стиль: "что получить", а не "как итерироваться". LINQ ленив — запрос выполняется только при итерации (например, при вызове .ToArray()).

Можно преобразовать строку в массив:

string text = "hello";
char[] chars = text.ToCharArray(); // ['h','e','l','l','o']

// Разделение строки
string csv = "apple,banana,orange";
string[] fruits = csv.Split(','); // ["apple", "banana", "orange"]

// С пробелами и разными разделителями
string line = "a b\tc\nd";
string[] words = line.Split(new char[] {' ', '\t', '\n'}, StringSplitOptions.RemoveEmptyEntries);

И массив в строку:

string[] names = {"Alice", "Bob", "Charlie"};
string result = string.Join(", ", names); // "Alice, Bob, Charlie"

int[] numbers = {1, 2, 3};
string numStr = string.Join("-", numbers); // "1-2-3"

В C# 7+ можно деструктурировать массивы и кортежи.

int[] numbers = {10, 20, 30};

// Деструктуризация с игнорированием
var (a, b, _) = numbers; // a=10, b=20, третий игнорируется

// Через позиционные свойства (работает с кортежами, но не напрямую с массивами)
// Но можно обернуть:
var data = (numbers[0], numbers[1], numbers[2]);
var (x, y, z) = data;

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

И поскольку мы затронули кортежи, давайте перейдемте к следующей теме.