Condividi tramite


Indicizzatori

Gli indicizzatori vengono definiti quando le istanze di una classe o di uno struct possono essere indicizzate come una matrice o un'altra raccolta. Il valore indicizzato può essere impostato o recuperato senza specificare in modo esplicito un tipo o un membro dell'istanza. Gli indicizzatori sono simili alle proprietà , ad eccezione del fatto che le funzioni di accesso accettano parametri.

Nell'esempio seguente viene definita una classe generica con get metodi di accesso e set per assegnare e recuperare valori.

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

L'esempio precedente mostra un indicizzatore di lettura/scrittura. Contiene sia le funzioni di accesso get che set. È possibile definire indicizzatori di sola lettura come membro con corpo di espressione, come illustrato negli esempi seguenti:

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

}

La get parola chiave non viene usata. => Introduce il corpo dell'espressione.

Gli indicizzatori abilitano le proprietà indicizzate : proprietà a cui si fa riferimento usando uno o più argomenti. Questi argomenti forniscono un indice in una raccolta di valori.

  • Gli indicizzatori consentono di indicizzare gli oggetti in modo simile alle matrici.
  • Un get accessore restituisce un valore. Un set accessor assegna un valore.
  • La this parola chiave definisce l'indicizzatore.
  • La value parola chiave è l'argomento per l'set accessore.
  • Gli indicizzatori non richiedono un valore di indice intero; spetta a te definire il meccanismo di ricerca specifico.
  • Gli indicizzatori possono essere sovraccaricati.
  • Gli indicizzatori possono avere uno o più parametri formali, ad esempio quando si accede a una matrice bidimensionale.
  • È possibile dichiarare partial indicizzatori nei partial tipi.

È possibile applicare quasi tutti gli elementi appresi dall'uso delle proprietà agli indicizzatori. L'unica eccezione a tale regola sono le proprietà implementate automaticamente. Il compilatore non può sempre generare l'archiviazione corretta per un indicizzatore. È possibile definire più indicizzatori in un tipo, purché gli elenchi di argomenti per ogni indicizzatore sia univoco.

Usi degli indicizzatori

Definisci gli indicizzatori nel tuo tipo quando la sua API modella una raccolta. L'indicizzatore non deve mappare direttamente i tipi di raccolta che fanno parte del framework .NET Core. Gli indicizzatori consentono di fornire l'API che corrisponde all'astrazione del tipo senza esporre i dettagli interni su come vengono archiviati o calcolati i valori per tale astrazione.

Matrici e vettori

Il tipo potrebbe modellare una matrice o un vettore. Il vantaggio della creazione di un indicizzatore è che è possibile definire lo spazio di archiviazione per tale raccolta in base alle proprie esigenze. Immagina uno scenario in cui il tuo modello di tipo rappresenta dati storici che sono troppo grandi per essere caricati in memoria tutti in una volta. È necessario caricare e scaricare sezioni della raccolta in base all'utilizzo. L'esempio seguente modella questo comportamento. Segnala il numero di punti dati esistenti. Crea pagine in cui contenere sezioni dei dati su richiesta. Rimuove le pagine dalla memoria per liberare spazio per le pagine necessarie da richieste più recenti.

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

È possibile seguire questo linguaggio di progettazione per modellare qualsiasi tipo di raccolta in cui esistono buoni motivi per non caricare l'intero set di dati in una raccolta in memoria. Si noti che la Page classe è una classe annidata privata che non fa parte dell'interfaccia pubblica. Questi dettagli sono nascosti agli utenti di questa classe.

Dizionari

Un altro scenario comune è quando è necessario modellare un dizionario o una mappa. Questo scenario si verifica quando il tipo archivia i valori in base alla chiave, possibilmente alle chiavi di testo. In questo esempio viene creato un dizionario che esegue il mapping degli argomenti della riga di comando alle espressioni lambda che gestiscono tali opzioni. L'esempio seguente mostra due classi: una ArgsActions classe che mappa un'opzione della riga di comando a un System.Action delegato, e una classe ArgsProcessor che usa ArgsActions per eseguire ogni Action quando rileva tale opzione.

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

