Operadores y expresiones de acceso a miembros: los operadores de punto, indexadores y de invocación.

Se usan varios operadores y expresiones para acceder a un miembro de tipo. Estos operadores incluyen el acceso a miembros (.), el elemento de matriz o el acceso al indexador ([]), el índice desde el extremo (), el intervalo (..^), los operadores condicionales NULL (?. y ?[]) y la invocación del método (()). Entre ellos se incluyen los operadores de acceso a miembros condicionales NULL (?.) y acceso al indexador (?[]).

Expresión de acceso a miembros .

Use el token . para acceder a un miembro de un espacio de nombres o un tipo, como se muestran en los ejemplos siguientes:

  • Use . para acceder a un espacio de nombres anidado dentro de un espacio de nombres, como se muestra en el siguiente ejemplo de una directiva using:
using System.Collections.Generic;
  • Use . para formar un nombre completo para tener acceso a un tipo dentro de un espacio de nombres, como se muestra en el código siguiente:
System.Collections.Generic.IEnumerable<int> numbers = [1, 2, 3];

Utilice una directiva using para hacer que el uso de nombres completos sea opcional.

  • Use . para acceder a los miembros del tipo, estáticos y no estáticos, tal como muestra el siguiente código:
List<double> constants =
[
    Math.PI,
    Math.E
];
Console.WriteLine($"{constants.Count} values to show:");
Console.WriteLine(string.Join(", ", constants));
// Output:
// 2 values to show:
// 3.14159265358979, 2.71828182845905

También puede usar . para acceder a un método de extensión.

Operador del indizador []

Los corchetes, [], se suelen usar para el acceso a matriz, indizador o elemento de puntero. A partir de C# 12, [] incluye una expresión de colección.

Acceso a matriz

En el ejemplo siguiente se muestra cómo se obtiene acceso a los elementos de matriz:

int[] fib = new int[10];
fib[0] = fib[1] = 1;
for (int i = 2; i < fib.Length; i++)
{
    fib[i] = fib[i - 1] + fib[i - 2];
}
Console.WriteLine(fib[fib.Length - 1]);  // output: 55

double[,] matrix = new double[2,2];
matrix[0,0] = 1.0;
matrix[0,1] = 2.0;
matrix[1,0] = matrix[1,1] = 3.0;
var determinant = matrix[0,0] * matrix[1,1] - matrix[1,0] * matrix[0,1];
Console.WriteLine(determinant);  // output: -3

Si un índice de matriz se encuentra fuera de los límites de la dimensión correspondiente de una matriz, se produce una excepción IndexOutOfRangeException.

Tal como se muestra en el ejemplo anterior, también usa corchetes al declarar un tipo de matriz o crear instancias de matriz.

Para obtener más información sobre las matrices, consulte Matrices.

Acceso a indizador

En el ejemplo siguiente se usa el tipo Dictionary<TKey,TValue> de .NET para mostrar el acceso al indizador:

var dict = new Dictionary<string, double>();
dict["one"] = 1;
dict["pi"] = Math.PI;
Console.WriteLine(dict["one"] + dict["pi"]);  // output: 4.14159265358979

Los indizadores le permiten indizar las instancias de un tipo definido por el usuario de un modo similar a la indización de matrices. A diferencia de los índices de matriz, que deben ser enteros, los parámetros de indizador se pueden declarar para ser de cualquier tipo.

Para más información sobre los indizadores, consulte Indizadores.

Otros usos de []

Para información sobre el acceso de los elementos de puntero, consulte la sección Operador de acceso de elemento de puntero del artículo Operadores relacionados con el puntero. Para obtener información sobre las expresiones de colección, consulte el artículo Expresiones de colección.

También usa los corchetes para especificar atributos:

[System.Diagnostics.Conditional("DEBUG")]
void TraceMethod() {}

Operadores condicionales NULL ?. y ?[]

Un operador condicional null aplica un acceso a miembros (?.) o la operación acceso a elementos (?[]) a su operando solo si ese operando se evalúa como no null; de lo contrario, devuelve null. En otras palabras:

  • Si a se evalúa como null, el resultado de a?.x o a?[x] es null.

  • Si a se evalúa como no NULL, el resultado de a?.x o a?[x] es el mismo que el resultado de a.x o a[x], respectivamente.

    Nota

    Si a.x o a[x] producen una excepción, a?.x o a?[x] produciría la misma excepción para a no NULL. Por ejemplo, si a es una instancia de matriz que no es NULL y x está fuera de los límites de a, a?[x] produciría una excepción IndexOutOfRangeException.

Los operadores de condición NULL se cortocircuitan. Es decir, si una operación en una cadena de la operación de acceso a elementos o miembros condicional devuelve null, no se ejecuta el resto de la cadena. En el ejemplo siguiente, B no se evalúa si A se evalúa como null y C no se evalúa si A o B se evalúan como null:

A?.B?.Do(C);
A?.B?[C];

Si A podría ser NULL, pero B y C no lo serían si A no lo es también, solo tiene que aplicar el operador condicional NULL a A:

