运算符与表达式
运算符
算术运算符
对数值操作数执行算术运算
////////////////////////////////////////////////////////
// 一元
// ++/--
// x++ 的结果是此操作前的 x 的值
int i = 3;
Console.WriteLine(i); // output: 3
Console.WriteLine(i++); // output: 3
Console.WriteLine(i); // output: 4
// ++x 的结果是此操作后的 x 的值
double a = 1.5;
Console.WriteLine(a); // output: 1.5
Console.WriteLine(++a); // output: 2.5
Console.WriteLine(a); // output: 2.5
// --类似
……
// 一元 + 运算符返回其操作数的值。 一元 - 运算符对其操作数的数值取负。
Console.WriteLine(+4); // output: 4
Console.WriteLine(-4); // output: -4
Console.WriteLine(-(-4)); // output: 4
uint a = 5;
var b = -a;
Console.WriteLine(b); // output: -5
Console.WriteLine(b.GetType()); // output: System.Int64
Console.WriteLine(-double.NaN); // output: NaN
//////////////////////////////////////////////////////////
// 二元
// 加减乘除
int a = 10;
int b = 3;
int sum = a + b; // 13 加
int diff = a - b; // 7 减
int product = a * b; // 30 乘
int quotient = a / b; // 3 除
int remainder = a % b; // 1 取余
// 浮点除法
// 如果操作数之一为 decimal,那么另一个操作数不得为 float 和 double,
// 因为 float 和 double 都无法隐式转换为 decimal。
// 必须将 float 或 double 操作数显式转换为 decimal 类型。
Console.WriteLine(16.8f / 4.1f); // output: 4.097561
Console.WriteLine(16.8d / 4.1d); // output: 4.09756097560976
Console.WriteLine(16.8m / 4.1m); // output: 4.0975609756097560975609756098
// 浮点余数
// 对于 float 和 double 操作数,有限的 x 和 y 的 x % y 的结果是值 z,因此
// z(如果不为零)的符号与 x 的符号相同。
// z 的绝对值是 |x| - n * |y| 得出的值,
// 其中 n 是小于或等于 |x| / |y| 的最大可能整数,|x| 和 |y| 分别是 x 和 y 的绝对值。
// 计算余数的此方法类似于用于整数操作数的方法,
// 但与 IEEE 754 规范不同。
// 如果需要符合 IEEE 754 规范的余数运算,
// 请使用 Math.IEEERemainder 方法。
Console.WriteLine(-5.2f % 2.0f); // output: -1.2
Console.WriteLine(5.9 % 3.1); // output: 2.8
Console.WriteLine(5.9m % 3.1m); // output: 2.8
// 复合赋值
int a = 5;
a += 9;
Console.WriteLine(a); // output: 14
a -= 4;
Console.WriteLine(a); // output: 10
a *= 2;
Console.WriteLine(a); // output: 20
a /= 4;
Console.WriteLine(a); // output: 5
a %= 3;
Console.WriteLine(a); // output: 2
// 由于数值提升,op 运算的结果可能不会隐式转换为 x 的 T 类型。
// 在这种情况下,如果 op 是预定义的运算符并且运算的结果可以显式转换为 x 的类型 T,
// 则形式为 x op= y 的复合赋值表达式等效于 x = (T)(x op y),但 x 仅计算一次。
byte a = 200;
byte b = 100;
//数值提升
var c = a + b;
Console.WriteLine(c.GetType()); // output: System.Int32
Console.WriteLine(c); // output: 300
//溢出
a += b;
Console.WriteLine(a); // output: 44
整数算术溢出
整数被零除总是引发 DivideByZeroException。 如果发生整数算术溢出,溢出检查上下文(可能已检查或未检查)将控制引发的行为:
在已检查的上下文中,如果在常数表达式中发生溢出,则会发生编译时错误。 否则,当在运行时执行此运算时,则引发 OverflowException。
在未检查的上下文中,会通过丢弃任何不适应目标类型的高序位来将结果截断。
在使用 checked 和 unchecked 语句时,可以使用 checked 和 unchecked 运算符来控制溢出检查上下文,在该上下文中将计算一个表达式
int a = int.MaxValue;
int b = 3;
Console.WriteLine(unchecked(a + b)); // output: -2147483646
try
{
int d = checked(a + b);
}
catch(OverflowException)
{
Console.WriteLine($"Overflow occurred when adding {a} to {b}.");
}
浮点运算溢出
使用 float 和 double 类型的算术运算永远不会引发异常。 使用这些类型的算术运算的结果可能是表示无穷大和非数字的特殊值之一
对于 decimal 类型的操作数,算术溢出始终会引发 OverflowException。 被零除总是引发 DivideByZeroException。
double a = 1.0 / 0.0;
Console.WriteLine(a); // output: Infinity
Console.WriteLine(double.IsInfinity(a)); // output: True
Console.WriteLine(double.MaxValue + double.MaxValue); // output: Infinity
double b = 0.0 / 0.0;
Console.WriteLine(b); // output: NaN
Console.WriteLine(double.IsNaN(b)); // output: True
精度丢失
由于实数和浮点运算的浮点表达形式的常规限制,在使用浮点类型的计算中可能会发生舍入误差。 也就是说,表达式得出的结果可能与预期的数学运算结果不同。 以下示例演示了几种此类情况:
因此在浮点运算中尽量使用decimal!!!
Console.WriteLine(.41f % .2f); // output: 0.00999999
double a = 0.1;
double b = 3 * a;
Console.WriteLine(b == 0.3); // output: False
Console.WriteLine(b - 0.3); // output: 5.55111512312578E-17
decimal c = 1 / 3.0m;
decimal d = 3 * c;
Console.WriteLine(d == 1.0m); // output: False
Console.WriteLine(d); // output: 0.9999999999999999999999999999
比较运算符
比较数值操作数
// 小于运算符 <
Console.WriteLine(7.0 < 5.1); // output: False
Console.WriteLine(5.1 < 5.1); // output: False
Console.WriteLine(0.0 < 5.1); // output: True
Console.WriteLine(double.NaN < 5.1); // output: False
Console.WriteLine(double.NaN >= 5.1); // output: False
// 大于运算符 >
Console.WriteLine(7.0 > 5.1); // output: True
Console.WriteLine(5.1 > 5.1); // output: False
Console.WriteLine(0.0 > 5.1); // output: False
Console.WriteLine(double.NaN > 5.1); // output: False
Console.WriteLine(double.NaN <= 5.1); // output: False
// 小于或等于运算符 <=
Console.WriteLine(7.0 <= 5.1); // output: False
Console.WriteLine(5.1 <= 5.1); // output: True
Console.WriteLine(0.0 <= 5.1); // output: True
Console.WriteLine(double.NaN > 5.1); // output: False
Console.WriteLine(double.NaN <= 5.1); // output: False
// 大于或等于运算符 >=
Console.WriteLine(7.0 >= 5.1); // output: True
Console.WriteLine(5.1 >= 5.1); // output: True
Console.WriteLine(0.0 >= 5.1); // output: False
Console.WriteLine(double.NaN < 5.1); // output: False
Console.WriteLine(double.NaN >= 5.1); // output: False
布尔逻辑运算符
将对 bool 操作数执行逻辑运算
////////////////////////////////////////////////
// 一元
// 逻辑非运算符 !
bool passed = false;
Console.WriteLine(!passed); // output: True
Console.WriteLine(!true); // output: False
////////////////////////////////////////////////
// 二元
// 逻辑“与”运算符 &
bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}
bool a = false & SecondOperand();
Console.WriteLine(a);
// Output:
// Second operand is evaluated.
// False
bool b = true & SecondOperand();
Console.WriteLine(b);
// Output:
// Second operand is evaluated.
// True
// 逻辑异或运算符 ^
// 如果 x 计算结果为 true 且 y 计算结果为 false,
// 或者 x 计算结果为 false 且 y 计算结果为 true,
// 那么 x ^ y 的结果为 true。 否则,结果为 false。
// 也就是说,对于 bool 操作数,^ 运算符的计算结果与不等运算符 != 相同。
Console.WriteLine(true ^ true); // output: False
Console.WriteLine(true ^ false); // output: True
Console.WriteLine(false ^ true); // output: True
Console.WriteLine(false ^ false); // output: False
// 逻辑或运算符 |
bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}
bool a = true | SecondOperand();
Console.WriteLine(a);
// Output:
// Second operand is evaluated.
// True
bool b = false | SecondOperand();
Console.WriteLine(b);
// Output:
// Second operand is evaluated.
// True
// 条件逻辑“与”运算符 &&
bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}
bool a = false && SecondOperand();
Console.WriteLine(a);
// Output:
// False
bool b = true && SecondOperand();
Console.WriteLine(b);
// Output:
// Second operand is evaluated.
// True
// 条件逻辑或运算符 ||
bool SecondOperand()
{
Console.WriteLine("Second operand is evaluated.");
return true;
}
bool a = true || SecondOperand();
Console.WriteLine(a);
// Output:
// True
bool b = false || SecondOperand();
Console.WriteLine(b);
// Output:
// Second operand is evaluated.
// True
// 复合赋值
bool test = true;
test &= false;
Console.WriteLine(test); // output: False
test |= true;
Console.WriteLine(test); // output: True
test ^= false;
Console.WriteLine(test); // output: True
可以为 null 的布尔逻辑运算符
对于 bool? 操作数,&(逻辑与)和 |(逻辑或)运算符支持三值逻辑,
仅当其两个操作数的计算结果都为 true 时,& 运算符才生成 true。
如果 x 或 y 的计算结果为 false,则 x & y 将生成 false(即使另一个操作数的计算结果为 null)。
否则,x & y 的结果为 null。
仅当其两个操作数的计算结果都为 false 时,| 运算符才生成 false。
如果 x 或 y 的计算结果为 true,则 x | y 将生成 true(即使另一个操作数的计算结果为 null)。
否则,x | y 的结果为 null。
bool? test = null;
Display(!test); // output: null
Display(test ^ false); // output: null
Display(test ^ null); // output: null
Display(true ^ null); // output: null
void Display(bool? b) => Console.WriteLine(b is null ? "null" : b.Value.ToString());
位运算符和移位运算符
将对整数类型的操作数执行位运算或移位运算
常用操作
////////////////////////////////////////////////
// 一元
// 按位求补运算符 ~
uint a = 0b_0000_1111_0000_1111_0000_1111_0000_1100;
uint b = ~a;
Console.WriteLine(Convert.ToString(b, toBase: 2));
// Output:
// 11110000111100001111000011110011
////////////////////////////////////////////////
// 二元
// 左移位运算符 <<
uint x = 0b_1100_1001_0000_0000_0000_0000_0001_0001;
Console.WriteLine($"Before: {Convert.ToString(x, toBase: 2)}");
uint y = x << 4;
Console.WriteLine($"After: {Convert.ToString(y, toBase: 2)}");
// Output:
// Before: 11001001000000000000000000010001
// After: 10010000000000000000000100010000
// 右移位运算符 >>
uint x = 0b_1001;
Console.WriteLine($"Before: {Convert.ToString(x, toBase: 2), 4}");
uint y = x >> 2;
Console.WriteLine($"After: {Convert.ToString(y, toBase: 2).PadLeft(4, '0'), 4}");
// Output:
// Before: 1001
// After: 0010
// 高顺序空位位置是根据左侧操作数类型设置的
// 如果左侧操作数的类型是 int 或 long,则右移运算符将执行 算术移位:
// 对于正数,高位补0;
// 对于负数,高位补1。
int a = int.MinValue;
Console.WriteLine($"Before: {Convert.ToString(a, toBase: 2)}");
int b = a >> 3;
Console.WriteLine($"After: {Convert.ToString(b, toBase: 2)}");
// Output:
// Before: 10000000000000000000000000000000
// After: 11110000000000000000000000000000
// 如果左侧操作数的类型是 uint 或 ulong,则右移运算符执行逻辑移位:
// 高顺序空位位置始终设置为0。
uint c = 0b_1000_0000_0000_0000_0000_0000_0000_0000;
Console.WriteLine($"Before: {Convert.ToString(c, toBase: 2), 32}");
uint d = c >> 3;
Console.WriteLine($"After: {Convert.ToString(d, toBase: 2).PadLeft(32, '0'), 32}");
// Output:
// Before: 10000000000000000000000000000000
// After: 00010000000000000000000000000000
// 移位计数的处理
// 移位操作时,移位计数(n)的值取决于右操作数的类型:
// 对于 int 类型,移位计数取值范围是 0 到 31(因为 int 类型是 32 位的,超过31位移位结果为0)。
// 对于 long 类型,移位计数取值范围是 0 到 63(因为 long 类型是 64 位的)。
// 如果指定的移位计数大于对应类型的位数,则实际使用的移位计数为 n % 32(对于 int)或 n % 64(对于 long)。
int a = 1; // 0000 0001
int result = a << 33; // 结果相当于 a << 1 (因为 33 % 32 = 1)
Console.WriteLine(result); // 输出:2
long b = 1L; // 0000 0001
long result = b << 65; // 结果相当于 b << 1 (因为 65 % 64 = 1)
Console.WriteLine(result); // 输出:2
// 逻辑“与”运算符 &
uint a = 0b_1111_1000;
uint b = 0b_1001_1101;
uint c = a & b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 10011000
// 逻辑异或运算符 ^
uint a = 0b_1111_1000;
uint b = 0b_0001_1100;
uint c = a ^ b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 11100100
// 逻辑或运算符 |
uint a = 0b_1010_0000;
uint b = 0b_1001_0001;
uint c = a | b;
Console.WriteLine(Convert.ToString(c, toBase: 2));
// Output:
// 10110001
// 复合赋值
uint INITIAL_VALUE = 0b_1111_1000;
uint a = INITIAL_VALUE;
a &= 0b_1001_1101;
Display(a); // output: 10011000
a = INITIAL_VALUE;
a |= 0b_0011_0001;
Display(a); // output: 11111001
a = INITIAL_VALUE;
a ^= 0b_1000_0000;
Display(a); // output: 01111000
a = INITIAL_VALUE;
a <<= 2;
Display(a); // output: 1111100000
a = INITIAL_VALUE;
a >>= 4;
Display(a); // output: 00001111
a = INITIAL_VALUE;
a >>>= 4;
Display(a); // output: 00001111
void Display(uint x) => Console.WriteLine($"{Convert.ToString(x, toBase: 2).PadLeft(8, '0'), 8}");
相等运算符
将检查其操作数是否相等
值类型的相等性
如果内置值类型的值相等,则其操作数相等;
如果基本整数类型的相应值相等,则相同枚举类型的两个操作数相等。
用户定义的 struct 类型默认情况下不支持 == 运算符。 要支持 == 运算符,用户定义的结构必须重载它。
int a = 1 + 2 + 3;
int b = 6;
Console.WriteLine(a == b); // output: True
char c1 = 'a';
char c2 = 'A';
Console.WriteLine(c1 == c2); // output: False
Console.WriteLine(c1 == char.ToLower(c2)); // output: True
注意
对于 ==、<、>、<= 和 >= 运算符, 如果任何操作数不是数字(Double.NaN 或 Single.NaN),则运算的结果为 false。
这意味着 NaN 值不大于、小于或等于任何其他 double(或 float)值,包括 NaN。
引用类型的相等性
默认情况下,如果两个非记录引用类型操作符引用同一对象,则这两个操作符相等
public class ReferenceTypesEquality
{
public class MyClass
{
private int id;
public MyClass(int id) => this.id = id;
}
public static void Main()
{
var a = new MyClass(1);
var b = new MyClass(1);
var c = a;
Console.WriteLine(a == b); // output: False
Console.WriteLine(a == c); // output: True
}
}
如示例所示,默认情况下,用户定义的引用类型支持 == 运算符。 但是,引用类型可重载 == 运算符。 如果引用类型重载 == 运算符,使用 Object.ReferenceEquals 方法来检查该类型的两个引用是否引用同一对象。
记录类型相等性
记录类型支持 == 和 != 运算符,这些运算符默认提供值相等性语义。
也就是说,当两个记录操作数均为 null 或所有字段的对应值和自动实现的属性相等时,两个记录操作数都相等
public class RecordTypesEquality
{
public record Point(int X, int Y, string Name);
public record TaggedNumber(int Number, List<string> Tags);
public static void Main()
{
var p1 = new Point(2, 3, "A");
var p2 = new Point(1, 3, "B");
var p3 = new Point(2, 3, "A");
Console.WriteLine(p1 == p2); // output: False
Console.WriteLine(p1 == p3); // output: True
var n1 = new TaggedNumber(2, new List<string>() { "A" });
var n2 = new TaggedNumber(2, new List<string>() { "A" });
Console.WriteLine(n1 == n2); // output: False
}
}
字符串相等性
如果两个字符串均为 null 或者两个字符串实例具有相等长度且在每个字符位置有相同字符,则这两个字符串操作数相等:
string s1 = "hello!";
string s2 = "HeLLo!";
Console.WriteLine(s1 == s2.ToLower()); // output: True
string s3 = "Hello!";
Console.WriteLine(s1 == s3); // output: False
委托相等
若两个运行时间类型相同的委托操作数均为 null,或其调用列表长度系统且在每个位置具有相同的条目,则二者相等:
Action a = () => Console.WriteLine("a");
Action b = a + a;
Action c = a + a;
Console.WriteLine(object.ReferenceEquals(b, c)); // output: False
Console.WriteLine(b == c); // output: True
通过计算语义上相同的 Lambda 表达式生成的委托不相等,如以下示例所示:
Action a = () => Console.WriteLine("a");
Action b = () => Console.WriteLine("a");
Console.WriteLine(a == b); // output: False
Console.WriteLine(a + b == a + b); // output: True
Console.WriteLine(b + a == a + b); // output: False
不等运算符
如果操作数不相等,不等于运算符 != 返回 true,否则返回 false。
对于内置类型的操作数,表达式 x != y 生成与表达式 !(x == y) 相同的结果。
赋值运算符
赋值运算符 = 将其右操作数的值赋给变量、属性或由其左操作数给出的索引器元素。
赋值表达式的结果是分配给左操作数的值。
右操作数类型必须与左操作数类型相同,或可隐式转换为左操作数的类型。
赋值运算符 = 为右联运算符,即形式的表达式
a = b = c => a = (b = c)
赋值操作的左操作数接收右操作数的值。
当操作数是值类型时,赋值操作将复制右侧操作数的内容。
当操作数为引用类型时,赋值操作会复制对对象的引用。
List<double> numbers = [1.0, 2.0, 3.0];
Console.WriteLine(numbers.Capacity);
numbers.Capacity = 100;
Console.WriteLine(numbers.Capacity);
// Output:
// 4
// 100
int newFirstElement;
double originalFirstElement = numbers[0];
newFirstElement = 5;
numbers[0] = newFirstElement;
Console.WriteLine(originalFirstElement);
Console.WriteLine(numbers[0]);
// Output:
// 1
// 5
ref 赋值
Ref 赋值 = ref 使其左侧操作数成为右侧操作数的别名,
ref 赋值的左侧操作数可以是局部引用变量、ref 字段和 ref、out 或 in 方法参数。
两个操作数的类型必须相同。
void Display(double[] s) => Console.WriteLine(string.Join(" ", s));
double[] arr = { 0.0, 0.0, 0.0 };
Display(arr);// 0 0 0
ref double arrayElement = ref arr[0];
arrayElement = 3.0;
Display(arr);// 3 0 0
arrayElement = ref arr[arr.Length - 1];
arrayElement = 5.0;
Display(arr);// 3 0 5
Null 合并赋值
空合并赋值运算符(??=)是一种非常有用的语法糖,用于简化在变量为 null 时的赋值操作。
这个运算符是在 C# 8.0 中引入的。
工作原理
如果 variable 不为 null,则保持其原有值不变。
如果 variable 为 null,则将 value 赋值给 variable。
string name = null;
// 如果 name 为 null,则将 "Default Name" 赋值给它
name ??= "Default Name";
Console.WriteLine(name); // 输出: Default Name
运算符优先级与结合性
优先级(高=>低) | 运算符 | 描述 | 结合性 |
---|---|---|---|
1 | [] , . , () , ++ (后缀), -- (后缀), -> | 初级运算符 | 左 |
2 | ++ (前缀), -- (前缀), + (一元), - (一元), ! , ~ , (type) , * (指针), & , await , sizeof , typeof , checked , unchecked , default , nameof | 一元运算符 | 右 |
3 | * , / , % | 二元运算符 乘除 | 左 |
4 | + , - | 二元运算符 加减 | 左 |
5 | << , >> | 移位运算 | 左 |
6 | < , > , <= , >= , is , as , == , != | 关系运算 | 左 |
7 | & | 位运算 AND | 左 |
8 | ^ | 位运算 XOR | 左 |
9 | | | 位运算 OR | 左 |
10 | && | 逻辑运算 AND | 左 |
11 | || | 逻辑运算 OR | 左 |
12 | ?? , ?: | 条件运算符 | 左 |
13 | = , += , -= , *= , /= , %= , <<= , >>= , &= , ^= , |= , ??= | 赋值运算符 | 右 |
14 | => | Lambda 表达式 | 左 |
15 | , | 逗号运算符 | 左 |
相同优先级的运算符出现在同一表达式中时,它们的计算顺序由运算符的结合性决定
左结合 (Left-associative)
表示运算符从左到右进行求值。
右结合 (Right-associative)
表示运算符从右到左进行求值。
运算符重载
在 C# 中,运算符重载允许你为自定义类型(如类或结构体)定义或修改运算符的行为。通过重载运算符,你可以让自定义类型支持标准的运算符操作,如 +, -, *, == 等,从而使得这些类型在使用时更加直观和简洁。
基本概念
1.重载运算符
运算符重载实际上是定义了一个静态方法,该方法为特定运算符提供了新的功能。
重载运算符的方法必须使用关键字 operator,并与运算符的符号一起使用。
2.允许重载的运算符
你可以重载大多数的 C# 运算符,包括
一元运算符(如 +, -, !),
二元运算符(如 +, -, *, /),
比较运算符(如 ==, !=),
特定运算符(如 [], (),不过这两个通过索引器和调用运算符来实现)。
有些运算符不能被重载,
如 &&, ||, ?:, =, typeof, sizeof, new, is, as 等。
public struct Vector
{
public int X { get; }
public int Y { get; }
public Vector(int x, int y)
{
X = x;
Y = y;
}
// 重载加法运算符
public static Vector operator +(Vector v1, Vector v2)
{
return new Vector(v1.X + v2.X, v1.Y + v2.Y);
}
// 重载减法运算符
public static Vector operator -(Vector v1, Vector v2)
{
return new Vector(v1.X - v2.X, v1.Y - v2.Y);
}
// 重载相等运算符
public static bool operator ==(Vector v1, Vector v2)
{
return v1.X == v2.X && v1.Y == v2.Y;
}
// 重载不相等运算符
public static bool operator !=(Vector v1, Vector v2)
{
return !(v1 == v2);
}
public override bool Equals(object obj)
{
if (!(obj is Vector)) return false;
Vector v = (Vector)obj;
return this == v;
}
public override int GetHashCode()
{
return (X, Y).GetHashCode();
}
}
表达式
表达式是由操作数和运算符组成的序列,计算结果为一个值。表达式可以是简单的,也可以是复杂的。
常见表达式
1.常量表达式
常量表达式是由常量值组成的表达式,其结果在编译时就能确定。
int a = 10; // 常量表达式
double b = 3.14; // 常量表达式
2.变量表达式
变量表达式是指单个变量,它的值由变量的当前值决定。
int c = a; // 变量表达式
3.算术表达式
算术表达式使用算术运算符(如 +, -, *, /, %)对数字进行计算。
int sum = a + c; // 算术表达式
int product = a * 2; // 算术表达式
4.赋值表达式
赋值表达式用于将值赋给变量。赋值运算符有多种形式,如 =, +=, -=, *= 等。
int d = 5; // 简单赋值表达式
d += 10; // 复合赋值表达式
5.布尔表达式
布尔表达式会返回一个布尔值(true 或 false),通常使用比较运算符(如 ==, !=, >, <, >=, <=)和逻辑运算符(如 &&, ||, !)。
bool isEqual = a == c; // 布尔表达式
bool isGreater = a > 5 && c < 20; // 复杂布尔表达式
6.关系表达式
关系表达式比较两个值,返回布尔值。关系运算符包括 ==, !=, >, <, >=, <=。
bool result = a > b; // 关系表达式
7.条件表达式
条件表达式使用三元运算符 ? :,根据一个布尔表达式的结果返回不同的值。
int max = (a > c) ? a : c; // 条件表达式
8.类型转换表达式
类型转换表达式用于将一个类型的值显式转换为另一种类型。
double e = 9.8;
int f = (int)e; // 类型转换表达式,将 double 转换为 int
9.对象创建表达式
对象创建表达式使用 new 运算符来创建类的实例。
MyClass obj = new MyClass();
10.方法调用表达式
方法调用表达式调用一个方法,并可能传递参数。方法的返回值就是表达式的结果。
int length = "Hello".Length; // 方法调用表达式
11.索引器表达式
索引器表达式用于访问数组或集合中的元素。
int[] numbers = { 1, 2, 3 };
int firstNumber = numbers[0]; // 索引器表达式;
12.成员访问表达式
成员访问表达式通过点号 . 访问对象的字段、属性或方法。
string greeting = "Hello, World!";
int length = greeting.Length; // 成员访问表达式
13.await 表达式
用于异步编程,等待异步操作的完成。
await Task.Delay(1000); // await 表达式
Lambda表达式和匿名方法
Lambda 表达式是 C# 中一种简洁而强大的语法,用于创建匿名函数。它在函数式编程、LINQ 查询、委托传递、事件处理等场景中非常常见。
表达形式
Lambda 表达式可采用以下任意一种形式:
1.表达式 lambda(单个表达式) (input-parameters) => expression
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(2, 3)); // 输出 5
表达式 lambda 还可以转换为表达式树
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)
2.语句 lambda(多个语句) (input-parameters) => { <sequence-of-statements> }
Func<int, int, int> addWithLogging = (x, y) => {
Console.WriteLine($"Adding {x} and {y}");
int result = x + y;
return result;
};
Console.WriteLine(addWithLogging(2, 3)); // 输出 "Adding 2 and 3" 和 5
另外,通过使用 async 和 await 关键字,你可以轻松创建包含异步处理的 lambda 表达式和语句。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
//等价于
button1.Click += async (sender, e) =>
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
};
}
private async void button1_Click(object sender, EventArgs e)
{
await ExampleMethodAsync();
textBox1.Text += "\r\nControl returned to Click event handler.\n";
}
private async Task ExampleMethodAsync()
{
// The following line simulates a task-returning asynchronous process.
await Task.Delay(1000);
}
}
参数与返回值
任何 Lambda 表达式都可以转换为委托类型。
其参数的类型和返回值定义了 Lambda 表达式可转换成的委托类型。
如果 lambda 表达式不返回值,则可以将其转换为 Action 委托类型之一;
否则,可将其转换为 Func 委托类型之一。
其中内置委托类型(Action、Func)参考委托与事件
例如,有 2 个参数且不返回值的 Lambda 表达式可转换为 Action<T1,T2> 委托。
有 1 个参数且不返回值的 Lambda 表达式可转换为 Func<T,TResult> 委托。
// 无参数
Action line = () => Console.WriteLine();
// 一个参数 Func\<T,TResult>
Func<double, double> cube = x => x * x * x;
// 两个参数 Func\<T,T,TResult>
Func<int, int, bool> testForEquality = (x, y) => x == y;
// 多个参数
// ……
// 显式指定类型
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
// 弃元(未使用的两个或更多输入参数)
Func<int, int, int> constant = (_, _) => 42;
// 默认值 (C# 12以后)
var IncrementBy = (int source, int increment = 1) => source + increment;
// params数组(C# 12以后)
var sum = (params IEnumerable<int> values) =>
{
int sum = 0;
foreach (var value in values)
sum += value;
return sum;
};
var empty = sum();
Console.WriteLine(empty); // 0
var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15
// 元祖作为参数及返回值
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)
// 显式返回类型(C# 10 以后)
var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>
// 将属性添加到 Lambda 表达式及其参数(C# 10 以后)
var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;
lambda 可以引用外部变量。
外部变量是在定义 lambda 表达式的方法中或包含 lambda 表达式的类型中的范围内变量。 (闭包的实现)
public static class VariableScopeWithLambdas
{
public class VariableCaptureGame
{
internal Action<int>? updateCapturedLocalVariable;
internal Func<int, bool>? isEqualToCapturedLocalVariable;
public void Run(int input)
{
//外部变量
int j = 0;
updateCapturedLocalVariable = x =>
{
//使用
j = x;
bool result = j > input;
Console.WriteLine($"{j} is greater than {input}: {result}");
};
isEqualToCapturedLocalVariable = x => x == j;
Console.WriteLine($"Local variable before lambda invocation: {j}");
updateCapturedLocalVariable(10);
Console.WriteLine($"Local variable after lambda invocation: {j}");
}
}
public static void Main()
{
var game = new VariableCaptureGame();
int gameInput = 5;
game.Run(gameInput);
int jTry = 10;
bool result = game.isEqualToCapturedLocalVariable!(jTry);
Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");
int anotherJ = 3;
game.updateCapturedLocalVariable!(anotherJ);
bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
}
// Output:
// Local variable before lambda invocation: 0
// 10 is greater than 5: True
// Local variable after lambda invocation: 10
// Captured local variable is equal to 10: True
// 3 is greater than 5: False
// Another lambda observes a new value of captured variable: True
}
表达式树
什么是表达式树
表达式树是一种树形数据结构,用于表示计算机程序中的算术或逻辑表达式。每个节点代表表达式中的一个操作或操作数,树的结构反映了操作的优先级和执行顺序。
考虑一个简单的数学表达式:3 + 5 * 2。这个表达式可以用表达式树表示为:
在执行表达式树时,通常会通过遍历树来计算表达式的结果。最常见的遍历方式是后序遍历(Post-order Traversal),因为后序遍历能够确保先计算子节点,再对父节点执行操作,符合算术表达式的执行顺序。
后序遍历的执行顺序:
遍历左子树(3),得到 3。
遍历右子树:
先遍历左子树(5),得到 5。
再遍历右子树(2),得到 2。
最后对 5 和 2 进行乘法运算,得到 5 * 2 = 10。
访问根节点 +,对 3 和 10 进行加法运算,得到 3 + 10 = 13。
最终结果是 13。
在 C# 中,是C#中用于表示代码中的Lambda表达式或匿名方法的树形数据结构。
表达式树允许你以编程方式构建、分析和修改代码中的表达式。
它主要用于动态生成和执行代码、构建查询语言和实现表达式分析等场景。
主要类和接口
Expression: 这是所有表达式树节点的基类。它定义了表达式树的基本结构和接口。
Expression<TDelegate>: 表示一个具体的表达式树,其中 TDelegate 是一个委托类型。
例如,
Expression<Func<int, int>> 表示一个接受 int 参数并返回 int 的表达式。
BinaryExpression: 表示二元操作符(如加法、减法)的表达式。
ConstantExpression: 表示一个常量的表达式。
ParameterExpression: 表示一个参数的表达式。
使用表达式树
举个例子,假设我们希望使用Expression创建表达式 3 + 5 * 2
using System;
using System.Linq.Expressions;
class Program
{
static void Main(string[] args)
{
// 创建表达式节点
// 常量 3
Expression left = Expression.Constant(3);
// 常量 5
Expression right1 = Expression.Constant(5);
// 常量 2
Expression right2 = Expression.Constant(2);
// 乘法 5 * 2
Expression multiply = Expression.Multiply(right1, right2);
// 加法 3 + (5 * 2)
Expression add = Expression.Add(left, multiply);
// 将表达式树编译成可执行代码
var lambda = Expression.Lambda<Func<int>>(add);
// 等同于 Func<int> compiledExpression = () => 3 + (5 * 2);
var compiledExpression = lambda.Compile();
// 执行表达式
int result = compiledExpression();
Console.WriteLine($"The result of the expression is: {result}");
}
}
表达式树应用
其实在 c# 中计算 var result = 3 + 5 * 2; 这样的简单表达式在通常情况下不会以表达式树的方式表示。编译器会直接将它编译为 IL(中间语言)代码,并直接执行。
但是,如果我们需要在运行时生成和编译代码,而不是在编译时确定非常有用。
var paramX = Expression.Parameter(typeof(int), "x");
var paramY = Expression.Parameter(typeof(int), "y");
var body = Expression.Add(paramX, paramY);
var addFunc = Expression.Lambda<Func<int, int, int>>(body, paramX, paramY).Compile();
另外表达式树最广泛的应用场景是 LINQ(Language Integrated Query),尤其是 LINQ to SQL、Entity Framework 和其他 ORM 框架。
这些框架会将 LINQ 查询转换为 SQL 查询。在这种情况下,表达式树被用来表示查询,框架会解析这些表达式树并将其转换为等效的 SQL 查询。
var query = dbContext.Users
.Where(u => u.Age > 18)
.Select(u => u.Name);
//转换为
// 模拟 LINQ 表达式树解析
IQueryable<User> users = dbContext.Users;
// Where 子句解析成表达式树
Expression<Func<User, bool>> whereExpression = u => u.Age > 18;
// Select 子句解析成表达式树
Expression<Func<User, string>> selectExpression = u => u.Name;
// 最终组合的表达式树
var query = users.Where(whereExpression).Select(selectExpression);
另外LINQ 查询表达式在编译时被转换为表达式树。表达式树允许 LINQ 提供程序(如 Entity Framework)分析和转换查询为其他形式,如 SQL 查询。
比如
// 使用查询表达式进行查询
var query = from u in users
where u.Age > 28
orderby u.Name
select u.Name;
// 使用方法链进行相同的查询
var query = users
Where(u => u.Age > 28)
OrderBy(u => u.Name)
Select(u => u.Name);
// 等效于
// 构建表达式树
ParameterExpression param = Expression.Parameter(typeof(User), "u");
Expression ageCondition = Expression.GreaterThan(Expression.Property(param"Age"), Expression.Constant(28));
Expression nameProperty = Expression.Property(param, "Name");
var whereExpr = Expression.Lambda<Func<User, bool>>(ageCondition, param);
var selectExpr = Expression.Lambda<Func<User, string>>(nameProperty, param);
var filteredUsers = users.AsQueryable().Where(whereExpr).Select(selectExpr);
模式匹配
在 C# 中,模式匹配(Pattern Matching)是指在条件语句中使用模式来匹配数据结构的能力。模式匹配使得代码更加简洁和易读,特别是在处理复杂的条件判断时。C# 中的模式匹配功能在 C# 7.0 和 C# 8.0 中得到了显著增强,并在 C# 9.0 和 C# 10.0 中进一步扩展。
基本模式匹配(C# 7.0 及以上)
C# 7.0 引入了基本的模式匹配功能,主要包括 is 运算符和 switch 语句的模式匹配。
is 运算符
is 运算符允许你检查一个对象是否符合某种类型模式,并且可以将其转换为该类型。
using System;
public class Program
{
public static void Main()
{
object obj = "Hello, World!";
if (obj is string s)
{
Console.WriteLine($"String length: {s.Length}"); // 输出: String length: 13
}
}
}
switch 语句中的模式匹配
switch 语句支持匹配特定类型和解构对象。
using System;
public class Program
{
public static void Main()
{
object obj = 123;
switch (obj)
{
case int i:
Console.WriteLine($"Integer: {i}"); // 输出: Integer: 123
break;
case string s:
Console.WriteLine($"String: {s}");
break;
default:
Console.WriteLine("Unknown type");
break;
}
}
}
增强的模式匹配(C# 8.0)
C# 8.0 引入了更多的模式匹配功能,包括属性模式和位置模式。
属性模式
using System;
public class Program
{
public static void Main()
{
var person = new Person { Name = "Alice", Age = 30 };
switch (person)
{
case { Age: >= 18, Name: var name }:
Console.WriteLine($"Adult: {name}"); // 输出: Adult: Alice
break;
case { Age: < 18 }:
Console.WriteLine("Minor");
break;
}
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
位置模式
位置模式(Positional Patterns)用于匹配和解构元组和其他具名元组。
using System;
public class Program
{
public static void Main()
{
var point = (X: 10, Y: 20);
switch (point)
{
case (0, 0):
Console.WriteLine("Origin");
break;
case (var x, var y):
Console.WriteLine($"Point at ({x}, {y})"); // 输出: Point at (10, 20)
break;
}
}
}
进一步扩展(C# 9.0 和 C# 10.0)
C# 9.0 和 C# 10.0 引入了记录类型、简化的属性模式和更多的模式匹配功能。
记录类型
记录类型允许你使用模式匹配来匹配和解构对象。
using System;
public class Program
{
public static void Main()
{
var person = new Person("Alice", 30);
switch (person)
{
case Person { Age: >= 18, Name: var name }:
Console.WriteLine($"Adult: {name}"); // 输出: Adult: Alice
break;
case Person { Age: < 18 }:
Console.WriteLine("Minor");
break;
}
}
}
public record Person(string Name, int Age);
简化的模式匹配
C# 10.0 继续扩展模式匹配功能,例如更精细的类型模式和更好的解构支持。
using System;
public class Program
{
public static void Main()
{
object obj = new Point(10, 20);
if (obj is Point(int x, int y))
{
Console.WriteLine($"Point at ({x}, {y})"); // 输出: Point at (10, 20)
}
}
}
public record Point(int X, int Y);