表达式
表达式由操作数和运算符组成。表达式的运算符指明了向操作数应用的运算。操作数可以是文本、字段、局部变量和表达式。
表达式可以分为以下几类:
- 具体的值。
- 变量。
null
。- 匿名函数。
- 元组。
- 属性访问。
- 索引器访问。
- 什么都没有:访问返回值为
void
的方法时产生。
还有一些特殊的表达式:
- 命名空间:只能出现在成员的左侧。
- 类型:只能出现在成员的左侧。
- 方法组。
- 事件访问:只能出现在
+=
、-=
的左侧。 throw
表达式。
C#中有两种表达式:lvalue和rvalue。lvalue表达式可以出现在赋值语句的左边或右边,而rvalue表达式只能出现在赋值语句的右边。
运算符
C#提供了许多支持内置类型的运算符:
- 赋值运算符:执行赋值(Value Assignment)操作。
- 算术运算符:对数值操作数执行算术运算。
- 比较运算符:比较操作数(数值或引用)。
true
和false
运算符。- 布尔逻辑运算符:对
bool
操作数执行逻辑运算。 - 位运算符:对整型操作数执行位运算或移位操作。
- 条件(三目)运算符。
- 成员访问运算符:访问类型成员。
- 类型检查运算符:执行类型检查或类型转换。
- 指针运算符:执行指针相关的操作。
- 其他。
赋值运算符=
、空合并运算符??
和??=
、条件运算符?:
为右结合运算符,即从右向左执行运算。其余所有二元运算符均为左结合运算符,即从左向右执行运算。
大部分运算符可重载。借助运算符重载,可以在自定义的类或结构类型中指定运算操作。
赋值运算符
赋值运算符=
将右操作数的值赋给左操作数给出的变量、属性或索引器,为右结合运算符。赋值表达式的结果是赋给左操作数的值。 右操作数类型必须与左操作数类型相同或者可以隐式转换为左操作数。
a = b = c;
// 等价于
a = (b = c);
引用赋值= ref
可以将左操作数作为右操作数的别名(Alias),即传递的是引用,操作左操作数会改变右操作数而不管其是否为值类型。
复合赋值运算符op=
是将任意二元运算符与赋值运算符结合,即x op= y
等价于x = x op y
。复合赋值运算符支持算术运算符、布尔逻辑运算符、位运算符。+=
和-=
还用于事件的添加和移除。
由于数值提升,op
运算的结果可能不会隐式转换为x
的T
类型。此时,如果op
是预定义的运算符且运算结果可以显式转换为x
的类型T
,则x op= y
的复合赋值表达式等效于x = (T)(x op y)
,但x
仅计算一次。
// 算术运算符的复合赋值表达式
byte a = 100;
byte b = 200;
a += b;
Console.WriteLine(a); // output: 44
// 逻辑运算符的复合赋值表达式
bool test = false;
test &= false; // test = false;
test |= true; // test = true;
test ^= false; // test = true;
// 位运算符
uint a = 0b_1111_1000;
a &= 0b_1001_1101; // a = 0b_1001_1000
a |= 0b_0011_0001; // a = 0b_1011_1001
a ^= 0b_1000_0000; // a = 0b_0011_1001
a <<= 2; // a = 0b_1110_0100
a >>= 4; // a = 0b_0000_1110
无法重载赋值运算符。不能显式重载复合赋值运算符。但若用户定义类型重载了二元运算符op
,则op=
运算符(如果存在)也将被隐式重载。
算术运算符
算术运算符对数字型操作数执行算术运算。所有的整型和浮点型都支持算术运算符。算术运算符包括一元增量++
、减量--
、取正+
和取负-
以及二元加+
、减-
、乘*
、除/
和取余%
。
增量++
和减量--
可以将操作数按1递增和递减。二者都支持前/后缀运算。前缀运算是先增/减再返回结果;后缀则先返回值,再增/减。
int i = 3;
Console.WriteLine(i); // Output: i = 3
Console.WriteLine(--i); // Output: i = 2
Console.WriteLine(i++); // Output: i = 2
Console.WriteLine(i); // Output: i = 3
取正+
返回原操作数,取负-
返回原操作数的相反数。unlong
类型不支持-
。
Console.WriteLine(+4); // output: 4
Console.WriteLine(-4); // output: -4
Console.WriteLine(+(-4)); // output: -4
Console.WriteLine(-(-4)); // output: 4
加+
用于求和、字符串串联和委托组合。减-
用于求差和删除委托。乘*
用于求积;一元*
运算符是指针解引用运算符。除/
用于求商。取余%
用于获取余数。
Console.WriteLine(5 % -4); // output: 1
Console.WriteLine(-5 % 4); // output: -1
Console.WriteLine(-5.2f % 2.0f); // output: -1.2
Console.WriteLine(5.9m % 3.1m); // output: 2.8
取余运算中,对于两个整型操作数,
a%b
的结果为a-(a/b)*b
的值。非零余数的符号与左操作数符号相同。对于float
和double
操作数,有限的x
和y
的x%y
的结果是值z
,其中z
(如果不为零)的符号与x
相同;z
的绝对值是|x|-n*|y|
,其中n
是小于或等于|x|/|y|
的最大可能整数。对于decimal
操作数,%
等效于System.Decimal
类型的余数运算符。
对于整型,上述运算符(++
和--
除外)是为int
、uint
、long
和ulong
类型定义的。当操作数为其他整型(sbyte
、byte
、short
、ushort
或char
)时,它们的值被转换为int
类型,这也是运算的结果类型。
当算术运算的结果超出所涉及的数字类型的可能有限值范围时,算术运算符的行为取决于其操作数的类型。
- 整型运算:不能除0,求商时舍弃小数;溢出时丢弃高阶位来截断结果。
- 浮点型运算(
float
、double
)不会抛出异常,因为有正负无穷大。 - 整型和浮点型运算:由于实数和浮点运算的浮点表示的一般限制,在使用浮点类型进行计算时可能会出现舍入错误。
算术运算符是左结合 的,优先级从大到小为:
- 后缀自增、后缀自减。
- 前缀自增、前缀自减。
- 乘、除、取模。
- 加、减。
可以重载算术运算符。当二元算术运算符被重载时,相应的复合赋值运算符也被隐式重载。 不能显式重载复合赋值运算符。
比较运算符
比较运算符用于比较数值型操作数。所有整型和浮点型都支持比较运算符。当操作数为char
时,将比较对应的字符代码。当操作数为enum
时,会比较相应的基础类型值。 比较运算符有大于>
、 小于<
、等于==
、大于等于>=
、小于等于<=
和不等于!=
,返回结果为true
或false
。
==
和!=
不仅可以比较值类型,也可以比较其他类型。对于非记录引用类型,比较是否引用同一对象。 对于记录类型,当两个记录操作数均为null
或所有字段的对应值和自动实现的属性相等时,两个记录操作数都相等。对于string
,如果均为null
或者二者长度相等且在每个字符位置有相同字符,则两操作数相等。 对于delegate
,若两个运行时间类型相同的委托操作数均为null
,或其调用列表长度且在每个位置具有相同的条目,则二者相等。
可以重载比较运算符。如果重载了<
或>
、<=
或>=
、==
或!=
运算符之一,必须同时重载另一个。 记录类型不能显式重载==
和!=
运算符。
true和false运算符
true
运算符返回bool
值true
,表示其操作数肯定为true
。false
运算符返回bool
值true
,表示其操作数肯定为false
。二者不能保证互为补充。也就是说,对于同一个操作数,true
和false
运算符都可能返回bool
值false
。如果一个类型定义了这两个运算符中的一个,它还必须定义另一个运算符。
布尔逻辑运算符
布尔逻辑运算符对bool
类型操作数执行逻辑运算。布尔逻辑运算符包括一元逻辑非!
,二元逻辑与&
、逻辑或|
和逻辑异或^
(始终计算两个操作数),二元条件逻辑与&&
和条件逻辑或||
(仅在必要时计算右操作数,亦称短路逻辑运算符)。对于整型,&
、|
、^
执行位运算。
对于null?
,逻辑与&
、逻辑或|
支持三值逻辑。只有当两个操作数都为null
时才返回null
。
布尔逻辑运算符的优先级从大到小为:
- 逻辑非。
- 逻辑与。
- 逻辑异或。
- 逻辑或。
- 条件逻辑与。
- 条件逻辑或。
不可以重载&&
和||
运算符。 不过,如果自定义类型以某种方式重载true
和false
运算符以及&
或|
运算符,可以对相应类型的操作数执行&&
或||
运算。重载了其他布尔逻辑运算符后会隐式重载相应的复合赋值运算符。
位运算符
位运算符操作整型或字符型char
的操作数,有一元按位求补~
,二元二进制左移<<
、右移>>
、无符号右移>>>
,二元按位与&
、按位或|
和按位异或^
。
按位求补~
产生其操作数的按位求补(反转每个位)。也可以使用~
来声明终结器。
左移位<<
将其左操作数向左移动右操作数的位数,结果为左操作数乘2的右操作数次方。左移运算自动丢弃高阶并将低阶置0。
右移位>>
将其左操作数向右移动右操作数的位数,结果为左操作数除2的右操作数次方。右移位运算自动丢弃低阶。如果左操作数为带符号类型,则执行算术移位:左操作数的最高有效位(符号位)的值将传播到高顺序空位(非负高顺序空位设0,负则设1)。如果左操作数为无符号类型,则执行逻辑移位:高顺序空位始终设置为0。
无符号右移位>>>
对所有整数类型总执行逻辑移位,无论左操作数类型如何,高阶位始终设0。
uint a = 0b_00001111_00001111_00001111_00001100;
uint b = ~a; // b = 0b_11110000_11110000_11110000_11110011
uint c = a << 2; // c = 0b_00111100_00111100_00111100_00110000
uint d = a >> 2; // d = 0b_00000011_11000011_11000011_11000011
uint x = 0b_1111_1000;
uint y = 0b_1001_1101;
uint m = x & y; // m = 0b_1001_1000
uint n = x | y; // n = 0b_1111_1101
uint o = x ^ y; // o = 0b_0110_0101
同样的,位运算符是针对int
、uint
、long
和ulong
定义的。如果两个操作数都是其他整数类型(sbyte
、byte
、short
、ushort
或char
),它们的值将转换为int
,并返回int
结果。如果操作数是不同的整型,它们的值将转换为最接近的整型。复合操作符(如>>=
)不将其参数转换为int
,也不将结果类型转换为int
。
所有enum
类型也支持~
、&
、|
和^
运算符。 对于相同enum
类型的操作数,对底层整数类型的相应值执行逻辑运算。通常需要使用位逻辑运算符的enum
类型,需使用Flags
特性定义。
位和移位操作永远不会导致溢出,并且在已检查和未检查的上下文中产生相同的结果。
位运算符的优先级从大到小为:
- 按位求补。
- 左移位、右移位、无符号右移位。
- 按位与。
- 按位异或。
- 按位或。
可以重载位运算符。当位运算符被重载时,相应的复合赋值运算符也被隐式重载。 不能显式重载复合赋值运算符。
条件运算符
条件运算符?:
也称三目运算符,用于计算布尔表达式,并根据布尔表达式的计算结果true
还是false
来返回两个表达式中的一个,为右联运算符。
// 语法:condition ? consequent : alternative
Console.WriteLine( temperatrue > 26 ? "hot" : "cold");
a ? b : c ? d : e;
// 等价于
a ? b : (c ? d : e);
从C# 9.0开始,条件表达式由目标确定类型。也就是说,如果条件表达式的目标类型是已知的,则consequent
和alternative
的类型必须可隐式转换为目标类型。如果条件表达式的目标类型是未知的(例如使用var
关键字时)或者采用C# 8.0及更早版本,则consequent
和alternative
的类型必须相同,或者必须存在从一种类型到另一种类型的隐式转换。
不能重载条件运算符。
成员访问运算符
成员访问运算符用于访问类型成员,包括
- 成员访问
.
:访问命名空间或类型的成员。 - 数组元素或索引器访问与指定特性
[]
:访问数组元素或类型的索引器,或者指定特性。 - 从末尾开始索引(index-from-end)
^
:表示元素的位置从序列的末尾开始。 - 范围
..
:指定可用于获取序列元素范围的索引范围(左闭右开)。 - 空条件访问
?.
和?[]
:仅在操作数非空时执行成员或元素访问操作。空条件访问运算符会逻辑短路。 - 方法调用
()
:调用已访问的方法或调用委托。
// .可以访问类型成员
using System.Collections.Generic;
Console.WriteLine(".可以访问类型成员");
// 在声明数组类型或实例化数组实例时,也会使用方括号
int[] nums = new int[4];
nums[0] = nums[1] = 2;
// 索引器访问
Dictionary<string, int> dic = new Dictionary<string, int>();
dic["one"] = 1;
// 从末尾开始索引
int[] xs = [0, 10, 20, 30, 40];
int last = xs[^1];
Console.WriteLine(last); // output: 40
// 范围
int[] numbers = [0, 10, 20, 30, 40, 50];
int[] subset = numbers[1..(1 + 3)]; // subset: 10 20 30
// 空条件访问(会逻辑短路)
A?.B?.Do(C);
成员访问运算符不能被重载。
类型检查运算符
类型检查运算符对操作数执行类型检查或类型转换。其中,is
运算符检查操作数运行时类型是否与给定类型兼容;()
、as
运算符显式地将操作数转换为该类型(as
在无法转换时返回null
而不抛出异常);typeof
运算符获取操作数的类型对象。
类型检查运算符不能被重载。
指针运算符
指针运算符能够获取变量的地址,对指针解引用,比较指针值,以及对指针和整数进行加减操作。指针运算符包括获取地址&
、解引用*
、访问成员->
和元素[]
、对指针进行算术运算。
T
类型指针和一个整数做加减会将该指针移动指定整数倍的sizeof(T)
。指针之间的差是地址差除以sizeof(T)
。指针之间比较的是地址。
int number = 27;
int* pointerToNumber = &number;
*pointerToNumber = 30;
x->y;
// 等价于
(*x).y;
char* pointerToChars = stackalloc char[123];
for (int i = 65; i < 123; i++)
{
pointerToChars[i] = (char)i;
}
指针运算符可以被重载。
其他运算符
空合并运算符??
当左操作数不为空,则返回左操作数;否则,对右操作数求值并返回其结果。如果左操作数非空,则不计算其右操作数(短路)。 空合并运算符是右结合的,不能被重载。
=>
用于分隔Lanbda表达式中的输入和输出、为成员定义表达式主体,不能被重载。
Func<string> greet = () => "Hello, World!";
public override string ToString() => $"{fname} {lname}".Trim();
nameof
获取变量、类型或成员的名称。nameof
表达式在编译时求值,在运行时没有影响。当操作数是类型或名称空间时,获取的非完全限定名称。
new
用于创建类型的新实例或泛型约束。
sizeof
用于获取给定类型所占用的字节数。sizeof
的实参必须是非托管类型的名称或约束为非托管类型的类型形参。
default
用于生成当前类型的默认值或在switch
中做默认的case
。
delegate
用于创建匿名方法。
await
用于挂起异步方法的求值操作直至该方法执行完毕。
with
用于生成操作数的副本(浅拷贝)并修改指定的属性和字段。
优先级
运算符的优先级和结合性决定表达式的计算顺序。 在表达式中,较高优先级的运算符会优先被计算。可以通过()
控制优先级。
下表将按运算符优先级从高到低列出各个运算符。
类别 | 运算符 | 结合性 |
---|---|---|
后缀 | () [] -> . ++ - - | 从左到右 |
一元 | + - ! ~ ++ - - (type)* & sizeof | 从右到左 |
乘除 | * / % | 从左到右 |
加减 | + - | 从左到右 |
移位 | << >> | 从左到右 |
比较 | < <= > >= | 从左到右 |
相等 | == != | 从左到右 |
位与 AND | & | 从左到右 |
位异或 XOR | ^ | 从左到右 |
位或 OR | | | 从左到右 |
逻辑与 AND | && | 从左到右 |
逻辑或 OR | || | 从左到右 |
条件 | ?: | 从右到左 |
赋值 | = += -= *= /= %=>>= <<= &= ^= |= | 从右到左 |
逗号 | , | 从左到右 |