Підрозділ 13.6
Індекси та діапазони (Index та Range)
Пояснює індекси й діапазони C# 8.0: типи Index і Range, оператор ^ для доступу з кінця, оператор .. для підмасивів та застосування зі Span.
13.6. Індекси та діапазони (`Index` та `Range`)
До C# 8 отримати останній елемент масиву виглядало так: arr[arr.Length - 1]. Передостанній — arr[arr.Length - 2]. Взяти підмасив від 2-го до 5-го: arr.Skip(2).Take(3).ToArray() або ручний цикл. Ці операції прості концептуально, але громіздкі в записі і схильні до помилок «на одиницю».
C# 8 (.NET Core 3.0+) ввів два нових типи — System.Index і System.Range — та відповідний синтаксис: оператор ^ для індексування від кінця і оператор .. для діапазонів. Разом вони роблять роботу з послідовностями значно лаконічнішою.

Тип Index та оператор `^`
System.Index — структура, що представляє позицію в послідовності. Вона може бути прямою (від початку, як звичайний int) або зворотньою (від кінця, через оператор ^):
Index i0 = 2; // третій елемент (звичайний індекс)
Index i1 = ^1; // останній елемент
Index i2 = ^2; // передостаннійОператор ^n означає «n-й від кінця», де ^1 — останній, ^2 — передостанній і т. д. ^0 рівний .Length — це позиція за межею масиву, тому arr[^0] дасть IndexOutOfRangeException:
string[] names = { "Петренко", "Коваль", "Бойко", "Сидоренко" };
Console.WriteLine(names[^1]); // "Сидоренко" — останній
Console.WriteLine(names[^2]); // "Бойко" — передостанній
Console.WriteLine(names[0]); // "Петренко" — перший (прямий індекс)Index можна зберігати у змінній і передавати у методи:
Index last = ^1;
Console.WriteLine(names[last]); // "Сидоренко"Index.GetOffset(length) перетворює ^-індекс у числовий: ^1.GetOffset(4) == 3.
Тип Range та оператор `..`
System.Range — структура, що представляє діапазон індексів. Синтаксис a..b означає «від a включно до b виключно». Обидві межі можуть бути звичайними індексами або ^-індексами:
double[] pressures = { 120, 135, 128, 142, 118, 155, 130 };
double[] first3 = pressures[0..3]; // { 120, 135, 128 } — індекси 0,1,2
double[] mid = pressures[2..5]; // { 128, 142, 118 } — індекси 2,3,4
double[] last3 = pressures[^3..]; // { 155, 130, ? } — від ^3 до кінця
double[] all = pressures[..]; // весь масивМежі .. є необов'язковими: arr[..n] — перші n елементів (від 0), arr[n..] — від n до кінця, arr[..] — весь масив.
Поєднання ^ і .. робить типові операції дуже виразними:
double[] withoutFirst = pressures[1..]; // відкинути перший
double[] withoutLast = pressures[..^1]; // відкинути останній
double[] inner = pressures[1..^1]; // без першого і останнього
double[] lastFive = pressures[^5..]; // останні 5Range можна зберігати у змінній
Range — це значущий тип (struct), його можна зберігати і передавати:
Range recent = ^5..; // останні 5
Range inner = 1..^1; // без крайніх
double[] recentReadings = pressures[recent];
double[] innerReadings = pressures[inner];Range.GetOffsetAndLength(length) повертає кортеж (offset, length) — числові значення для ручного використання, якщо API не підтримує Range безпосередньо:
Range r = 1..^1;
var (offset, length) = r.GetOffsetAndLength(pressures.Length);
// offset=1, length=5 (для масиву з 7 елементів)Застосування до рядків
Оператор [..] працює зі string так само, як Substring, але значно лаконічніше:
string icd = "I10.9";
char first = icd[0]; // 'I'
char last = icd[^1]; // '9'
string code = icd[1..]; // "10.9" — без першого символу
string digits = icd[1..^2]; // "10" — код без літери і дрібниці
string ext = icd[^3..]; // ".9" — три символи з кінцяРезультат str[a..b] — це новий рядок (heap-копія), тобто семантично еквівалентний str.Substring(a, b - a). Для безкопійного варіанту — str.AsSpan()[a..b], що повертає ReadOnlySpan<char>.
Range та масиви: завжди нова копія
На відміну від Span, при застосуванні Range до масиву (T[]) завжди створюється новий масив:
int[] src = { 1, 2, 3, 4, 5 };
int[] copy = src[1..4]; // новий масив { 2, 3, 4 }
copy[0] = 99;
Console.WriteLine(src[1]); // 2 — src не змінивсяЯкщо потрібна безкопійна вибірка — використовуйте Span<T> або ReadOnlySpan<T>:
Span<int> slice = src.AsSpan()[1..4]; // вказівник, не копія
slice[0] = 99;
Console.WriteLine(src[1]); // 99 — оригінал змінивсяВибір між arr[a..b] (нова копія) і span[a..b] (без копії) залежить від сценарію: якщо результат зберігається надовго або передається куди-небудь — копія доречна; якщо обробляється тут-і-зараз у межах методу — Span ефективніший.
`List` та інші колекції
Index і Range підтримуються не всіма колекціями. T[] і string підтримують повністю. Span<T> і ReadOnlySpan<T> — повністю. List<T> підтримує Index (list[^1]), але не підтримує Range (list[1..3] — помилка компіляції). Для зрізів List<T> використовується метод .GetRange(index, count).