A?.B.C();

En el ejemplo anterior, B no se evalúa y no se llama a C() si A es NULL. Sin embargo, si se interrumpe el acceso a miembros encadenados, por ejemplo, entre paréntesis como en (A?.B).C(), no se produciría un cortocircuito.

En los ejemplos siguientes se muestra el uso de los operadores ?. y ?[]:

double SumNumbers(List<double[]> setsOfNumbers, int indexOfSetToSum)
{
    return setsOfNumbers?[indexOfSetToSum]?.Sum() ?? double.NaN;
}

var sum1 = SumNumbers(null, 0);
Console.WriteLine(sum1);  // output: NaN

List<double[]?> numberSets =
[
    [1.0, 2.0, 3.0],
    null
];

var sum2 = SumNumbers(numberSets, 0);
Console.WriteLine(sum2);  // output: 6

var sum3 = SumNumbers(numberSets, 1);
Console.WriteLine(sum3);  // output: NaN
namespace MemberAccessOperators2;

public static class NullConditionalShortCircuiting
{
    public static void Main()
    {
        Person? person = null;
        person?.Name.Write(); // no output: Write() is not called due to short-circuit.
        try
        {
            (person?.Name).Write();
        }
        catch (NullReferenceException)
        {
            Console.WriteLine("NullReferenceException");
        }; // output: NullReferenceException
    }
}

public class Person
{
    public required FullName Name { get; set; }
}

public class FullName
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public void Write() => Console.WriteLine($"{FirstName} {LastName}");
}

En el primero de los dos ejemplos anteriores también se usa el operador de fusión de NULL ?? para especificar una expresión alternativa que se evaluará en caso de que el resultado de la operación condicional NULL sea null.

Si a.x o a[x] es de un tipo de valor que no admite un valor NULL, T, a?.x o a?[x] es del tipo de valor que admite un valor NULL T? correspondiente. Si necesita una expresión de tipo T, aplique el operador de fusión de NULL ?? a una expresión condicional NULL, tal como se muestra en el ejemplo siguiente:

int GetSumOfFirstTwoOrDefault(int[]? numbers)
{
    if ((numbers?.Length ?? 0) < 2)
    {
        return 0;
    }
    return numbers[0] + numbers[1];
}

Console.WriteLine(GetSumOfFirstTwoOrDefault(null));  // output: 0
Console.WriteLine(GetSumOfFirstTwoOrDefault([]));  // output: 0
Console.WriteLine(GetSumOfFirstTwoOrDefault([3, 4, 5]));  // output: 7

En el ejemplo anterior, si no utiliza el operador ??, numbers?.Length < 2 da como resultado false cuando numbers es null.

Nota

El operador ?. evalúa el operando de la izquierda no más de una vez, lo que garantiza que no se pueda cambiar a null después de verificarse como no NULL.

El operador de acceso de miembro condicional NULL ?. también se conoce con el nombre de operador Elvis.

Invocación de delegado seguro para subprocesos

Use el operador ?. para comprobar si un delegado es distinto de NULL y se invoca de forma segura para subprocesos (por ejemplo, cuando se genera un evento), tal como se muestra en el código siguiente:

PropertyChanged?.Invoke(…)

El código es equivalente al siguiente:

var handler = this.PropertyChanged;
if (handler != null)
{
    handler(…);
}

El ejemplo anterior es una manera segura para subprocesos para asegurarse de que solo se invoca un valor no NULL handler. Dado que las instancias de delegado son inmutables, ningún subproceso puede cambiar el valor al que hace referencia la variable local handler. En concreto, si el código que ha ejecutado otro subproceso cancela la suscripción del evento PropertyChanged y PropertyChanged se convierte en null antes de que se invoque handler, el objeto al que hace referencia handler queda intacto.

Expresión de invocación ()

Utilice paréntesis, (), para llamar a un método o invocar un delegado.

En el ejemplo siguiente se muestra cómo llamar a un método, con o sin argumentos, y cómo invocar un delegado:

Action<int> display = s => Console.WriteLine(s);

List<int> numbers =
[
    10,
    17
];
display(numbers.Count);   // output: 2

numbers.Clear();
display(numbers.Count);   // output: 0

También usa paréntesis al invocar un constructor con el operador new.

Otros usos de ()

También usa los paréntesis para ajustar el orden en el que se van a evaluar operaciones en una expresión. Para obtener más información, vea Operadores de C# (referencia de C#).

Expresiones de conversión, que realizan conversiones de tipo explícitas, también utilizan paréntesis.

Indexación desde el operador final ^

Los operadores de índice e intervalo se pueden usar con un tipo contable. Un tipo contable es un tipo que tiene una propiedad int denominada Count o Length con un descriptor de acceso get accesible. Las expresiones de colección también se basan en tipos contables.

El operador ^ indica la posición del elemento a partir del final de una secuencia. En el caso de una secuencia de longitud length, ^n apunta al elemento con desplazamiento length - n desde el inicio de una secuencia. Por ejemplo, ^1 apunta al último elemento de una secuencia y ^length apunta al primer elemento de una secuencia.

