OOP Course
Сьогодні

Підрозділ 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 та 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..];   // останні 5

Range можна зберігати у змінній

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).

Вимірювання тиску: Index та Range — runnable приклад

Range зі Span та GetOffsetAndLength — runnable приклад

Розроблено Tomka Yurii · © 2026 ·