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