Indexers (Indeksatory)

Indeksatory są podobne do właściwości. Na wiele sposobów indeksatory bazują na tych samych funkcjach językowych co właściwości. Indeksatory włączają właściwości indeksowane : właściwości przywoływane przy użyciu co najmniej jednego argumentu. Te argumenty udostępniają indeks do kolekcji wartości.

Składnia indeksatora

Dostęp do indeksatora można uzyskać za pomocą nazwy zmiennej i nawiasów kwadratowych. Argumenty indeksatora są umieszczane w nawiasach kwadratowych:

var item = someObject["key"];
someObject["AnotherKey"] = item;

Indeksatory są deklarowane przy użyciu słowa kluczowego this jako nazwy właściwości i deklarowanie argumentów w nawiasach kwadratowych. Ta deklaracja będzie zgodna z użyciem pokazanym w poprzednim akapicie:

public int this[string key]
{
    get { return storage.Find(key); }
    set { storage.SetAt(key, value); }
}

W tym początkowym przykładzie można zobaczyć relację między składnią właściwości a indeksatorami. Ta analogia obejmuje większość reguł składni dla indeksatorów. Indeksatory mogą mieć dowolne prawidłowe modyfikatory dostępu (publiczne, chronione wewnętrzne, chronione, wewnętrzne, prywatne lub prywatne). Mogą być zapieczętowane, wirtualne lub abstrakcyjne. Podobnie jak we właściwościach, można określić różne modyfikatory dostępu dla metod pobierania i ustawiania metod dostępu w indeksatorze. Można również określić indeksatory tylko do odczytu (pomijając zestaw metod dostępu) lub indeksatory tylko do zapisu (pomijając metodę pobierania).

Możesz zastosować prawie wszystko, czego nauczysz się od pracy z właściwościami do indeksatorów. Jedynym wyjątkiem od tej reguły są właściwości zaimplementowane automatycznie. Kompilator nie zawsze może wygenerować prawidłowy magazyn dla indeksatora.

Obecność argumentów odwołującego się do elementu w zestawie elementów odróżnia indeksatory od właściwości. Można zdefiniować wiele indeksatorów w typie, o ile listy argumentów dla każdego indeksatora są unikatowe. Przyjrzyjmy się różnym scenariuszom, w których można użyć co najmniej jednego indeksatora w definicji klasy.

Scenariusze

Indeksatory należy zdefiniować w swoim typie, gdy jego interfejs API modeluje kolekcję, w której definiuje się argumenty tej kolekcji. Indeksatory mogą lub nie mogą być mapowane bezpośrednio na typy kolekcji, które są częścią platformy .NET Core. Twój typ może mieć inne obowiązki oprócz modelowania kolekcji. Indeksatory umożliwiają podanie interfejsu API zgodnego z abstrakcją typu bez uwidaczniania wewnętrznych szczegółów sposobu przechowywania lub obliczania wartości dla tej abstrakcji.

Zapoznajmy się z niektórymi typowymi scenariuszami dotyczącymi używania indeksatorów. Dostęp do przykładowego folderu dla indeksatorów można uzyskać. Aby uzyskać instrukcje dotyczące pobierania, zobacz Przykłady i samouczki.

Tablice i wektory

Jednym z najbardziej typowych scenariuszy tworzenia indeksatorów jest to, że typ modeluje tablicę lub wektor. Indeksator można utworzyć w celu modelowania uporządkowanej listy danych.

Zaletą tworzenia własnego indeksatora jest możliwość zdefiniowania magazynu dla tej kolekcji zgodnie z potrzebami. Wyobraź sobie scenariusz, w którym typ modeluje dane historyczne, które są zbyt duże, aby załadować je do pamięci jednocześnie. Musisz załadować i zwolnić sekcje kolekcji na podstawie użycia. W poniższym przykładzie przedstawiono modele tego zachowania. Raportuje, ile punktów danych istnieje. Tworzy strony do przechowywania sekcji danych na żądanie. Usuwa strony z pamięci, aby zapewnić miejsce na strony wymagane przez nowsze żądania.

public class DataSamples
{
    private class Page
    {
        private readonly List<Measurements> pageData = new List<Measurements>();
        private readonly int startingIndex;
        private readonly int length;
        private bool dirty;
        private DateTime lastAccess;

        public Page(int startingIndex, int length)
        {
            this.startingIndex = startingIndex;
            this.length = length;
            lastAccess = DateTime.Now;

            // This stays as random stuff:
            var generator = new Random();
            for(int i=0; i < length; i++)
            {
                var m = new Measurements
                {
                    HiTemp = generator.Next(50, 95),
                    LoTemp = generator.Next(12, 49),
                    AirPressure = 28.0 + generator.NextDouble() * 4
                };
                pageData.Add(m);
            }
        }
        public bool HasItem(int index) =>
            ((index >= startingIndex) &&
            (index < startingIndex + length));

