Indexerare

Indexerare liknar egenskaper. Indexerare bygger på samma språkfunktioner som egenskaper på många sätt. Indexerare aktiverar indexerade egenskaper: egenskaper som refereras med ett eller flera argument. Dessa argument ger ett index i en samling värden.

Indexerarens syntax

Du kommer åt en indexerare via ett variabelnamn och hakparenteser. Du placerar indexerarargumenten inom hakparenteserna:

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

Du deklarerar indexerare med nyckelordet this som egenskapsnamn och deklarerar argumenten inom hakparenteser. Den här deklarationen skulle matcha den användning som visas i föregående stycke:

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

I det här första exemplet kan du se relationen mellan syntaxen för egenskaper och för indexerare. Den här analogi går igenom de flesta syntaxregler för indexerare. Indexerare kan ha giltiga åtkomstmodifierare (offentliga, skyddade interna, skyddade, interna, privata eller privata skyddade). De kan vara förseglade, virtuella eller abstrakta. Precis som med egenskaper kan du ange olika åtkomstmodifierare för get- och set-åtkomsterna i en indexerare. Du kan också ange skrivskyddade indexerare (genom att utelämna den inställda åtkomstorn) eller skrivskyddade indexerare (genom att utelämna get-accessorn).

Du kan använda nästan allt du lär dig av att arbeta med egenskaper för indexerare. Det enda undantaget till regeln är automatiskt implementerade egenskaper. Kompilatorn kan inte alltid generera rätt lagring för en indexerare.

Förekomsten av argument för att referera till ett objekt i en uppsättning objekt skiljer indexerare från egenskaper. Du kan definiera flera indexerare för en typ, så länge argumentlistorna för varje indexerare är unika. Nu ska vi utforska olika scenarier där du kan använda en eller flera indexerare i en klassdefinition.

Scenarier

Du definierar indexerare i din typ när dess API modellerar en samling där du definierar argumenten till den samlingen. Indexerarna kanske inte mappas direkt till de samlingstyper som ingår i .NET Core-ramverket. Din typ kan ha andra ansvarsområden utöver modellering av en samling. Med indexerare kan du ange det API som matchar din typs abstraktion utan att exponera den inre informationen om hur värdena för abstraktionen lagras eller beräknas.

Låt oss gå igenom några av de vanliga scenarierna för att använda indexerare. Du kan komma åt exempelmappen för indexerare. Instruktioner för nedladdning finns i Exempel och självstudier.

Matriser och vektorer

Ett av de vanligaste scenarierna för att skapa indexerare är när din typ modellerar en matris eller en vektor. Du kan skapa en indexerare för att modellera en ordnad lista med data.

Fördelen med att skapa en egen indexerare är att du kan definiera lagringen för samlingen så att den passar dina behov. Föreställ dig ett scenario där din typ modellerar historiska data som är för stora för att läsas in i minnet samtidigt. Du måste läsa in och ta bort delar av samlingen baserat på användning. Exemplet nedan modellerar det här beteendet. Den rapporterar om hur många datapunkter som finns. Den skapar sidor för att lagra delar av data på begäran. Det tar bort sidor från minnet för att göra plats för sidor som behövs av nyare begäranden.

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

Du kan följa det här designidomet för att modellera alla typer av samlingar där det finns goda skäl att inte läsa in hela datamängden i en minnesintern samling. Observera att Page klassen är en privat kapslad klass som inte ingår i det offentliga gränssnittet. Den här informationen är dold för alla användare av den här klassen.

Ordlistor

Ett annat vanligt scenario är när du behöver modellera en ordlista eller en karta. Det här scenariot är när din typ lagrar värden baserat på nyckel, vanligtvis textnycklar. Det här exemplet skapar en ordlista som mappar kommandoradsargument till lambda-uttryck som hanterar dessa alternativ. I följande exempel visas två klasser: en klass som mappar ett kommandoradsalternativ till ett Action ombud och en som använder ArgsActions för att köra var Action och en ArgsActionsArgsProcessor när det påträffar det alternativet.

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

