Compartilhar via


Indexadores

Você define indexadores quando instâncias de uma classe ou struct podem ser indexadas como uma matriz ou outra coleção. O valor indexado pode ser definido ou recuperado sem especificar explicitamente um tipo ou membro de instância. Os indexadores se assemelham a propriedades, exceto pelo fato de que seus acessadores recebem parâmetros.

O exemplo a seguir define uma classe genérica com métodos de get e set simples para atribuir e recuperar valores.

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

O exemplo anterior mostra um indexador de leitura/gravação. Ele contém os acessadores get e set. Você pode definir indexadores somente leitura como um membro de expressão encorpado, conforme mostrado nos seguintes exemplos:

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

}

A get palavra-chave não é usada; => introduz o corpo da expressão.

Os indexadores habilitam propriedades indexadas : propriedades referenciadas usando um ou mais argumentos. Esses argumentos fornecem um índice em algumas coleções de valores.

  • Os indexadores permitem que os objetos sejam indexados de forma semelhante às matrizes.
  • Um get acessador retorna um valor. Um set acessador atribui um valor.
  • A this palavra-chave define o indexador.
  • A value palavra-chave é o argumento para o set acessador.
  • Os indexadores não exigem um valor de índice inteiro; cabe a você definir o mecanismo de pesquisa específico.
  • Os indexadores podem ser sobrecarregados.
  • Os indexadores podem ter um ou mais parâmetros formais, por exemplo, ao acessar uma matriz bidimensional.
  • Você pode declarar partial indexadores em partial tipos.

Você pode aplicar quase tudo o que aprendeu trabalhando com propriedades aos indexadores. A única exceção a essa regra é as propriedades implementadas automaticamente. Nem sempre o compilador pode gerar o armazenamento correto para um indexador. Você pode definir vários indexadores em um tipo, desde que as listas de argumentos para cada indexador sejam exclusivas.

Usos de indexadores

Você define indexadores em seu tipo quando sua API modela alguma coleção. Seu indexador não é obrigatório para mapear diretamente para os tipos de coleção que fazem parte da estrutura principal do .NET. Os indexadores permitem que você forneça a API que corresponde à abstração do tipo sem expor os detalhes internos de como os valores dessa abstração são armazenados ou computados.

Matrizes e vetores

Seu tipo pode modelar uma matriz ou um vetor. A vantagem de criar seu próprio indexador é que você pode definir o armazenamento dessa coleção para atender às suas necessidades. Imagine um cenário em que seu tipo modela dados históricos grandes demais para serem carregados de uma só vez na memória. Você precisa carregar e descarregar seções da coleção com base no uso. O exemplo a seguir modela esse comportamento. Ele relata 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.

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

Você pode seguir esse idioma de design para modelar qualquer tipo de coleção em que haja bons motivos 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 estão ocultos dos usuários dessa classe.

Dicionários

Outro cenário comum é quando você precisa modelar um dicionário ou um mapa. Este cenário ocorre quando seu tipo armazena valores com base em chave, possivelmente em 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 System.Action delegado, e uma ArgsProcessor que usa a ArgsActions para executar cada Action quando encontra essa opção.

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

Neste exemplo, a coleção ArgsAction mapeia próximo à coleção subjacente. get determina se uma opção determinada está configurada. Em caso afirmativo, ele retorna a opção Action associada. Caso contrário, retorna um Action que não faz nada. O acessador público não inclui um acessador set. 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 são restritos a serem do mesmo tipo.

O exemplo a seguir mostra uma classe que gera valores para um conjunto mandelbrot. Para obter 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 e Y. O get acessador calcula o número de iterações até que um ponto seja determinado como não no conjunto. Quando o número máximo de iterações é atingido, o ponto está no conjunto e o valor maxIterations da classe é retornado. (As imagens geradas pelo computador popularizadas para o conjunto Mandelbrot definem cores para o número de iterações necessárias para determinar que um ponto está fora do conjunto.)

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

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

Resumindo

Você cria indexadores sempre que tiver um elemento semelhante a uma propriedade em sua classe em que essa propriedade representa não um único valor, mas sim um conjunto de valores. Um ou mais argumentos identificam cada item individual. Esses argumentos podem identificar exclusivamente qual item no conjunto deve ser referenciado. Os indexadores estendem o conceito de propriedades, em que um membro é tratado como um item de dados externamente à classe, mas como um método internamente. Os indexadores permitem que os argumentos encontrem um único item em uma propriedade que representa um conjunto de itens.

Você pode acessar a pasta de exemplo para indexadores. Para obter instruções de download, consulte Exemplos e Tutoriais.

Especificação da linguagem C#

Para obter mais informações, consulte Indexadores na Especificação da Linguagem C#. A especificação de idioma é a fonte definitiva para a sintaxe e o uso de C#.