Indexery

Indexery jsou podobné vlastnostem. Indexery se řadou způsobů vytvářejí na stejných jazykových funkcích jako vlastnosti. Indexery umožňují indexované vlastnosti: vlastnosti odkazované pomocí jednoho nebo více argumentů. Tyto argumenty poskytují index do některé kolekce hodnot.

Syntaxe indexeru

K indexeru se dostanete prostřednictvím názvu proměnné a hranatých závorek. Argumenty indexeru umístíte do závorek:

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

Deklarujete indexery pomocí klíčového this slova jako název vlastnosti a deklarujete argumenty v hranatých závorkách. Tato deklarace by odpovídala použití uvedenému v předchozím odstavci:

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

V tomto počátečním příkladu vidíte vztah mezi syntaxí pro vlastnosti a indexery. Tato analogie provádí většinu pravidel syntaxe indexerů. Indexery můžou mít jakékoli platné modifikátory přístupu (veřejné, chráněné interní, chráněné, interní, soukromé nebo soukromé). Mohou být zapečetěné, virtuální nebo abstraktní. Stejně jako u vlastností můžete určit různé modifikátory přístupu pro přístupové objekty get a nastavit v indexeru. Můžete také zadat indexery jen pro čtení (vynecháním přístupového objektu set) nebo indexerů jen pro zápis (vynecháním přístupového objektu get).

Můžete použít téměř všechno, co se naučíte od práce s vlastnostmi na indexery. Jedinou výjimkou tohoto pravidla jsou automaticky implementované vlastnosti. Kompilátor nemůže vždy generovat správné úložiště indexeru.

Přítomnost argumentů odkazujících na položku v sadě položek rozlišuje indexery od vlastností. Pro typ můžete definovat více indexerů, pokud jsou seznamy argumentů pro každý indexer jedinečné. Pojďme se podívat na různé scénáře, ve kterých můžete v definici třídy použít jeden nebo více indexerů.

Scénáře

Indexery byste ve svém typu definovali, když jeho rozhraní API modeluje kolekci, ve které definujete argumenty této kolekce. Indexery se můžou nebo nemusí mapovat přímo na typy kolekcí, které jsou součástí rozhraní .NET Core. Váš typ může mít kromě modelování kolekce i jiné odpovědnosti. Indexery umožňují poskytnout rozhraní API, které odpovídá abstrakci vašeho typu, aniž byste museli odhalit vnitřní podrobnosti o tom, jak se hodnoty pro tuto abstrakci ukládají nebo počítají.

Pojďme si projít některé běžné scénáře použití indexerů. Přístup k ukázkové složce pro indexery. Pokyny ke stažení najdete v tématu Ukázky a kurzy.

Matice a vektory

Jedním z nejběžnějších scénářů pro vytváření indexerů je situace, kdy typ modeluje pole nebo vektor. Můžete vytvořit indexer pro modelování seřazeného seznamu dat.

Výhodou vytvoření vlastního indexeru je, že můžete definovat úložiště pro danou kolekci tak, aby vyhovovalo vašim potřebám. Představte si scénář, ve kterém vaše typy modeluje historická data, která jsou příliš velká na načtení do paměti najednou. Na základě využití je potřeba načíst a uvolnit oddíly kolekce. Následující příklad toto chování modeluje. Zaznamenává, kolik datových bodů existuje. Vytvoří stránky pro uložení oddílů dat na vyžádání. Odebere stránky z paměti, aby se uvolnilo místo pro stránky potřebné novějšími požadavky.

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);
    }
}

Podle tohoto návrhového idiomu můžete modelovat jakýkoli druh kolekce, kde existují dobré důvody, proč nenačíst celou sadu dat do kolekce v paměti. Všimněte si, že Page třída je soukromá vnořená třída, která není součástí veřejného rozhraní. Tyto podrobnosti jsou pro všechny uživatele této třídy skryté.

Slovníky

