Usando indexadores (Guia de Programação em C#)

Os indexadores são uma conveniência sintática que permite criar uma classe, um struct ou uma interface que os aplicativos clientes podem acessar como uma matriz. O compilador gera uma propriedade Item (ou uma propriedade de nome alternativo, se IndexerNameAttribute estiver presente) e os métodos acessadores apropriados. Os indexadores são implementados em tipos cuja principal finalidade é encapsular uma coleção ou matriz interna. Por exemplo, suponha que você tenha uma classe TempRecord que representa a temperatura em Fahrenheit, conforme registrada em 10 momentos diferentes durante um período de 24 horas. A classe contém uma matriz temps do tipo float[] para armazenar os valores de temperatura. Ao implementar um indexador nessa classe, os clientes podem acessar as temperaturas em uma instância TempRecord como float temp = tempRecord[4], e não como float temp = tempRecord.temps[4]. A notação do indexador não apenas simplifica a sintaxe para aplicativos clientes, mas também torna a classe e a finalidade dela mais intuitivas para que os outros desenvolvedores entendam.

Para declarar um indexador em uma classe ou struct, use a palavra-chave this, como mostra o seguinte exemplo:

// Indexer declaration
public int this[int index]
{
    // get and set accessors
}

Importante

Declarar um indexador vai gerar automaticamente uma propriedade chamada Item no objeto. A propriedade Item não pode ser acessada diretamente por meio da expressão de acesso a membro da instância. Além disso, se você adicionar a sua propriedade Item a um objeto com indexador, será gerado um erro do compilador CS0102. Para evitar esse erro, use a opção de IndexerNameAttribute renomear o indexador, conforme detalhado mais para frente neste artigo.

Comentários

O tipo de um indexador e o tipo dos seus parâmetros devem ser pelo menos tão acessíveis quanto o próprio indexador. Para obter mais informações sobre níveis de acessibilidade, consulte Modificadores de acesso.

Para obter mais informações sobre como usar indexadores com uma interface, consulte Indexadores de Interface.

A assinatura de um indexador consiste do número e dos tipos de seus parâmetros formais. Ela não inclui o tipo de indexador nem os nomes dos parâmetros formais. Se você declarar mais de um indexador na mesma classe, eles terão diferentes assinaturas.

Um indexador não é classificado como uma variável; portanto, um valor de indexador não pode ser passado por referência (como um parâmetro ref ou out), a menos que seu valor seja uma referência (ou seja, ele retorna por referência).

Para fornecer o indexador com um nome que outras linguagens possam usar, use System.Runtime.CompilerServices.IndexerNameAttribute, como mostra o seguinte exemplo:

// Indexer declaration
[System.Runtime.CompilerServices.IndexerName("TheItem")]
public int this[int index]
{
    // get and set accessors
}

Esse indexador tem o nome TheItem, pois ele é substituído pelo atributo de nome do indexador. Por padrão, o nome do indexador é Item.

Exemplo 1

O exemplo a seguir mostra como declarar um campo de matriz privada, temps e um indexador. O indexador permite acesso direto à instância tempRecord[i]. A alternativa ao uso do indexador é declarar a matriz como um membro público e acessar seus membros, tempRecord.temps[i], diretamente.

public class TempRecord
{
    // Array of temperature values
    float[] temps =
    [
        56.2F, 56.7F, 56.5F, 56.9F, 58.8F,
        61.3F, 65.9F, 62.1F, 59.2F, 57.5F
    ];

    // To enable client code to validate input
    // when accessing your indexer.
    public int Length => temps.Length;
    
    // Indexer declaration.
    // If index is out of range, the temps array will throw the exception.
    public float this[int index]
    {
        get => temps[index];
        set => temps[index] = value;
    }
}

Observe que, quando o acesso de um indexador é avaliado, por exemplo, em uma instrução Console.Write, o acessador get é invocado. Portanto, se não existir nenhum acessador get, ocorrerá um erro em tempo de compilação.

var tempRecord = new TempRecord();

// Use the indexer's set accessor
tempRecord[3] = 58.3F;
tempRecord[5] = 60.1F;

// Use the indexer's get accessor
for (int i = 0; i < 10; i++)
{
    Console.WriteLine($"Element #{i} = {tempRecord[i]}");
}

Indexando usando outros valores

O C# não limita o tipo de parâmetro do indexador ao inteiro. Por exemplo, pode ser útil usar uma cadeia de caracteres com um indexador. Esse indexador pode ser implementado pesquisando a cadeia de caracteres na coleção e retornando o valor adequado. Como os acessadores podem ser sobrecarregados, as versões do inteiro e da cadeia de caracteres podem coexistir.

Exemplo 2

O exemplo a seguir declara uma classe que armazena os dias da semana. Um acessador get aceita uma cadeia de caracteres, o nome de um dia e retorna o inteiro correspondente. Por exemplo, "Sunday" retorna 0, "Monday" retorna 1 e assim por diante.

// Using a string as an indexer value
class DayCollection
{
    string[] days = ["Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat"];

    // Indexer with only a get accessor with the expression-bodied definition:
    public int this[string day] => FindDayIndex(day);

    private int FindDayIndex(string day)
    {
        for (int j = 0; j < days.Length; j++)
        {
            if (days[j] == day)
            {
                return j;
            }
        }

        throw new ArgumentOutOfRangeException(
            nameof(day),
            $"Day {day} is not supported.\nDay input must be in the form \"Sun\", \"Mon\", etc");
    }
}

Exemplo de consumo 2

var week = new DayCollection();
Console.WriteLine(week["Fri"]);

try
{
    Console.WriteLine(week["Made-up day"]);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine($"Not supported input: {e.Message}");
}

Exemplo 3

O exemplo a seguir declara uma classe que armazena os dias da semana usando a enumeração System.DayOfWeek. Um acessador get aceita DayOfWeek, o valor de um dia, e retorna o inteiro correspondente. Por exemplo, DayOfWeek.Sunday retorna 0, DayOfWeek.Monday retorna 1 e assim por diante.

using Day = System.DayOfWeek;

class DayOfWeekCollection
{
    Day[] days =
    [
        Day.Sunday, Day.Monday, Day.Tuesday, Day.Wednesday,
        Day.Thursday, Day.Friday, Day.Saturday
    ];

    // Indexer with only a get accessor with the expression-bodied definition:
    public int this[Day day] => FindDayIndex(day);

    private int FindDayIndex(Day day)
    {
        for (int j = 0; j < days.Length; j++)
        {
            if (days[j] == day)
            {
                return j;
            }
        }
        throw new ArgumentOutOfRangeException(
            nameof(day),
            $"Day {day} is not supported.\nDay input must be a defined System.DayOfWeek value.");
    }
}

Exemplo de consumo 3

var week = new DayOfWeekCollection();
Console.WriteLine(week[DayOfWeek.Friday]);

try
{
    Console.WriteLine(week[(DayOfWeek)43]);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine($"Not supported input: {e.Message}");
}

Programação robusta

Há duas maneiras principais nas quais a segurança e a confiabilidade de indexadores podem ser melhoradas:

  • Certifique-se de incorporar algum tipo de estratégia de tratamento de erros para manipular a chance de passagem de código cliente em um valor de índice inválido. Anteriormente, no primeiro exemplo neste artigo, a classe TempRecord oferece uma propriedade Length que permite que o código cliente verifique a saída antes de passá-la para o indexador. Também é possível colocador o código de tratamento de erro dentro do próprio indexador. Certifique-se documentar para os usuários as exceções que você gera dentro de um acessador do indexador.

  • Defina a acessibilidade dos acessadores get e set para que ela seja mais restritiva possível. Isso é importante para o acessador set em particular. Para obter mais informações, consulte Restringindo a acessibilidade aos acessadores.

Confira também