Niedawno przeglądając kod jednego z projektów na GitHubie natknąłem się na dziwną, nieznaną mi wtedy składnię w dostępie przez indekser. Okazało się, że owa składnia to mała nowość z C# 8.0. Chciałbym ją dziś zaprezentować.
Indeksy
Składnia dostępu pod konkretny indeks w tablicy jest bardzo dobrze znana.
var array = new [] {10, 20, 30, 40, 50};
Console.WriteLine(array[2]);
Jeśli jednak chcielibyśmy odwołać się do przedostatniego elementu tablicy, moglibyśmy zrobić to w następujący, lekko nieczytelny, sposób:
array[array.Length-2]
Dzięki nowej składni, możemy to jednak zrobić inaczej:
array[^2]
Wystarczy poprzedzić liczbę znakiem ^, a indeks zostanie policzony od tyłu. Tak naprawdę zapis ^a jest równoznaczny z
new System.Index(a, fromEnd: true). Następnie obiekt typu System.Index jest przekazywany do indeksera. Pozwala to na własną obsługę nowej składni. Poniżej krótki przykład:
public class IndexIndexerClass
{
public int this[System.Index index]
{
get => index.IsFromEnd ? -index.Value : index.Value;
}
}
Wszystkie dostępne własności i metody System.Index możemy sprawdzić w dokumentacji.
Zakresy
Oprócz dostępu do elementów licząc od końca, nowa wersja języka została wzbogacona o możliwość odwoływania się do zakresów. Wygląda to tak:
array[1..3];
Jako wynik otrzymamy wszystkie elementy poczynając od pierwszego, a kończąc na trzecim (bez trzeciego). Możliwe jest także połączenie zakresów i indeksów:
array[1..^1]
Powyższe wyrażenie zwróci tablicę składającą się z wszystkich elementów oryginalnej tablicy oprócz pierwszego i ostatniego.
Również i w przypadku zakresów wyrażenie a..b zamieniane jest na odpowiadający obiekt: new System.Range(a, b). Aby klasa mogła korzystać z zakresów, wystarczy zaimplementować indekser przyjmujący System.Range.
public class RangeIndexerClass
{
public int this[System.Range range]
{
get => range.GetOffsetAndLength(100).Length;
}
}
Dostępne metody i własności System.Range można sprawdzić w dokumentacji.
Niejawna implementacja indeksów i zakresów
Najczęściej implementacja indekserów dla indeksów i zakresów będzie taka sama. Aby nie wymagać od programistów nadmiarowej pracy, przyjęte zostały następujące zasady:
- Jeśli typ posiada własność
LengthbądźCount(taki typ zwany jest Countable) to zapisarray[^a]będzie traktowany jakoarray[array.Length - a](bądź odpowiednio z Count). - Jeśli typ posiada metodę
Sliceto zapisarray[a..b]będzie zastąpiony poprzezarray.Slice(a, b-a). - Powyższe zasady mogą być ze sobą łączone.
Przykład podstawień wg powyższych reguł w SharpLab.io.
Bardziej formalny opis zasad podstawień znajduje się na MSDN.
Ciekawostka
Może nie do końca oczywste na pierwszy rzut oka, ale poniższe wyrażenia są jak najbardziej poprawne.
var index = ^1;
var range = 1..^2;
Podsumowanie
Przedstawiony w tym poście syntactic sugar wydaje mi się całkiem przydatnym udogodnieniem. Pokazuje także, że zespół pracujący nad rozwojem języka C# chce upraszczać życie programistom tegoż języka. Osobiście bardzo mnie to cieszy.
Na koniec dodam jeszcze, że przykłady z tego postu znajdują się także na moim GitHubie.