Dalším běžným scénářem je, když potřebujete modelovat slovník nebo mapu. Tento scénář je v případě, že váš typ ukládá hodnoty založené na klíči, obvykle textové klíče. Tento příklad vytvoří slovník, který mapuje argumenty příkazového řádku na výrazy lambda, které tyto možnosti spravují . Následující příklad ukazuje dvě třídy: ArgsActions třída, která mapuje možnost příkazového Action řádku na delegáta a ArgsProcessor která používá ArgsActions k provedení každé Action z nich, když narazí na danou možnost.

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;
    }
}

V tomto příkladu se ArgsAction kolekce mapuje těsně na podkladovou kolekci. Určuje get , jestli je daná možnost nakonfigurovaná. Pokud ano, vrátí přidruženou Action k této možnosti. Pokud ne, vrátí Action nic. Veřejný přístup neobsahuje přístupové set objekty. Místo toho návrh používá veřejnou metodu pro nastavení možností.

Multidimenzionální Mapy

Můžete vytvořit indexery, které používají více argumentů. Kromě toho tyto argumenty nejsou omezeny na stejný typ. Pojďme se podívat na dva příklady.

První příklad ukazuje třídu, která generuje hodnoty pro mandelbrot sadu. Další informace o matematice za sadou najdete v tomto článku. Indexer pomocí dvou dvojitých hodnot definuje bod v rovině X, Y. Get Accessor vypočítá počet iterací, dokud bod není určen, aby nebyl v sadě. Pokud dosáhnete maximálního počtu iterací, bod je v sadě a vrátí se hodnota maxIterations třídy. (Počítač generované obrázky popularizované pro sadu Mandelbrot definují barvy pro počet iterací nezbytných k určení, že bod je mimo sadu.)

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;
        }
    }
}

Sada Mandelbrot definuje hodnoty v každé souřadnici (x,y) pro reálné číselné hodnoty. Definuje slovník, který by mohl obsahovat nekonečný počet hodnot. Proto není k dispozici žádné úložiště za sadou. Místo toho tato třída vypočítá hodnotu pro každý bod při volání kódu přístupového objektu get . Nepoužívá se žádné podkladové úložiště.

Pojďme se podívat na poslední použití indexerů, kde indexer přebírá více argumentů různých typů. Zvažte program, který spravuje historická data o teplotě. Tento indexer používá město a datum k nastavení nebo získání vysokých a nízkých teplot pro dané umístění:

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;
        }
    }
}

Tento příklad vytvoří indexer, který mapuje data počasí na dva různé argumenty: město (reprezentované string) a datum (reprezentované DateTimehodnotou ). Interní úložiště používá k reprezentaci dvojrozměrného slovníku dvě Dictionary třídy. Veřejné rozhraní API už představuje základní úložiště. Funkce jazyka indexerů umožňují vytvořit veřejné rozhraní, které představuje vaši abstrakci, i když základní úložiště musí používat různé typy základních kolekcí.

Existují dvě části tohoto kódu, které nemusí být pro některé vývojáře neznámé. Tyto dvě using direktivy:

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>>;

vytvoření aliasu pro vytvořený obecný typ. Tyto příkazy umožňují kódu později použít popisnější DateMeasurements a CityDataMeasurements názvy místo obecné konstrukce Dictionary<DateTime, Measurements> a Dictionary<string, Dictionary<DateTime, Measurements> >. Tento konstruktor vyžaduje použití plně kvalifikovaných názvů typů na pravé straně znaménka = .

Druhou technikou je prokládání časových částí libovolného DateTime objektu použitého k indexování do kolekcí. .NET neobsahuje typ pouze pro datum. Vývojáři používají DateTime tento typ, ale pomocí Date vlastnosti zajistí, že se všechny DateTime objekty z daného dne budou shodovat.

Sečtením

Indexery byste měli vytvořit vždy, když máte ve třídě prvek podobný vlastnosti, kde tato vlastnost představuje ne jednu hodnotu, ale kolekci hodnot, ve kterých je každá jednotlivá položka identifikována sadou argumentů. Tyto argumenty můžou jednoznačně identifikovat, na kterou položku v kolekci se má odkazovat. Indexery rozšiřují koncept vlastností, kdy se člen považuje za datovou položku mimo třídu, ale jako metoda uvnitř. Indexery umožňují argumentům najít jednu položku ve vlastnosti, která představuje sadu položek.