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

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

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

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

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

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

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

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

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

Соответственно, инициализация:

int[] arr1 = new int[3] {1, 2, 3};
int[] arr2 = new int[] {1, 2, 3};
int[] arr3 = {1, 2, 3}; // краткая форма

Многомерные массивы (прямоугольные) это массивы с двумя и более измерениями. Все строки имеют одинаковую длину. Двумерный массив (матрица):

int[,] matrix = new int[2, 3]
{
{1, 2, 3},
{4, 5, 6}
};

Доступ:

int value = matrix[0, 1]; // 2
matrix[1, 2] = 99;

Размеры:

int rows = matrix.GetLength(0); // 2
int cols = matrix.GetLength(1); // 3

Трёхмерный массив:

int[,,] cube = new int[2, 3, 4];

Ступенчатые массивы (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[][] jagged = new int[][]
{
new int[] {1, 2},
new int[] {3, 4, 5},
new int[] {6}
};

Доступ:

int value = jagged[1][2]; // 5

То есть, [][] это ступенчатый массив, а [] массив.

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

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

Если элементы, как выше - однородные, допустим, 1, 2, 3 - то тип будет int[]. Но если они будут неоднородными:

var arr = new[] {1, "hello", true};

…то тип будет object[].

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

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

Массивы всегда проверяют границы при доступе (в обычном режиме). Это безопасно, но немного замедляет выполнение. C# 8.0 ввёл современные операторы для работы с подмассивами и индексами:

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

string[] names = { "Alice", "Bob", "Charlie" };
string last = names[^1]; // "Charlie"
string secondLast = names[^2]; // "Bob"
  • ^0 — за последним элементом (недопустимо для доступа)
  • ^1 — последний элемент
  • ^n — n-й элемент с конца

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

string[] slice = names[1..3]; // элементы с индекса 1 по 3 (не включая 3)
// → {"Bob", "Charlie"}
  • 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[..]; // копия всего массива

Диапазоны работают не только с массивами, но и с 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}

Очень эффективно, используется в высокопроизводительных сценариях (например, парсинг). Span<T> нельзя хранить в полях класса (только в стеке или ref struct).

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

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

Используется, когда нужно передавать диапазон между методами или асинхронно.

Класс 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}

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;

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

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