成员访问运算符和表达式 - 点、索引器和调用运算符。

使用多个运算符和表达式来访问类型成员。 这些运算符包括成员访问 (.)、数组元素或索引器访问 ([])、从末尾开始索引 (^)、范围 (..)、null 条件运算符(?.?[]),以及方法调用 (())。 其中包括 Null 条件成员访问 (?.) 和索引器访问 (?[]) 运算符。

成员访问表达式 .

可以使用 . 标记来访问命名空间或类型的成员,如以下示例所示:

  • 使用 . 访问命名空间内的嵌套命名空间,如以下 . 的示例所示:
using System.Collections.Generic;
  • 使用 . 构成限定名称以访问命名空间中的类型,如下面的代码所示:
System.Collections.Generic.IEnumerable<int> numbers = [1, 2, 3];

使用 using 指令来使用可选的限定名称。

  • 使用 . 访问类型成员(静态和非静态),如下面的代码所示:
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

还可以使用 . 访问.

索引器运算符 []

方括号 [] 通常用于数组、索引器或指针元素访问。 从 C# 12 开始,[] 会将集合表达式括起来。

数组访问

下面的示例演示如何访问数组元素:

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

如果数组索引超出数组相应维度的边界,将引发 IndexOutOfRangeException

如上述示例所示,在声明数组类型或实例化数组实例时,还会使用方括号。

有关数组的详细信息,请参阅数组

索引器访问

下面的示例使用 .NET Dictionary<TKey,TValue> 类型来演示索引器访问:

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

使用索引器,可通过类似于编制数组索引的方式对用户定义类型的实例编制索引。 与必须是整数的数组索引不同,可以将索引器参数声明为任何类型。

有关索引器的详细信息,请参阅索引器

[] 的其他用法

要了解指针元素访问,请参阅与指针相关的运算符一文的指针元素访问运算符 [] 部分。 有关集合表达式的信息,请参阅集合表达式一文。

方括号还用于指定属性

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

Null 条件运算符 ?.?[]

仅当操作数的计算结果为非 NULL 时,NULL 条件运算符才对其操作数应用成员访问 (?.) 或元素访问 (?[]) 操作;否则,它会返回 null。 换句话说:

  • 如果 a 的计算结果为 null,则 a?.xa?[x] 的结果为 null

  • 如果 a 的计算结果为非 null,则 a?.xa?[x] 的结果将分别与 a.xa[x] 的结果相同。

    注意

    如果 a.xa[x] 引发异常,则 a?.xa?[x] 将对非 null a 引发相同的异常。 例如,如果 a 为非 null 数组实例且 xa的边界之外,则 a?[x] 将引发 IndexOutOfRangeException

NULL 条件运算符采用最小化求值策略。 也就是说,如果条件成员或元素访问运算链中的一个运算返回 null,则链的其余部分不会执行。 在以下示例中,如果 A 的计算结果为 null,则不会计算 B;如果 AB 的计算结果为 null,则不会计算 C

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

如果 A 可以为 null,但如果 A 不为 null,BC 将不为 null,你只需要对 A 应用 null 条件运算符:

A?.B.C();

在上述示例中,如果 A 为 null,则不会计算 B,也不会调用 C()。 但是,如果链接的成员访问被中断,例如被 (A?.B).C() 中的括号中断,则不会发生短路。

以下示例演示了 ?.?[] 运算符的用法:

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

前面两个示例中的第一个也使用 Null 合并运算符 ?? 来指定替代表达式,以便在 null 条件运算的结果为 null 时用于计算。

如果 a.xa[x] 是不可为 null 的值类型 T,则 a?.xa?[x] 属于对应的可为 null 的值类型 T?。 如果需要 T 类型的表达式,请将 Null 合并操作符 ?? 应用于 null 条件表达式,如下面的示例所示:

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

在前面的示例中,如果不使用 ?? 运算符,则在 numbersnull 时,numbers?.Length < 2 的计算结果为 false

注意

?. 运算符对其左操作数的计算不超过一次,从而确保在验证为非 null 后,不能将其更改为 null

Null 条件成员访问运算符 ?. 也称为 Elvis 运算符。

线程安全的委托调用

使用 ?. 运算符来检查委托是否非 null 并以线程安全的方式调用它(例如,?.时),如下面的代码所示:

PropertyChanged?.Invoke(…)

该代码等同于以下代码:

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

前面的示例是一种线程安全方法,可确保只调用非 null handler。 由于委托实例是不可变的,因此,任何线程都不能更改 handler 本地变量所引用的对象。 具体而言,如果另一个线程执行的代码从 PropertyChanged 事件中取消订阅,并且 PropertyChanged 在调用 handler 之前变为 null,则 handler 引用的对象不受影响。

调用表达式 ()

使用括号 () 调用()或调用委托

以下示例演示如何在使用或不使用参数的情况下调用方法,以及调用委托:

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

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

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

在调用带 new 运算符的构造函数时,还可以使用括号。

() 的其他用法

此外可以使用括号来调整表达式中计算操作的顺序。 有关详细信息,请参阅 C# 运算符

强制转换表达式,其执行显式类型转换,也可以使用括号。

从末尾运算符 ^ 开始索引

索引和范围运算符可以与可数类型一起使用。 可数类型具有名为 CountLengthint 属性以及可访问的 get 访问器集合表达式还依赖于可数类型。

^ 运算符指示序列末尾的元素位置。 对于长度为 length 的序列,^n 指向与序列开头偏移 length - n 的元素。 例如,^1 指向序列的最后一个元素,^length 指向序列的第一个元素。

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

如前面的示例所示,表达式 ^e 属于 System.Index 类型。 在表达式 ^e 中,e 的结果必须隐式转换为 int

还可以将 ^ 运算符与^一起使用以创建一个索引范围。 有关详细信息,请参阅索引和范围

从 C# 13 开始,可在对象初始值设定项中使用结束运算符中的索引。

范围运算符 ..

.. 运算符指定某一索引范围的开头和末尾作为其操作数。 左侧操作数是范围的包含性开头。 右侧操作数是范围的不包含性末尾。 任一操作数都可以是序列开头或末尾的索引,如以下示例所示:

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

如前面的示例所示,表达式 a..b 属于 System.Range 类型。 在表达式 a..b 中,ab 的结果必须隐式转换为 Int32Index

重要

当值为负数时,从 int 隐式转换为 Index 会引发 ArgumentOutOfRangeException

可以省略 .. 运算符的任何操作数来获取无限制范围:

  • a.. 等效于 a..^0
  • ..b 等效于 0..b
  • .. 等效于 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));

下表显示了表达集合范围的各种方法:

范围运算符表达式 说明
.. 集合中的所有值。
..end 从开头到 end(不含)的值。
start.. start(含)到结尾的值。
start..end start(含)到 end(不含)的值。
^start.. start(含)到倒计数结尾的值。
..^end 从开头到 end(不含,倒计数)的值。
start..^end start(含)到 end(不含,倒计数)的值。
^start..^end start(含)到 end(不含),均为倒计数。

以下示例演示了使用上表中所有范围的效果:

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

有关详细信息,请参阅索引和范围

.. 词元还用作集合表达式中的 spread 元素

运算符可重载性

.()^.. 运算符无法进行重载。 [] 运算符也被视为非可重载运算符。 使用索引器以支持对用户定义的类型编制索引。

C# 语言规范

有关更多信息,请参阅 C# 语言规范的以下部分:

有关索引和范围的详细信息,请参阅功能建议说明

请参阅