int[] xs = [0, 10, 20, 30, 40];
int last = xs[^1];
Console.WriteLine(last);  // output: 40

List<string> lines = ["one", "two", "three", "four"];
string prelast = lines[^2];
Console.WriteLine(prelast);  // output: three

string word = "Twenty";
Index toFirst = ^word.Length;
char first = word[toFirst];
Console.WriteLine(first);  // output: T

Como se muestra en el ejemplo anterior, la expresión ^e es del tipo System.Index. En la expresión ^e, el resultado de e debe poderse convertir implícitamente a int.

También puede usar el operador ^ con el operador de intervalo para crear un intervalo de índices. Para más información, consulte Índices y rangos.

A partir de C# 13, el índice del operador final se puede usar en un inicializador de objeto.

Operador de intervalo ..

El operador .. especifica el inicio y el final de un intervalo de índices como sus operandos. El operando izquierdo es un inicio inclusivo de un intervalo. El operando derecho es un inicio exclusivo de un intervalo. Cualquiera de los operandos puede ser un índice desde el inicio o desde el final de una secuencia, tal y como muestra el ejemplo siguiente:

int[] numbers = [0, 10, 20, 30, 40, 50];
int start = 1;
int amountToTake = 3;
int[] subset = numbers[start..(start + amountToTake)];
Display(subset);  // output: 10 20 30

int margin = 1;
int[] inner = numbers[margin..^margin];
Display(inner);  // output: 10 20 30 40

string line = "one two three";
int amountToTakeFromEnd = 5;
Range endIndices = ^amountToTakeFromEnd..^0;
string end = line[endIndices];
Console.WriteLine(end);  // output: three

void Display<T>(IEnumerable<T> xs) => Console.WriteLine(string.Join(" ", xs));

Como se muestra en el ejemplo anterior, la expresión a..b es del tipo System.Range. En la expresión a..b, los resultados de a y b deben poderse convertir implícitamente a Int32 o Index.

Importante

Las conversiones implícitas de int a Index producen una excepción ArgumentOutOfRangeException cuando el valor es negativo.

Puede omitir cualquiera de los operandos del operador .. para obtener un intervalo abierto:

  • a.. es equivalente a a..^0
  • ..b es equivalente a 0..b
  • .. es equivalente a 0..^0
int[] numbers = [0, 10, 20, 30, 40, 50];
int amountToDrop = numbers.Length / 2;

int[] rightHalf = numbers[amountToDrop..];
Display(rightHalf);  // output: 30 40 50

int[] leftHalf = numbers[..^amountToDrop];
Display(leftHalf);  // output: 0 10 20

int[] all = numbers[..];
Display(all);  // output: 0 10 20 30 40 50

void Display<T>(IEnumerable<T> xs) => Console.WriteLine(string.Join(" ", xs));

En la tabla siguiente se muestran varias maneras de expresar los intervalos de colección:

Expresión del operador de intervalo Descripción
.. Todos los valores de la colección.
..end Valores desde el principio hasta end exclusivamente.
start.. Valores desde start inclusivamente hasta el final.
start..end Valores desde start inclusivamente hasta end exclusivamente.
^start.. Valores desde start inclusivamente hasta el final contando desde el final.
..^end Valores desde el inicio hasta end exclusivamente contando desde el final.
start..^end Valores de start inclusivamente a end exclusivamente contando desde el final.
^start..^end Valores de start inclusivamente a end exclusivamente contando ambos desde el final.

En el ejemplo siguiente se muestra el efecto de usar todos los intervalos presentados en la tabla anterior:

int[] oneThroughTen =
[
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10
];

Write(oneThroughTen, ..);
Write(oneThroughTen, ..3);
Write(oneThroughTen, 2..);
Write(oneThroughTen, 3..5);
Write(oneThroughTen, ^2..);
Write(oneThroughTen, ..^3);
Write(oneThroughTen, 3..^4);
Write(oneThroughTen, ^4..^2);

static void Write(int[] values, Range range) =>
    Console.WriteLine($"{range}:\t{string.Join(", ", values[range])}");
// Sample output:
//      0..^0:      1, 2, 3, 4, 5, 6, 7, 8, 9, 10
//      0..3:       1, 2, 3
//      2..^0:      3, 4, 5, 6, 7, 8, 9, 10
//      3..5:       4, 5
//      ^2..^0:     9, 10
//      0..^3:      1, 2, 3, 4, 5, 6, 7
//      3..^4:      4, 5, 6
//      ^4..^2:     7, 8

Para más información, consulte Índices y rangos.

El token .. también se usa para el elemento de propagación en una expresión de colección.

Posibilidad de sobrecarga del operador

Los operadores ., (), ^ y .. no se pueden sobrecargar. El operador [] también se considera un operador que no se puede sobrecargar. Use indizadores para admitir la indización con tipos definidos por el usuario.

Especificación del lenguaje C#

Para más información, vea las secciones siguientes de la Especificación del lenguaje C#:

Para más información sobre índices y rangos, vea la nota de propuesta de características.

Vea también