In questo esempio, la ArgsAction collezione corrisponde strettamente alla collezione sottostante. get determina se un'opzione è configurata. In tal caso, restituisce l'oggetto Action associato a quell'opzione. In caso contrario, restituisce un oggetto Action che non esegue alcuna operazione. La funzione di accesso pubblica non include un set accessor. La progettazione usa invece un metodo pubblico per impostare le opzioni.

Indicizzatori basati su data

Quando si usano dati basati su data, è possibile usare o DateTimeDateOnly come chiavi dell'indicizzatore. Usare DateOnly quando è necessaria solo la parte della data e si desidera evitare complicazioni correlate al tempo. L'esempio seguente mostra un sistema di rilevamento della temperatura che usa DateOnly come chiave dell'indicizzatore primario:

using System;
using System.Collections.Generic;

namespace Indexers;

public class DailyTemperatureData
{
    private readonly Dictionary<DateOnly, (double High, double Low)> _temperatureData = new();

    // Indexer using DateOnly for date-only scenarios
    public (double High, double Low) this[DateOnly date]
    {
        get
        {
            if (_temperatureData.TryGetValue(date, out var temp))
            {
                return temp;
            }
            throw new KeyNotFoundException($"No temperature data available for {date:yyyy-MM-dd}");
        }
        set
        {
            _temperatureData[date] = value;
        }
    }

    // Overload using DateTime for convenience, but only uses the date part
    public (double High, double Low) this[DateTime dateTime]
    {
        get => this[DateOnly.FromDateTime(dateTime)];
        set => this[DateOnly.FromDateTime(dateTime)] = value;
    }

    public bool HasDataFor(DateOnly date) => _temperatureData.ContainsKey(date);

    public IEnumerable<DateOnly> AvailableDates => _temperatureData.Keys;
}

In questo esempio vengono illustrati sia DateOnlyDateTime gli indicizzatori che gli indicizzatori. Mentre l'indicizzatore è l'interfaccia DateOnly primaria, l'overload DateTime offre praticità estraendo solo la parte della data. Questo approccio garantisce che tutti i dati relativi alla temperatura per un determinato giorno vengano trattati in modo coerente, indipendentemente dal componente dell'ora.

Mappe multidimensionali

È possibile creare indicizzatori che usano più argomenti. Inoltre, questi argomenti non devono necessariamente essere dello stesso tipo.

Nell'esempio seguente viene illustrata una classe che genera valori per un set di Mandelbrot. Per altre informazioni sulla matematica dietro il set, leggere questo articolo. L'indicizzatore usa due valori double per definire un punto nel piano X, Y. La get funzione di accesso calcola il numero di iterazioni fino a quando un punto non viene determinato come non incluso nel set. Quando viene raggiunto il numero massimo di iterazioni, il punto si trova nel set e viene restituito il valore maxIterations della classe. Le immagini generate al computer più comuni per il set di Mandelbrot attribuiscono colori al numero di iterazioni necessarie per determinare che un punto si trova al di fuori del set.

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

Il set Di Mandelbrot definisce i valori a ogni coordinata (x,y) per i valori numerici reali. Che definisce un dizionario che potrebbe contenere un numero infinito di valori. Di conseguenza, non c'è spazio di archiviazione dietro il set. Questa classe calcola invece il valore per ogni punto quando il codice chiama la get funzione di accesso. Non viene usata alcuna risorsa di archiviazione sottostante.

Riassumendo

Gli indicizzatori vengono creati ogni volta che si dispone di un elemento simile a una proprietà nella classe in cui tale proprietà rappresenta non un singolo valore, ma piuttosto un set di valori. Uno o più argomenti identificano ogni singolo elemento. Tali argomenti possono identificare in modo univoco l'elemento del set a cui fare riferimento. Gli indicizzatori estendono il concetto di proprietà, in cui un membro viene considerato come un elemento di dati dall'esterno della classe, ma come un metodo all'interno. Gli indicizzatori consentono agli argomenti di trovare un singolo elemento in una proprietà che rappresenta un set di elementi.

È possibile accedere alla cartella di esempio per gli indicizzatori. Per istruzioni sul download, vedere esempi ed esercitazioni .

Specifiche del linguaggio C#

Per altre informazioni, vedere Indicizzatori nella specifica del linguaggio C#. La specifica del linguaggio costituisce il riferimento ufficiale principale per la sintassi e l'uso di C#.