Массивы, списки и диапазоны
Как выбрать структуру под задачу
Практический ориентир:
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) | Возвращает индекс первого вхождения указанного значения; если не найдено — -1 | int 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;
Прямая деструктуризация массивов не поддерживается, но можно использовать расширения или кортежи.
И поскольку мы затронули кортежи, давайте перейдемте к следующей теме.