I det här exemplet ArgsAction mappar samlingen nära den underliggande samlingen. Avgör get om ett visst alternativ har konfigurerats. I så fall returneras det Action som är associerat med det alternativet. Annars returnerar den en Action som inte gör någonting. Den offentliga accessorn innehåller inte någon set accessor. Designen använder i stället en offentlig metod för att ange alternativ.

Flerdimensionella Kartor

Du kan skapa indexerare som använder flera argument. Dessutom är dessa argument inte begränsade till samma typ. Låt oss titta på två exempel.

Det första exemplet visar en klass som genererar värden för en Mandelbrot-uppsättning. Mer information om matematiken bakom uppsättningen finns i den här artikeln. Indexeraren använder två dubbelgångare för att definiera en punkt i X- och Y-planet. Get-accessorn beräknar antalet iterationer tills en punkt bestäms vara i uppsättningen. Om maximala iterationer uppnås finns punkten i uppsättningen och klassens maxIterations-värde returneras. (Datorn genererade bilder som populariserats för Mandelbrot-uppsättningen definierar färger för det antal iterationer som krävs för att fastställa att en punkt ligger utanför uppsättningen.)

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

Mandelbrot-uppsättningen definierar värden vid varje (x,y) koordinat för verkliga talvärden. Det definierar en ordlista som kan innehålla ett oändligt antal värden. Därför finns det ingen lagring bakom uppsättningen. I stället beräknar den här klassen värdet för varje punkt när koden anropar get accessorn. Det finns ingen underliggande lagring som används.

Nu ska vi undersöka en sista användning av indexerare, där indexeraren tar flera argument av olika typer. Överväg ett program som hanterar historiska temperaturdata. Den här indexeraren använder en stad och ett datum för att ange eller hämta höga och låga temperaturer för den platsen:

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

I det här exemplet skapas en indexerare som mappar väderdata på två olika argument: en stad (representerad av en string) och ett datum (representerat av en DateTime). Den interna lagringen använder två Dictionary klasser för att representera den tvådimensionella ordlistan. Det offentliga API:et representerar inte längre den underliggande lagringen. I stället kan du med språkfunktionerna i indexerare skapa ett offentligt gränssnitt som representerar din abstraktion, även om den underliggande lagringen måste använda olika typer av kärnsamlingar.

Det finns två delar av den här koden som kan vara obekanta för vissa utvecklare. Dessa två using direktiv:

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

skapa ett alias för en konstruerad allmän typ. Dessa instruktioner gör det möjligt för koden senare att använda de mer beskrivande DateMeasurements och CityDataMeasurements namnen i stället för den generiska konstruktionen av Dictionary<DateTime, Measurements> och Dictionary<string, Dictionary<DateTime, Measurements> >. Den här konstruktionen kräver att du använder de fullständigt kvalificerade typnamnen till höger om = tecknet.

Den andra tekniken är att ta bort tidsdelarna för alla DateTime objekt som används för att indexera i samlingarna. .NET innehåller inte en endast datumtyp. Utvecklare använder typen DateTime , men använder Date egenskapen för att säkerställa att alla DateTime objekt från den dagen är lika.

Sammanfattningsvis

Du bör skapa indexerare när du har ett egenskapsliknande element i klassen där egenskapen inte representerar ett enda värde, utan snarare en samling värden där varje enskilt objekt identifieras av en uppsättning argument. Dessa argument kan unikt identifiera vilket objekt i samlingen som ska refereras. Indexerare utökar begreppet egenskaper, där en medlem behandlas som ett dataobjekt utanför klassen, men som en metod på insidan. Indexerare tillåter argument att hitta ett enskilt objekt i en egenskap som representerar en uppsättning objekt.