Dela via


Indexerare

Du definierar indexerare när instanser av en klass eller struct kan indexeras som en matris eller annan samling. Det indexerade värdet kan anges eller hämtas utan att uttryckligen ange en typ eller instansmedlem. Indexerare liknar egenskaper förutom att deras åtkomstorer tar parametrar.

I följande exempel definieras en allmän klass med get och set åtkomstmetoder för att tilldela och hämta värden.

namespace Indexers;

public class SampleCollection<T>
{
   // Declare an array to store the data elements.
   private T[] arr = new T[100];

   // Define the indexer to allow client code to use [] notation.
   public T this[int i]
   {
      get => arr[i];
      set => arr[i] = value;
   }
}

Föregående exempel visar en indexerare för läsning/skrivning. Den innehåller både get - och set -åtkomstpunkterna. Du kan definiera skrivskyddade indexerare som en uttryckskroppsmedlem, enligt följande exempel:

namespace Indexers;

public class ReadOnlySampleCollection<T>(params IEnumerable<T> items)
{
   // Declare an array to store the data elements.
   private T[] arr = [.. items];

   public T this[int i] => arr[i];

}

Nyckelordet get används inte; => introducerar uttryckskroppen.

Indexerare aktiverar indexerade egenskaper: egenskaper som refereras med ett eller flera argument. Dessa argument ger ett index i en samling värden.

  • Indexerare gör det möjligt att indexera objekt som liknar matriser.
  • En get accessor returnerar ett värde. En set accessor tilldelar ett värde.
  • Nyckelordet this definierar indexeraren.
  • Nyckelordet value är argumentet till set accessorn.
  • Indexerare behöver inte ett heltalsindexvärde. Det är upp till dig hur du definierar den specifika uppslagsmekanismen.
  • Indexerare kan överbelastas.
  • Indexerare kan ha en eller flera formella parametrar, till exempel vid åtkomst till en tvådimensionell matris.
  • Du kan deklarera partial indexerare i partial typer.

Du kan använda nästan allt du har lärt dig när du arbetar med egenskaper för indexerare. Det enda undantaget till den regeln är automatiskt implementerade egenskaper. Kompilatorn kan inte alltid generera rätt lagring för en indexerare. Du kan definiera flera indexerare för en typ, så länge argumentlistorna för varje indexerare är unika.

Användning av indexerare

Du definierar indexerare i din typ när dess API modellerar en samling. Indexeraren behöver inte mappas direkt till de samlingstyper som ingår i .NET Core-ramverket. 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.

Matriser och vektorer

Din typ kan modellera en matris eller en vektor. 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 lasta av 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.

namespace Indexers;

public record Measurements(double HiTemp, double LoTemp, double AirPressure);

public class DataSamples
{
    private class Page
    {
        private readonly List<Measurements> pageData = new ();
        private readonly int _startingIndex;
        private readonly int _length;

        public Page(int startingIndex, int length)
        {
            _startingIndex = startingIndex;
            _length = length;

            // 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 { get; private set; } = false;
        public DateTime LastAccess { get; set; } = DateTime.Now;
    }

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

    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 användare av den här klassen.

Ordböcker

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, eventuellt 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 ArgsActions-klass som mappar ett kommandoradsalternativ till en System.Action delegering och en ArgsProcessor som använder ArgsActions för att köra varje Action när det påträffar det alternativet.

namespace Indexers;
public class ArgsProcessor
{
    private readonly ArgsActions _actions;

    public ArgsProcessor(ArgsActions actions)
    {
        _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();

    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 stämmer samlingen överens med den underliggande samlingen. get avgör om ett visst alternativ är konfigurerat. 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 saknar en 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.

I följande exempel visas 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. Accessorn get beräknar antalet iterationer tills en punkt avgörs inte vara i uppsättningen. När det maximala antalet iterationer har nåtts är punkten i uppsättningen och klassens maxIterations-värde returneras. Datorgenererade bilder som populariserats för Mandelbrotmängden definierar färger för det antal iterationer som krävs för att fastställa att en punkt ligger utanför mängden.

namespace Indexers;
public class Mandelbrot(int 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))
            { 
                (x, y) = (x * x - y * y + x0, 2 * x * y + y0);
                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 inget lagringsutrymme bakom enheten. 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.

Sammanfattningsvis

Du skapar indexerare när du har ett egenskapsliknande element i klassen där egenskapen inte representerar ett enda värde, utan snarare en uppsättning värden. Ett eller flera argument identifierar varje enskilt objekt. Dessa argument kan unikt identifiera vilket objekt i uppsättningen 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.

Du kan komma åt exempelmappen för indexerare. Instruktioner för nedladdning finns i Exempelfiler och Handledningar.

Språkspecifikation för C#

Mer information finns i Indexerare i C#-språkspecifikationen. Språkspecifikationen är den slutgiltiga källan för C#-syntax och -användning.