        public Measurements this[int index]
        {
            get
            {
                lastAccess = DateTime.Now;
                return pageData[index - startingIndex];
            }
            set
            {
                pageData[index - startingIndex] = value;
                dirty = true;
                lastAccess = DateTime.Now;
            }
        }

        public bool Dirty => dirty;
        public DateTime LastAccess => lastAccess;
    }

    private readonly int totalSize;
    private readonly List<Page> pagesInMemory = new List<Page>();

    public DataSamples(int totalSize)
    {
        this.totalSize = totalSize;
    }

    public Measurements this[int index]
    {
        get
        {
            if (index < 0)
                throw new IndexOutOfRangeException("Cannot index less than 0");
            if (index >= totalSize)
                throw new IndexOutOfRangeException("Cannot index past the end of storage");

            var page = updateCachedPagesForAccess(index);
            return page[index];
        }
        set
        {
            if (index < 0)
                throw new IndexOutOfRangeException("Cannot index less than 0");
            if (index >= totalSize)
                throw new IndexOutOfRangeException("Cannot index past the end of storage");
            var page = updateCachedPagesForAccess(index);

            page[index] = value;
        }
    }

    private Page updateCachedPagesForAccess(int index)
    {
        foreach (var p in pagesInMemory)
        {
            if (p.HasItem(index))
            {
                return p;
            }
        }
        var startingIndex = (index / 1000) * 1000;
        var newPage = new Page(startingIndex, 1000);
        addPageToCache(newPage);
        return newPage;
    }

    private void addPageToCache(Page p)
    {
        if (pagesInMemory.Count > 4)
        {
            // remove oldest non-dirty page:
            var oldest = pagesInMemory
                .Where(page => !page.Dirty)
                .OrderBy(page => page.LastAccess)
                .FirstOrDefault();
            // Note that this may keep more than 5 pages in memory
            // if too much is dirty
            if (oldest != null)
                pagesInMemory.Remove(oldest);
        }
        pagesInMemory.Add(p);
    }
}

Możesz postępować zgodnie z tym projektem idiom, aby modelować dowolną kolekcję, w której istnieją dobre powody, aby nie ładować całego zestawu danych do kolekcji w pamięci. Zwróć uwagę, że Page klasa jest prywatną klasą zagnieżdżona, która nie jest częścią interfejsu publicznego. Te szczegóły są ukryte przed wszystkimi użytkownikami tej klasy.

Słowniki

Innym typowym scenariuszem jest modelowanie słownika lub mapy. W tym scenariuszu typ przechowuje wartości na podstawie klucza, zazwyczaj kluczy tekstowych. W tym przykładzie tworzony jest słownik, który mapuje argumenty wiersza polecenia na wyrażenia lambda, które zarządzają tymi opcjami. W poniższym przykładzie pokazano dwie klasy: klasę ArgsActions , która mapuje opcję wiersza polecenia na Action delegata, oraz klasę ArgsProcessor , która używa ArgsActions elementu do wykonania w Action przypadku napotkania tej opcji.

public class ArgsProcessor
{
    private readonly ArgsActions actions;

    public ArgsProcessor(ArgsActions actions)
    {
        this.actions = actions;
    }

    public void Process(string[] args)
    {
        foreach(var arg in args)
        {
            actions[arg]?.Invoke();
        }
    }

}
public class ArgsActions
{
    readonly private Dictionary<string, Action> argsActions = new Dictionary<string, Action>();

    public Action this[string s]
    {
        get
        {
            Action action;
            Action defaultAction = () => {} ;
            return argsActions.TryGetValue(s, out action) ? action : defaultAction;
        }
    }

    public void SetOption(string s, Action a)
    {
        argsActions[s] = a;
    }
}

W tym przykładzie ArgsAction kolekcja jest ściśle mapowania na podstawową kolekcję. Określa get , czy dana opcja została skonfigurowana. Jeśli tak, zwraca Action wartość skojarzona z tą opcją. Jeśli tak nie jest, zwraca wartość Action , która nic nie robi. Publiczny akcesorium nie zawiera set akcesorium. Projekt używa raczej metody publicznej do ustawiania opcji.

Mapy wielowymiarowe

Można utworzyć indeksatory używające wielu argumentów. Ponadto te argumenty nie są ograniczone do tego samego typu. Przyjrzyjmy się dwóm przykładom.

W pierwszym przykładzie pokazano klasę, która generuje wartości dla zestawu Mandelbrot. Aby uzyskać więcej informacji na temat matematyki za zestawem, przeczytaj ten artykuł. Indeksator używa dwóch podwójnych do zdefiniowania punktu w płaszczyźnie X, Y. Metodę pobierania oblicza liczbę iteracji, dopóki punkt nie zostanie określony w zestawie. Jeśli osiągnięto maksymalną iterację, punkt znajduje się w zestawie, a wartość maxIterations klasy jest zwracana. (Komputer wygenerowany obrazy spopularyzowane dla zestawu Mandelbrot definiuje kolory dla liczby iteracji niezbędnych do określenia, że punkt znajduje się poza zestawem).

