成员访问运算符和表达式 - 点、索引器和调用运算符。
使用多个运算符和表达式来访问类型成员。 这些运算符包括成员访问 (.
)、数组元素或索引器访问 ([]
)、从末尾开始索引 (^
)、范围 (..
)、null 条件运算符(?.
和 ?[]
),以及方法调用 (()
)。 其中包括 Null 条件成员访问 (?.
) 和索引器访问 (?[]
) 运算符。
.
(成员访问):用于访问命名空间或类型的成员[]
(数组元素或索引器访问):用于访问数组元素或类型索引器?.
和?[]
(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?.x
或a?[x]
的结果为null
。如果
a
的计算结果为非 null,则a?.x
或a?[x]
的结果将分别与a.x
或a[x]
的结果相同。注意
如果
a.x
或a[x]
引发异常,则a?.x
或a?[x]
将对非 nulla
引发相同的异常。 例如,如果a
为非 null 数组实例且x
在a
的边界之外,则a?[x]
将引发 IndexOutOfRangeException。
NULL 条件运算符采用最小化求值策略。 也就是说,如果条件成员或元素访问运算链中的一个运算返回 null
,则链的其余部分不会执行。 在以下示例中,如果 A
的计算结果为 null
,则不会计算 B
;如果 A
或 B
的计算结果为 null
,则不会计算 C
:
A?.B?.Do(C);
A?.B?[C];
如果 A
可以为 null,但如果 A 不为 null,B
和 C
将不为 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.x
或 a[x]
是不可为 null 的值类型 T
,则 a?.x
或 a?[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
在前面的示例中,如果不使用 ??
运算符,则在 numbers
为 null
时,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
() 的其他用法
此外可以使用括号来调整表达式中计算操作的顺序。 有关详细信息,请参阅 C# 运算符。
强制转换表达式,其执行显式类型转换,也可以使用括号。
从末尾运算符 ^ 开始索引
索引和范围运算符可以与可数类型一起使用。 可数类型具有名为 Count
或 Length
的 int
属性以及可访问的 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
中,a
和 b
的结果必须隐式转换为 Int32 或 Index。
重要
当值为负数时,从 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# 语言规范的以下部分:
有关索引和范围的详细信息,请参阅功能建议说明。