三个轻量但高频的特性,让你的日常代码少写 30% 的废话
传统的 switch 是语句Statement,不是表达式Expression。你不能把它放在赋值的右侧,于是代码变成:
// C# 7.3:啰嗦五连——break、return、临时变量、重复书写
string dayType;
switch (day)
{
case DayOfWeek.Saturday:
case DayOfWeek.Sunday:
dayType = "周末";
break;
case DayOfWeek.Monday:
dayType = "星期一综合症";
break;
default:
dayType = "工作日";
break;
}
四个痛点:声明临时变量、重复赋值、每个分支 break、case 和 switch 本身都是纯噪音。
// C# 8:开关表达式——直接放在 = 右侧
string dayType = day switch
{
DayOfWeek.Saturday or DayOfWeek.Sunday => "周末",
DayOfWeek.Monday => "星期一综合症",
_ => "工作日",
};
一次只改一样东西,效果很明显:
string result;
switch (x) {
case 1: result = "一"; break;
case 2: result = "二"; break;
default: result = "其他"; break;
}
return result;
return x switch
{
1 => "一",
2 => "二",
_ => "其他",
};
| 元素 | 写法 | 含义 |
|---|---|---|
| 丢弃模式Discard Pattern | _ | 匹配所有剩余情况(代替 default) |
| 或模式Or Pattern | 1 or 2 | 匹配任一值 |
| 与模式And Pattern | > 0 and < 10 | 两个条件同时成立 |
| 属性模式Property Pattern | { Name: "张三" } | 在 case 里直接解构对象属性 |
| 元组模式Tuple Pattern | ("a", 1) | 同时匹配多个值 |
| when 子句When Clause | n when n > 10 | 附加条件 |
or/and/not,不是 |/&/!?| 和 & 在 C# 里已经是按位运算——对两个值做运算、产出一个新值。模式匹配不是在算值,是在描述形状。x is 1 | 2 & 3 怎么解析?是先算出 1 | 2 = 3,再 3 & 3 = 3,最后匹配 3?还是 1 or (2 and 3)?用 or/and 就没有歧义——模式有自己的优先级,跟表达式优先级彻底隔离。加上 not 没有好的运算符(! 是逻辑非,x is !null 读起来很奇怪),三个一起用关键字更一致。读起来也更像"条件描述"而非"位运算"。
属性模式的本质:不拆解对象,直接在 { } 里描述你要匹配的属性形状。
public decimal CalcDiscount(Order order) => order switch
{
{ IsVip: true, Total: >= 1000 } => 0.2m,
{ IsVip: true } => 0.1m,
{ Total: >= 500 } => 0.05m,
_ => 0m,
};
{ } 里的多个属性之间是 AND 关系——必须全部满足才命中该分支。但 未列出的属性会被忽略。{ Total: >= 500 } 不管 IsVip 是什么值都进来。
属性的值本身也可以是一个模式——属性模式可以无限嵌套:
public string DescribeAddress(Person p) => p switch
{
{ Address: { City: "北京", District: "海淀" } } => "海淀区居民",
{ Address: { City: "北京" } } => "北京居民",
{ Address: { Province: not null } } => "有地址但缺城市",
_ => "无地址信息",
};
属性模式可以和类型模式写在一起:TypeName { Prop: subpattern }。先检查类型,再检查属性形状。这是日常开发里最常用的模式组合。
public string ProcessShape(object shape) => shape switch
{
Circle { Radius: > 100 } => "大圆",
Circle { Radius: > 0 } => "小圆",
Rectangle { Width: var w, Height: var h } when w == h => "正方形",
Rectangle => "矩形",
null => "空",
_ => "未知形状",
};
注意 Rectangle 这行没带 { }——它是纯类型模式,只检查"是不是 Rectangle"。但如果 shape 是 null,这一行不会命中,因为类型模式自动排除 null。null 由下面单独的分支兜底。
TypeName { } 又是什么意思?写 Circle { } 就等于说"是 Circle 类型,且满足属性模式 { }"。而 { } 是属性模式的退化形式——不指定任何属性,匹配所有非 null 实例。效果上等价于纯类型模式 Circle。{ } 也能用——obj is { } 等价于 obj is not null。如果再加 var 模式,就能一边判 null 一边取出 NotNull 变量:obj is { } notNull。
上面第三行还藏着两个新东西:var w 是 var 模式(匹配任意值并绑定变量),when w == h 是附加条件。这就引出了下面两个话题。
当你要根据多个独立值的组合来做决策时,传统的做法是一层层的 if-else。元组模式让你把这些组合写成一个二维(或多维)的匹配表。
// C# 7.3:层层 if —— 逻辑分散、可读性差
string DescribeGame(string p1, string p2)
{
if (p1 == "rock" && p2 == "scissors") return "Player1 胜";
if (p1 == "rock" && p2 == "paper") return "Player2 胜";
if (p1 == "rock" && p2 == "rock") return "平局";
// ... 还有 6 种组合
return "未知";
}
// C# 8:元组模式——所有组合一目了然
string DescribeGame(string p1, string p2) => (p1, p2) switch
{
("rock", "scissors") => "Player1 胜",
("rock", "paper") => "Player2 胜",
("rock", "rock") => "平局",
("paper", "rock") => "Player1 胜",
("paper", "scissors") => "Player2 胜",
("paper", "paper") => "平局",
("scissors", "paper") => "Player1 胜",
("scissors", "rock") => "Player2 胜",
("scissors", "scissors")=> "平局",
_ => "非法手势",
};
不限于常量——你可以在元组的每个位置用任何模式:
string ClassifyPoint(int x, int y) => (x, y) switch
{
(0, 0) => "原点",
(_, 0) => "在 X 轴上",
(0, _) => "在 Y 轴上",
(> 0, > 0) => "第一象限",
(< 0, > 0) => "第二象限",
(< 0, < 0) => "第三象限",
(> 0, < 0) => "第四象限",
};
_ 丢弃你不关心的位置。比如上面 (_, 0) 的含义是"不管 x 是多少,只要 y 是 0 就命中"。
// 同时用元组和属性模式做复杂决策
string CanOrder(User user, Product product) => (user, product) switch
{
({ IsVip: true }, _) => "可以购买(VIP 不限购)",
(_, { Stock: 0 }) => "库存不足",
({ Age: < 18 }, { Category: "成人用品" }) => "年龄不符",
_ => "可以购买",
};
这里 (user, product) 构建了一个元组,元组里每个位置再用属性模式去解构。两种模式完全正交、可以任意嵌套。
when 给任何模式附加一个运行时条件——先匹配模式,再检查条件。模式本身是编译时确定的形状,when 是运行时求值的布尔表达式。
when模式匹配能做的事有限:
| 场景 | 模式能做吗? | 怎么办 |
|---|---|---|
| 比较两个变量 | ❌ 模式只能匹配固定常量 | { Width: var w, Height: var h } when w == h |
| 调用方法 | ❌ 模式里不能调用方法 | s when s.StartsWith("ERR") |
| 访问集合元素 | ❌ 模式不支持索引器 | list when list.Count > 0 && list[0] > 10 |
| 复杂布尔逻辑 | ⚠️ 部分支持(or/and/not) |
简单逻辑用 and/or,复杂逻辑用 when |
| 匹配常量 | ✅ 直接用 | 42 或 "hello" |
| 范围比较 | ✅ 关系模式(C# 9+) | > 0 and < 100 |
| null 检查 | ✅ | null 或 not null |
| 类型检查 | ✅ 类型模式 | Circle c |
// ❌ 不需要 when——模式本身就能搞定
string Bad(int n) => n switch
{
int x when x > 0 => "正数", // when 多余:直接用 > 0 模式
int x when x < 0 => "负数", // 同上
_ => "零",
};
// ✅ 模式优先——关系模式干的事别用 when
string Good(int n) => n switch
{
> 0 => "正数",
< 0 => "负数",
_ => "零",
};
// ✅ 必须用 when:条件涉及方法调用或变量间关系
string ValidateString(string s) => s switch
{
null => "空引用",
"" => "空字符串",
string x when x.Trim().Length == 0 => "全是空白", // 调了 Trim()
string x when x.Length > 100 => "超长字符串", // 调了 Length 属性
string x when x.StartsWith("ERR") => "错误消息", // 调了方法
_ => "普通字符串",
};
when 条件失败不会报错,而是继续尝试下一个分支。
string Grade(int score) => score switch
{
>= 90 when score <= 100 => "A",
>= 80 => "B",
>= 90 => "这句永远执行不到", // 如果 score >= 90,早就在第一行被匹配了
_ => "C",
};
第一行:score = 95 时,模式 >= 90 命中,然后 when score <= 100 也为 true → 返回 "A"。
score = 110 时,模式 >= 90 命中,但 when score <= 100 为 false → 跳过,继续试下一行 → >= 80 命中 → 返回 "B"。
所以 when 的本质是:模式先匹配,when 再过滤。模式匹配成功但 when 失败,等于这个分支没命中。
public string HandleRequest(HttpRequest req, User? user)
{
return (req, user) switch
{
// 元组模式:两个值一起匹配
// 属性模式:解构 req 的属性
// when:运行时条件——调用方法判断
({ Method: "GET", Path: "/admin" or "/admin/" }, { Role: "Admin" })
=> "管理员页面",
({ Method: "GET", Path: "/admin" or "/admin/" }, _)
=> "403 无权限",
({ Method: "POST" }, { } user) when user.CanPost()
=> "发布成功",
({ Method: "POST" }, _)
=> "请先登录",
_ => "未知请求",
};
}
一行拆解:(req, user) 构建元组 → 第一个位置用属性模式解构 Method 和 Path → Path 的值本身又用了 or 模式 → 第二个位置用属性模式检查 Role。四种模式装饰在一行里,各自独立、语义清晰。
_(或穷举所有可能值),否则编译器报错。这和传统 switch 语句不一样(传统 default 是可选的)。
传统 using 语句自带一层花括号和缩进:
// C# 7.3:每多一个 using,就多一层缩进
public void DoWork()
{
using (var conn = new SqlConnection(...))
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT ...";
using (var reader = cmd.ExecuteReader())
{
// 三层的缩进只是为了释放资源
}
}
}
C# 8 的 using 声明Using Declaration 不需要花括号:
// C# 8:变量在所在作用域结束时自动释放
public void DoWork()
{
using var conn = new SqlConnection(...);
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT ...";
using var reader = cmd.ExecuteReader();
// 离开方法时,reader → cmd → conn 依次 Dispose
}
| using 语句Statement | using 声明Declaration (C# 8) | |
|---|---|---|
| 写法 | using (var x = ...) { } | using var x = ...; |
| 释放时机 | 花括号 } 结束 | 变量离开作用域Scope 时 |
| 有自己的作用域 | 是 | 否——变量属于父作用域 |
// ✅ 用 using 声明:资源使用到方法结束
void Process()
{
using var conn = OpenConnection();
DoQuery(conn); // 用到方法最后
DoOtherQuery(conn); // 方法结束时释放——没问题
}
// ✅ 用老 using 语句:资源需要尽早释放
void Process()
{
using (var conn = OpenConnection())
{
DoQuery(conn); // } 时立即释放
}
// 这里 conn 已经没了,做其他耗时操作不影响连接
RunHeavyComputation();
}
^// C# 7.3:从末尾访问
var last = arr[arr.Length - 1];
var secondLast = arr[arr.Length - 2];
// C# 8:^ 操作符——"从末尾数"
var last = arr[^1]; // 倒数第一个
var secondLast = arr[^2]; // 倒数第二个
Index 是新类型,^n 是"从末尾数第 n 个":
Index i1 = ^1; // 倒数第一个
Index i2 = 0; // 从前往后第 0 个(正常索引)
Index i3 = ^0; // 等于 Length——不是最后一个!
^1 是倒数第一,不是 ^0?答案藏在公式里:^n = Length - n。这不是随意定的——是为了让 范围(Ranges)的数学性质干净。
先看正向索引和范围的关系:
// 正向索引:0 是第一个,Length 是"末尾之后的位置"
// 范围是左闭右开 [start, end),end 取"末尾之后"刚好表达"到结尾"
int[] arr = [10, 20, 30, 40, 50]; // Length = 5
arr[0..5]; // [10,20,30,40,50]——5 是 Length,"到末尾"
arr[2..5]; // [30,40,50]——"从索引 2 到末尾"
关键点:范围的右端是不包含的。所以"取到数组最后一个元素"的正确写法是 arr[0..Length],而不是 arr[0..Length-1]。
倒着数的时候,同样的数学必须成立。设计团队定下的规则是:
// ^n 映射到正向索引:^n = Length - n
^0 = 5 - 0 = 5 // 等于 Length——"末尾之后"
^1 = 5 - 1 = 4 // 最后一个元素
^2 = 5 - 2 = 3 // 倒数第二个
^5 = 5 - 5 = 0 // 第一个元素
arr[^3..^0]——右端是 ^0(等于 Length),完美符合左闭右开的语义:arr[^3..^0] = arr[2..5] = [30, 40, 50]。^0 是最后一个元素,那 arr[^3..^0] 的右端到底包含还是不包含?要么破坏左闭右开,要么用 ^3.. 这种省略形式绕过去——两种都是特殊 case,不如让公式统一。
换个角度想:正向索引的 0 是"开头",负向索引的 ^0 是"结尾之后"——对称。正向的 arr.Length 是"最后一个元素之后",负向的 ^0 也是。这套映射是双射(bijection),数学上干净。
^1 是最后一个。取单个元素用 ^1、^2。^0 表示"直到末尾"。取后缀用 arr[^3..^0],或者更简短的 arr[^3..](省略右端等价于省略 ^0)。
..int[] arr = [0, 1, 2, 3, 4, 5]; // 长度 6
var a = arr[0..3]; // [0, 1, 2]——左闭右开
var b = arr[^3..^1]; // [3, 4]——从倒数第3到倒数第1(不含)
var c = arr[3..]; // [3, 4, 5]——从索引3到末尾
var d = arr[..3]; // [0, 1, 2]——从开头到索引3(不含)
var e = arr[..]; // [0, 1, 2, 3, 4, 5]——全部(浅拷贝)
关键规则:
[start..end)——包含 start,不包含 end..3(开头到3)、3..(3到末尾)、..(全部)^ 可以用于任一端:^3..^1、"从倒数第3到倒数第1(不含)"// 切片会走 Slice 方法——Span/Memory 也同样支持
ReadOnlySpan<char> slice = "Hello World".AsSpan()[0..5]; // "Hello"
// 字符串切片返回 string(不需要 .AsSpan())
string sub = str[1..^1]; // 去掉首尾字符
任何有 Count/Length 属性和带 int 索引器的类型(加上 Slice(int, int) 方法)都自动支持。常见的:T[]、string、Span<T>、List<T>。
// A
var a = n switch { 1 => "一", _ => "其他" };
// B
var b = n switch { 1 => "一", 2 => "二" };
// C
string GetName(DayOfWeek d) => d switch
{
DayOfWeek.Monday => "周一",
DayOfWeek.Tuesday => "周二",
_ => "其他"
};
arr[^2..^0] 返回什么?(arr = [10, 20, 30, 40, 50],5 个元素)point = (0, -5) 时输出什么?string Describe(int x, int y) => (x, y) switch
{
(0, 0) => "原点",
(_, 0) => "在 X 轴上",
(0, _) => "在 Y 轴上",
(> 0, > 0) => "第一象限",
(< 0, > 0) => "第二象限",
(< 0, < 0) => "第三象限",
(> 0, < 0) => "第四象限",
};
when 子句,以下哪句话错误?Lesson 02 · C# 8 Switch Expressions / Using Declarations / Indices & Ranges · 下一课预告:Lesson 03 — yield return 同步迭代器(异步流的前置基础)