public class Mandelbrot
{
    readonly private int maxIterations;

    public Mandelbrot(int maxIterations)
    {
        this.maxIterations = maxIterations;
    }

    public int this [double x, double y]
    {
        get
        {
            var iterations = 0;
            var x0 = x;
            var y0 = y;

            while ((x*x + y * y < 4) &&
                (iterations < maxIterations))
            {
                var newX = x * x - y * y + x0;
                y = 2 * x * y + y0;
                x = newX;
                iterations++;
            }
            return iterations;
        }
    }
}

Zestaw Mandelbrot definiuje wartości na każdej współrzędnej (x,y) dla wartości liczb rzeczywistych. Definiuje słownik, który może zawierać nieskończoną liczbę wartości. W związku z tym nie ma magazynu za zestawem. Zamiast tego ta klasa oblicza wartość dla każdego punktu, gdy kod wywołuje metodę get dostępu. Nie ma używanego magazynu bazowego.

Przeanalizujmy jedno ostatnie użycie indeksatorów, w którym indeksator przyjmuje wiele argumentów różnych typów. Rozważmy program, który zarządza historycznymi danymi o temperaturze. Ten indeksator używa miasta i daty do ustawienia lub uzyskania wysokich i niskich temperatur dla tej lokalizacji:

using DateMeasurements =
    System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>;
using CityDataMeasurements =
    System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>>;

public class HistoricalWeatherData
{
    readonly CityDataMeasurements storage = new CityDataMeasurements();

    public Measurements this[string city, DateTime date]
    {
        get
        {
            var cityData = default(DateMeasurements);

            if (!storage.TryGetValue(city, out cityData))
                throw new ArgumentOutOfRangeException(nameof(city), "City not found");

            // strip out any time portion:
            var index = date.Date;
            var measure = default(Measurements);
            if (cityData.TryGetValue(index, out measure))
                return measure;
            throw new ArgumentOutOfRangeException(nameof(date), "Date not found");
        }
        set
        {
            var cityData = default(DateMeasurements);

            if (!storage.TryGetValue(city, out cityData))
            {
                cityData = new DateMeasurements();
                storage.Add(city, cityData);
            }

            // Strip out any time portion:
            var index = date.Date;
            cityData[index] = value;
        }
    }
}

W tym przykładzie tworzony jest indeksator, który mapuje dane pogodowe na dwa różne argumenty: miasto (reprezentowane przez string) i datę (reprezentowaną przez ).DateTime Magazyn wewnętrzny używa dwóch Dictionary klas do reprezentowania słownika dwuwymiarowego. Publiczny interfejs API nie reprezentuje już magazynu bazowego. Zamiast tego funkcje językowe indeksatorów umożliwiają tworzenie interfejsu publicznego reprezentującego abstrakcję, mimo że magazyn podstawowy musi używać różnych podstawowych typów kolekcji.

Istnieją dwie części tego kodu, które mogą być nieznane niektórym deweloperom. Te dwie using dyrektywy:

using DateMeasurements = System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>;
using CityDataMeasurements = System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>>;

utwórz alias dla skonstruowanego typu ogólnego. Te instrukcje umożliwiają później kodowi używanie bardziej opisowych DateMeasurements i CityDataMeasurements nazw zamiast ogólnej konstrukcji Dictionary<DateTime, Measurements> i Dictionary<string, Dictionary<DateTime, Measurements> >. Ta konstrukcja wymaga użycia w pełni kwalifikowanych nazw typów po prawej stronie = znaku.

Druga technika polega na usuwaniu fragmentów czasu dowolnego DateTime obiektu używanego do indeksowania w kolekcjach. Platforma .NET nie zawiera typu tylko daty. Deweloperzy używają DateTime typu , ale używają Date właściwości , aby upewnić się, że każdy DateTime obiekt z tego dnia jest równy.

Sumowanie

Indeksatory należy utworzyć za każdym razem, gdy w klasie istnieje element przypominający właściwość, w którym ta właściwość nie reprezentuje jednej wartości, ale raczej kolekcję wartości, w których każdy pojedynczy element jest identyfikowany przez zestaw argumentów. Te argumenty mogą jednoznacznie określić, do którego elementu w kolekcji należy się odwoływać. Indeksatory rozszerzają koncepcję właściwości, gdzie składowa jest traktowana jak element danych spoza klasy, ale jak metoda wewnątrz. Indeksatory umożliwiają argumentom znalezienie pojedynczego elementu we właściwości reprezentującej zestaw elementów.