C# 8.0 wniósł dwie nowości dla tablic jak i wszystkiego co implementuje interfejs IList<T>. Te dwie nowości to Index i Range:
Pierwszą z nowości jest Index - jak sama nazwa wskazuje jest to pozycja elementu w tablicy. Index to typ wartościowy - struktura readonly. Tworzyć obiekt tego typu możemy jak dla każdej innej struktury:
int[] tab = { 1, 2, 3, 4, 5, 6 };
var index0 = new Index(1, fromEnd: true);
Console.WriteLine(tab[index0]);
Dodatkowy parametr (opcjonalny) fromEnd pozwala zdefiniować czy szukamy elementu o indeksie 1 od początku kolekcji czy od końca. W tym przypadku otrzymamy 1 wartość od końca czyli 6. Index nie musi być "ręcznie" tworzony w kodzie - możemy posłużyć się operatorem ^ i tak aby pobrać ostatni element wystarczy zapis:
int[] tab = { 1, 2, 3, 4, 5, 6 };
Console.WriteLine(tab[^1]);
Ktoś może zapytać: "zaraz, skoro tablice są numerowane od 0, to dlaczego ostatni element pobieramy za pomocą ^1 a nie ^0"?
Sprawa jest dosyć prosta patrząc na poniższy rysunek, przedstawiający 4 elementową tablicę:
Kiedy chcemy pobrać element kolekcji (np. tablicy) poprzez indeks - żądamy pobrania elementu który zaczyna się na danej pozycji. Czyli dla przykładu jeżeli chcemy pobrać element na pozycji 0 to znaczy że chcemy odczytać wartość zawartą między indeksami 0 i 1. Dlatego własnie, patrząc na rysunek jeżeli chcielibyśmy pobrać wartość elementu ^0 oznaczałoby to chęć pobrania elementu tablicy który jest za ostatnim elementem tablicy, czyli chcemy odczytać pamięć która już nie należy do bloku tablicy. Próba taka zwróci oczywiście błąd. W drugą stronę działa to tak samo - jeżeli chcielibyśmy odczytać element na pozycji 4. Ponieważ tablica ma tylko 4 elementu, nie ma ona wartości dla indeksu między 4 i 5.
Drugą nowością jest operator zakresu. Zakres to nic innego jak dwa indeksy: początek i koniec. Najłatwiej będzie zaprezentować ten operator na przykładach:
static void Main(string[] args)
{
int[] tab = { 1, 2, 3, 4, 5, 6 };
var range1 = new Range(2, 3);
var range2 = 2..3;
var range3 = 1..^2;
var range4 = Range.All;
var range5 = Range.EndAt(2);
var range6 = 0..^1;
var range7 = ..;
var range8 = 2..;
var range9 = ..2;
PrintContent(tab, range1, nameof(range1));
PrintContent(tab, range2, nameof(range2));
PrintContent(tab, range3, nameof(range3));
PrintContent(tab, range4, nameof(range4));
PrintContent(tab, range5, nameof(range5));
PrintContent(tab, range6, nameof(range6));
PrintContent(tab, range7, nameof(range7));
PrintContent(tab, range8, nameof(range8));
PrintContent(tab, range9, nameof(range9));
}
private static void PrintContent(int[] tab, Range range, string rangeName)
{
Console.WriteLine(rangeName);
Console.WriteLine(string.Join(", ", tab[range]));
Console.WriteLine(new string('-', 40));
}
Po zapoznaniu się z częścią poświęconą indeksom bardzo łatwo zrozumieć operator zakresu. Warto zapamiętać domyślne wartości:
- jeżeli nie podamy lewego zakresu (startowego) będzie to zawsze 0
- jeżeli nie podamy prawego zakresu (końcowego) będzie to zawsze -1
- jeżeli stworzymy pusty obiekt zakresu (new Range()) zostanie stworzony zakres 0..0, czyli nie pobierzemy żadnego elementu. Jest to o tyle istotne, że domyślnie dla zakresu ".." dostajemy całą tablicę.
W jaki sposób działa Range? Działanie jest proste - zostaje stworzona nowa tablica i przekopiowane do niej wartości z zadanego zakresu. Zakres działa także dla typu string. Stosując go na obiektach łańcuchowych otrzymujemy sub-string.