Uso de indizadores (Guía de programación de C#)

Los indizadores son una comodidad sintáctica que le permiten crear una clase, estructura o interfaz a la que las aplicaciones cliente pueden acceder como una matriz. El compilador generará una propiedad Item (o una propiedad con otro nombre si está presente IndexerNameAttribute) y los métodos de descriptor de acceso adecuados. Los indexadores se implementan con más frecuencia en tipos cuyo propósito principal consiste en encapsular una matriz o colección interna. Por ejemplo, imagine que tiene una clase TempRecord que representa la temperatura en grados Fahrenheit que se registra en 10 momentos diferentes durante un período de 24 horas. La clase contiene una matriz temps de tipo float[] para almacenar los valores de temperatura. Si implementa un indizador en esta clase, los clientes pueden tener acceso a las temperaturas en una instancia de TempRecord como float temp = tempRecord[4] en lugar de como float temp = tempRecord.temps[4]. La notación del indizador no solo simplifica la sintaxis para las aplicaciones cliente; también hace que la clase y su finalidad sean más intuitivas para que otros desarrolladores las entiendan.

Para declarar un indizador en una clase o un struct, use la palabra clave this, como en este ejemplo:

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

Importante

Al declarar un indizador, se generará automáticamente una propiedad denominada Item en el objeto. No se puede acceder directamente a la propiedad Item desde la instancia de expresión de acceso a miembros. Además, si agrega una propiedad Item propia a un objeto con un indizador, obtendrá un error del compilador CS0102. Para evitar este error, use IndexerNameAttribute para cambiar el nombre del indizador como se detalla a continuación.

Comentarios

Los tipos de un indexador y de sus parámetros deben ser al menos igual de accesibles que el propio indexador. Para obtener más información sobre los niveles de accesibilidad, vea Modificadores de acceso.

Para obtener más información sobre cómo usar los indexadores con una interfaz, vea Indizadores en interfaces.

La firma de un indexador consta del número y los tipos de sus parámetros formales. No incluye el tipo de indizador ni los nombres de los parámetros formales. Si declara más de un indexador en la misma clase, deben tener firmas diferentes.

Un valor de indexador no está clasificado como una variable; por tanto, no se puede pasar un valor de indexador como un parámetro ref u out.

Para proporcionar el indizador con un nombre que puedan usar otros lenguajes, use System.Runtime.CompilerServices.IndexerNameAttribute, como se muestra en el ejemplo siguiente:

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

Este indizador tendrá el nombre TheItem, ya que lo reemplaza el atributo de nombre del indizador. De forma predeterminada, el nombre del indizador es Item.

Ejemplo 1

En el ejemplo siguiente, se muestra cómo declarar un campo de matriz privada, temps, como un indexador. El indexador permite el acceso directo a la instancia tempRecord[i]. La alternativa a usar el indexador es declarar la matriz como un miembro public y tener acceso directamente a sus miembros tempRecord.temps[i].

public class TempRecord
{
    // Array of temperature values
    float[] temps = new float[10]
    {
        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;
    }
}

Tenga en cuenta que, cuando se evalúa el acceso de un indexador (por ejemplo, en una instrucción Console.Write), se invoca al descriptor de acceso get. Por tanto, si no hay ningún descriptor de acceso get, se produce un error en tiempo de compilación.

class Program
{
    static void Main()
    {
        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]}");
        }

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
    /* Output:
        Element #0 = 56.2
        Element #1 = 56.7
        Element #2 = 56.5
        Element #3 = 58.3
        Element #4 = 58.8
        Element #5 = 60.1
        Element #6 = 65.9
        Element #7 = 62.1
        Element #8 = 59.2
        Element #9 = 57.5
    */
}

Indexación con otros valores

C# no limita el tipo de parámetro indizador a un entero. Por ejemplo, puede ser útil usar una cadena con un indexador. Este tipo de indexador podría implementarse al buscar la cadena de la colección y devolver el valor adecuado. Como los descriptores de acceso se pueden sobrecargar, pueden coexistir las versiones de cadena y entero.

Ejemplo 2

En el ejemplo siguiente se declara una clase que almacena los días de la semana. Un descriptor de acceso get toma una cadena, el nombre de un día, y devuelve el entero correspondiente. Por ejemplo, "Sunday" devuelve 0, "Monday" devuelve 1 y así sucesivamente.

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

Ejemplo de consumo 2

class Program
{
    static void Main(string[] args)
    {
        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}");
        }
    }
    // Output:
    // 5
    // Not supported input: Day Made-up day is not supported.
    // Day input must be in the form "Sun", "Mon", etc (Parameter 'day')
}

Ejemplo 3

En el ejemplo siguiente se declara una clase que almacena los días de la semana mediante la enumeración System.DayOfWeek. Un descriptor de acceso get toma un valor DayOfWeek, el nombre de un día, y devuelve el entero correspondiente. Por ejemplo, DayOfWeek.Sunday devuelve 0, DayOfWeek.Monday devuelve 1, y así sucesivamente.

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

Ejemplo de consumo 3

class Program
{
    static void Main()
    {
        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}");
        }
    }
    // Output:
    // 5
    // Not supported input: Day 43 is not supported.
    // Day input must be a defined System.DayOfWeek value. (Parameter 'day')
}

Programación sólida

Hay dos formas principales en que se pueden mejorar la seguridad y confiabilidad de los indexadores:

  • Asegúrese de incorporar algún tipo de estrategia de control de errores para controlar la posibilidad de que el código de cliente pase un valor de índice no válido. En el primer ejemplo de este tema, la clase TempRecord proporciona una propiedad Length que permite al código de cliente comprobar la entrada antes de pasarla al indexador. También puede colocar el código de control de errores en el propio indexador. Asegúrese de documentar para los usuarios cualquier excepción que se produzca dentro de un descriptor de acceso del indexador.

  • Establezca la accesibilidad de los descriptores de acceso get and set para que sea tan restrictiva como razonable. Esto es importante para el descriptor de acceso set en particular. Para más información, vea Restringir la accesibilidad del descriptor de acceso.

Vea también