Indexadores

Os indexadores são semelhantes às propriedades. De muitas maneiras, os indexadores se baseiam nos mesmos recursos de linguagem que as propriedades. Os indexadores habilitam propriedades indexadas : propriedades referenciadas usando um ou mais argumentos. Esses argumentos fornecem um índice em alguma coleção de valores.

Sintaxe do indexador

Você acessa um indexador por meio de um nome de variável e colchetes. Coloque os argumentos do indexador entre parênteses:

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

Você declara indexadores usando a this palavra-chave como o nome da propriedade e declarando os argumentos entre colchetes. Esta declaração corresponderia ao uso mostrado no parágrafo anterior:

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

Neste exemplo inicial, você pode ver a relação entre a sintaxe para propriedades e para indexadores. Esta analogia realiza a maioria das regras de sintaxe para indexadores. Os indexadores podem ter qualquer modificador de acesso válido (público, protegido interno, protegido, interno, privado ou privado protegido). Podem ser selados, virtuais ou abstratos. Assim como acontece com as propriedades, você pode especificar diferentes modificadores de acesso para os acessadores get e set em um indexador. Você também pode especificar indexadores somente leitura (omitindo o acessador definido) ou indexadores somente gravação (omitindo o acessador get).

Você pode aplicar quase tudo o que aprende trabalhando com propriedades a indexadores. A única exceção a essa regra são as propriedades implementadas automaticamente. O compilador nem sempre pode gerar o armazenamento correto para um indexador.

A presença de argumentos para fazer referência a um item em um conjunto de itens distingue indexadores de propriedades. Você pode definir vários indexadores em um tipo, desde que as listas de argumentos para cada indexador sejam exclusivas. Vamos explorar diferentes cenários onde você pode usar um ou mais indexadores em uma definição de classe.

Cenários

Você definiria indexadores em seu tipo quando sua API modelasse alguma coleção onde você definisse os argumentos para essa coleção. Seus indexadores podem ou não mapear diretamente para os tipos de coleção que fazem parte da estrutura principal do .NET. Seu tipo pode ter outras responsabilidades além de modelar uma coleção. Os indexadores permitem que você forneça a API que corresponde à abstração do seu tipo sem expor os detalhes internos de como os valores dessa abstração são armazenados ou calculados.

Vamos percorrer alguns dos cenários comuns para usar indexadores. Você pode acessar a pasta de exemplo para indexadores. Para obter instruções de download, consulte Exemplos e tutoriais.

Matrizes e vetores

Um dos cenários mais comuns para criar indexadores é quando seu tipo modela uma matriz ou um vetor. Você pode criar um indexador para modelar uma lista ordenada de dados.

A vantagem de criar seu próprio indexador é que você pode definir o armazenamento para essa coleção para atender às suas necessidades. Imagine um cenário em que seu tipo modela dados históricos que são muito grandes para serem carregados na memória de uma só vez. Você precisa carregar e descarregar seções da coleção com base no uso. O exemplo a seguir modela esse comportamento. Ele informa quantos pontos de dados existem. Ele cria páginas para armazenar seções dos dados sob demanda. Ele remove páginas da memória para abrir espaço para páginas necessárias para solicitações mais recentes.

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

Você pode seguir esta linguagem de design para modelar qualquer tipo de coleção onde há boas razões para não carregar todo o conjunto de dados em uma coleção na memória. Observe que a Page classe é uma classe aninhada privada que não faz parte da interface pública. Esses detalhes são ocultos de todos os usuários dessa classe.

Dicionários

Outro cenário comum é quando você precisa modelar um dicionário ou um mapa. Este cenário é quando o seu tipo armazena valores com base na chave, normalmente chaves de texto. Este exemplo cria um dicionário que mapeia argumentos de linha de comando para expressões lambda que gerenciam essas opções. O exemplo a seguir mostra duas classes: uma ArgsActions classe que mapeia uma opção de linha de comando para um Action delegado e uma ArgsProcessor que usa o ArgsActions para executar cada Action um quando encontra essa opção.

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

Neste exemplo, a coleção é mapeada ArgsAction de perto para a coleção subjacente. O get determina se uma determinada opção foi configurada. Em caso afirmativo, ele retorna o Action associado a essa opção. Se não, devolve um Action que não faz nada. O acessador público não inclui um set acessante. Em vez disso, o design está usando um método público para definir opções.

Mapas Multidimensionais

Você pode criar indexadores que usam vários argumentos. Além disso, esses argumentos não se limitam a ser do mesmo tipo. Vejamos dois exemplos.

O primeiro exemplo mostra uma classe que gera valores para um conjunto de Mandelbrot. Para mais informações sobre a matemática por trás do conjunto, leia este artigo. O indexador usa dois duplos para definir um ponto no plano X, Y. O acessador get calcula o número de iterações até que um ponto seja determinado como não estando no conjunto. Se as iterações máximas forem atingidas, o ponto estará no conjunto e o valor maxIterations da classe será retornado. (As imagens geradas por computador popularizadas para o conjunto de Mandelbrot definem cores para o número de iterações necessárias para determinar que um ponto está fora do conjunto.)

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

O conjunto de Mandelbrot define valores em cada coordenada (x,y) para valores de números reais. Isso define um dicionário que pode conter um número infinito de valores. Portanto, não há armazenamento por trás do conjunto. Em vez disso, essa classe calcula o valor para cada ponto quando o código chama o get acessador. Não há armazenamento subjacente usado.

Vamos examinar um último uso de indexadores, onde o indexador usa vários argumentos de diferentes tipos. Considere um programa que gerencie dados históricos de temperatura. Este indexador usa uma cidade e uma data para definir ou obter as altas e baixas temperaturas para esse local:

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

Este exemplo cria um indexador que mapeia dados meteorológicos em dois argumentos diferentes: uma cidade (representada por um string) e uma data (representada por um DateTime). O armazenamento interno usa duas Dictionary classes para representar o dicionário bidimensional. A API pública não representa mais o armazenamento subjacente. Em vez disso, os recursos de linguagem dos indexadores permitem que você crie uma interface pública que represente sua abstração, mesmo que o armazenamento subjacente deva usar diferentes tipos de coleção principal.

Há duas partes desse código que podem ser desconhecidas para alguns desenvolvedores. Estas duas using diretivas:

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

Crie um alias para um tipo genérico construído. Essas instruções permitem que o código mais tarde use o mais descritivo DateMeasurements e CityDataMeasurements nomes em vez da construção genérica de Dictionary<DateTime, Measurements> e Dictionary<string, Dictionary<DateTime, Measurements> >. Essa construção requer o uso de nomes de tipo totalmente qualificados no lado direito do = sinal.

A segunda técnica é retirar as porções de tempo de qualquer DateTime objeto usado para indexar nas coleções. O .NET não inclui um tipo somente data. Os desenvolvedores usam o DateTime tipo, mas usam a Date propriedade para garantir que qualquer DateTime objeto daquele dia seja igual.

Resumindo

Você deve criar indexadores sempre que tiver um elemento semelhante a uma propriedade em sua classe, onde essa propriedade representa não um único valor, mas sim uma coleção de valores onde cada item individual é identificado por um conjunto de argumentos. Esses argumentos podem identificar exclusivamente qual item da coleção deve ser referenciado. Os indexadores estendem o conceito de propriedades, onde um membro é tratado como um item de dados de fora da classe, mas como um método interno. Os indexadores permitem que os argumentos encontrem um único item em uma propriedade que representa um conjunto de itens.