跳转到内容

附录 C C# 速查表

这份表给写过别的语言、要快速想起 C# 怎么写的人:按节查,不从头读。每个条目是一行说明加一段代码,只讲怎么写,不讲为什么;需要原理的条目在说明末尾注明正文章节。代码以 .NET 10 和 C# 14 为准:csharp 代码块都是单文件程序,存成 app.cs 后用 dotnet run app.cs 运行,每段都能直接运行,注释里写的是运行结果。单文件程序用顶层语句,不用写 Main,默认打开可空引用类型和常用命名空间的隐式 using。用 // 文件名 分成几段的代码块属于一个项目,按标注的文件名放进项目里再运行。打印小数时一律指定 CultureInfo.InvariantCulture,免得小数点随系统的区域设置变成逗号。标“第三方”的工具和包需要另外安装。

官方下载地址:https://dotnet.microsoft.com/download

查看 SDK 版本,列出装了哪些 SDK 和运行时

终端窗口
dotnet --version
dotnet --list-sdks
dotnet --list-runtimes

查看完整的环境信息:SDK、运行时、操作系统、生效的 global.json

终端窗口
dotnet --info

在代码里查看运行时版本

using System.Runtime.InteropServices;
Console.WriteLine(Environment.Version); // 10.0.12
Console.WriteLine(RuntimeInformation.FrameworkDescription); // .NET 10.0.12

Windows 上用 winget 安装 .NET 10 SDK

终端窗口
winget install Microsoft.DotNet.SDK.10

macOS / Linux 上用官方安装脚本,装到用户目录,不需要管理员权限

终端窗口
curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh
bash dotnet-install.sh --channel 10.0

多个 SDK 可以并存,dotnet 默认用最新的;项目里用 global.json 固定 SDK 版本

终端窗口
dotnet new globaljson --sdk-version 10.0.401 --roll-forward latestFeature

生成的 global.json:放在项目根目录,它下面的所有目录都按它选 SDK;latestFeature 允许用 10.0 里更新的功能版本和补丁版本

{
"sdk": {
"rollForward": "latestFeature",
"version": "10.0.401"
}
}

C# 版本跟着目标框架走:net10.0 默认就是 C# 14,不用另装编译器

<TargetFramework>net10.0</TargetFramework>

运行单个文件(.NET 10 起):不用建项目,SDK 在后台建一个隐藏项目构建并缓存,第一次会慢几秒,详见第 2 章

终端窗口
dotnet run app.cs

单文件程序用顶层语句:直接写语句,类型声明放在语句后面

var p = new Point(1, 2);
Console.WriteLine(p); // Point { X = 1, Y = 2 }
record Point(int X, int Y);

没有随 SDK 附带的交互式环境(REPL),用单文件程序加 dotnet run 代替(dotnet run x.cs 的机制详见第 2 章)

终端窗口
dotnet run scratch.cs

给程序传参数:-- 后面的都交给程序,代码里用 args 读

// 运行:dotnet run app.cs -- a b
Console.WriteLine(string.Join(" ", args)); // a b
Console.WriteLine(args.Length); // 2

单文件里引用 NuGet 包:#:package 包名@版本,写在文件开头

#:package Humanizer@3.0.10
using Humanizer;
Console.WriteLine("some_property_name".Humanize()); // some property name

解析命令行选项:标准库里没有现成的解析器,用 NuGet 包 System.CommandLine 代替

#:package System.CommandLine@2.0.12
using System.CommandLine;
// 运行:dotnet run app.cs -- 小明 --times 2
var name = new Argument<string>("name");
var times = new Option<int>("--times") { DefaultValueFactory = _ => 1 };
var root = new RootCommand { name, times };
root.SetAction(result =>
{
for (int i = 0; i < result.GetValue(times); i++)
{
Console.WriteLine($"你好,{result.GetValue(name)}"); // 打印两次"你好,小明"
}
});
return root.Parse(args).Invoke();

单文件里设置项目属性:#:property;单文件程序默认按 Native AOT 发布做兼容分析,用反射的代码会出 IL 警告,不打算 AOT 发布时可以关掉,详见第 22 章

#:property PublishAot=false
using System.Text.Json;
Console.WriteLine(JsonSerializer.Serialize(new { Name = "a", Age = 1 })); // {"Name":"a","Age":1}

单文件里换用别的 SDK:#:sdk,例如几行写一个 Web 服务

#:sdk Microsoft.NET.Sdk.Web
var app = WebApplication.Create(args);
app.MapGet("/", () => "你好");
app.Run(); // 浏览器打开 http://localhost:5000 看到"你好",Ctrl+C 停止

在项目目录里运行、只构建;项目里传参数同样写在 -- 后面

终端窗口
dotnet run
dotnet build
dotnet run -- a b

改了代码自动重新构建并运行(在项目目录里)

终端窗口
dotnet watch run

编译成可执行文件:单文件程序 dotnet publish 默认走 Native AOT,产物在 artifacts/ 下,目标机器不用装 .NET;本机要有 C/C++ 链接工具(Windows 上是 Visual Studio 的“使用 C++ 的桌面开发”),详见第 2 章

终端窗口
dotnet publish app.cs
./artifacts/app/app # Windows 上是 artifacts\app\app.exe

读环境变量,不存在时给默认值

Console.WriteLine(Environment.GetEnvironmentVariable("APP_MODE") ?? "dev"); // dev

以非零退出码结束程序:顶层语句里 return 一个整数

Console.Error.WriteLine("配置文件不存在");
return 1;

新建控制台项目:生成 Demo/Demo.csproj 和 Demo/Program.cs

终端窗口
dotnet new console -o Demo
cd Demo

列出可用的项目模板

终端窗口
dotnet new list

添加 NuGet 包,写进 .csproj 的 PackageReference;不写版本时取最新的正式版(.NET 10 起也可写成名词在前的 dotnet package add)

终端窗口
dotnet add package Humanizer

添加指定版本的包

终端窗口
dotnet add package Humanizer --version 2.14.1

移除包

终端窗口
dotnet remove package Humanizer

列出项目的包;--outdated 列出有新版本的包

终端窗口
dotnet list package
dotnet list package --outdated

.csproj:输出类型、目标框架、隐式 using、可空引用类型、依赖(dotnet new console 生成后加了一个包)

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Humanizer" Version="2.14.1" />
</ItemGroup>
</Project>

锁文件默认不生成:在 .csproj 的 PropertyGroup 里打开,还原时生成 packages.lock.json,提交进版本库,详见第 11 章

<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>

在 CI 里按锁文件原样还原,.csproj 和锁文件对不上就报错

终端窗口
dotnet restore --locked-mode

按 .csproj 下载全部依赖;build、run、test 会先自动还原,一般不用单独运行

终端窗口
dotnet restore

新建类库,让控制台项目引用它(写进 .csproj 的 ProjectReference)

终端窗口
dotnet new classlib -o Calc
dotnet add Demo reference Calc

用解决方案文件把几个项目放在一起(.NET 10 默认生成 .slnx 格式)

终端窗口
dotnet new sln -n Shop
dotnet sln add Demo Calc

没有 package.json 那样的 scripts,用 dotnet 的子命令代替:构建、运行、测试、格式化都是子命令,在解决方案目录里运行会作用于所有项目

终端窗口
dotnet build
dotnet test
dotnet format

var 让编译器推断类型,推断出来就固定了;也可以写出类型,详见第 3 章

var name = "Ada"; // string
int count = 0;
count += 1;
Console.WriteLine($"{name} {count}"); // Ada 1

多重赋值用元组解构

var (x, y) = (1, 2);
Console.WriteLine($"{x} {y}"); // 1 2

交换两个变量

int a = 1, b = 2;
(a, b) = (b, a);
Console.WriteLine($"{a} {b}"); // 2 1

const 是编译期常量,只能是数字、字符串、布尔这类字面量,编译时直接写进用到它的地方,详见第 4 章

const int MaxRetries = 3;
const string Prefix = "log-";
Console.WriteLine($"{Prefix}{MaxRetries}"); // log-3

运行时才知道的常量用 static readonly 字段,只能在声明处或构造方法里赋值

Console.WriteLine(Config.StartedAt <= DateTime.Now); // True
static class Config
{
public static readonly DateTime StartedAt = DateTime.Now;
}

readonly 只管字段本身,字段指向的集合照样能改,详见第 4 章

var box = new Box();
box.Items.Add("a");
Console.WriteLine(box.Items.Count); // 1
class Box
{
public readonly List<string> Items = [];
}

没有只读的局部变量,用 const 或只读字段代替(const 和 readonly 详见第 4 章)

const double Rate = 0.13;
Console.WriteLine(Rate > 0); // True

作用域规则:局部变量是块级的,只在所在的花括号里可见;内层块不能再声明和外层同名的变量(编译错误 CS0136)

int total = 0;
for (int i = 0; i < 3; i++)
{
int square = i * i;
total += square;
}
Console.WriteLine(total); // 5

局部变量要先赋值再读,否则编译错误 CS0165;default 取类型的默认值

int n = default;
string? s = default;
Console.WriteLine($"{n} {s is null}"); // 0 True

局部函数和 lambda 能直接读写外层的局部变量

int counter = 0;
void Bump() => counter++;
Bump();
Bump();
Console.WriteLine(counter); // 2

字符串转整数:int.Parse 格式不对抛 FormatException;不确定格式时用 TryParse;Convert 可指定进制

Console.WriteLine(int.Parse("42")); // 42
Console.WriteLine(int.TryParse("42px", out var n) ? n : -1); // -1
Console.WriteLine(Convert.ToInt32("ff", 16)); // 255

带小数的字符串和数字互转都要指定区域设置;InvariantCulture 固定用点号

using System.Globalization;
double d = double.Parse("3.5", CultureInfo.InvariantCulture);
Console.WriteLine((d * 3).ToString(CultureInfo.InvariantCulture)); // 10.5

插值字符串里有小数时,用 string.Create 指定区域设置

using System.Globalization;
double price = 1234.5;
Console.WriteLine(string.Create(CultureInfo.InvariantCulture, $"{price} {price:F2}")); // 1234.5 1234.50

浮点转整数要强制转换,向零截断;向下取整用 Math.Floor

Console.WriteLine((int)3.9); // 3
Console.WriteLine((int)-3.9); // -3
Console.WriteLine(Math.Floor(-3.9)); // -4

整数除法:两个整数相除得整数,向零截断;% 的符号跟被除数;有一边是浮点数才得小数

Console.WriteLine(7 / 2); // 3
Console.WriteLine(-7 / 2); // -3
Console.WriteLine(-7 % 3); // -1
Console.WriteLine(7 / 2.0 == 3.5); // True

乘方用 Math.Pow,参数和结果都是 double

Console.WriteLine(Math.Pow(2, 10)); // 1024

Math.Round 默认“四舍六入五成双”;要四舍五入写 MidpointRounding.AwayFromZero

Console.WriteLine(Math.Round(2.5)); // 2
Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero)); // 3

整数溢出默认静默回绕;放进 checked 里溢出抛 OverflowException

int max = int.MaxValue;
Console.WriteLine(max + 1); // -2147483648
try
{
Console.WriteLine(checked(max + 1));
}
catch (OverflowException)
{
Console.WriteLine("溢出"); // 溢出
}

大整数用 BigInteger

using System.Numerics;
Console.WriteLine(BigInteger.Pow(2, 100)); // 1267650600228229401496703205376

double 有二进制误差;金额用 decimal(字面量加 m)

Console.WriteLine(0.1 + 0.2 == 0.3); // False
Console.WriteLine(0.1m + 0.2m == 0.3m); // True

bool 和整数不能互相隐式转换,条件里必须是 bool;字符串转布尔用 bool.Parse(不分大小写)

int count = 3;
Console.WriteLine(count != 0); // True
Console.WriteLine(bool.Parse("TRUE")); // True

判断类型用 is:判断和转换一步完成

object value = 42;
if (value is int n)
{
Console.WriteLine(n + 1); // 43
}
Console.WriteLine(value is string); // False

查看运行时的类型名

object value = 42;
Console.WriteLine(value.GetType().Name); // Int32
Console.WriteLine(typeof(List<int>).Name); // List`1

数字字面量:下划线分隔、十六进制、二进制;long 加后缀 L

Console.WriteLine(1_000_000); // 1000000
Console.WriteLine(0xFF); // 255
Console.WriteLine(0b1010); // 10
Console.WriteLine(5_000_000_000L); // 5000000000

char 是一个 UTF-16 码元,可以和整数互转

Console.WriteLine((int)'A'); // 65
Console.WriteLine((char)('A' + 2)); // C

插值:字符串前加 $,表达式写在花括号里

var name = "小明";
var age = 18;
Console.WriteLine($"{name} 今年 {age} 岁"); // 小明 今年 18 岁

格式:小数位、千分位、百分比、补零、宽度(负数左对齐)

using System.Globalization;
Console.WriteLine(string.Create(CultureInfo.InvariantCulture,
$"{3.14159:F2}|{1234567:N0}|{0.25:P0}|{42:D5}|{"右",4}|{"左",-4}|"));
// 3.14|1,234,567|25 %|00042| 右|左 |

拼接少量用 + 或插值,拼接一列用 string.Join

string[] parts = ["2026", "10", "08"];
Console.WriteLine("a" + "b"); // ab
Console.WriteLine(string.Join("-", parts)); // 2026-10-08

循环里拼很多段用 StringBuilder

using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 3; i++)
{
sb.Append(i).Append(',');
}
Console.WriteLine(sb.ToString().TrimEnd(',')); // 0,1,2

取字符和子串:^1 从末尾数,a..b 是范围,不含结束位置

var s = "CSharp";
Console.WriteLine($"{s[0]} {s[^1]} {s[..2]} {s[^4..]}"); // C p CS harp

查找和比较:Contains、StartsWith、IndexOf、Equals;StartsWith 和 IndexOf 的字符串参数要配 StringComparison.Ordinal,否则按当前区域设置比较;忽略大小写用 OrdinalIgnoreCase

var s = "hello world";
Console.WriteLine(s.Contains("wor")); // True
Console.WriteLine(s.IndexOf('o')); // 4
Console.WriteLine(s.IndexOf("z", StringComparison.Ordinal)); // -1
Console.WriteLine(s.StartsWith("he", StringComparison.Ordinal)); // True
Console.WriteLine(string.Equals("Hello", "hello", StringComparison.OrdinalIgnoreCase)); // True

替换:Replace 替换所有出现的地方

Console.WriteLine("a-b-c".Replace("-", "/")); // a/b/c

用正则替换和提取

using System.Text.RegularExpressions;
var s = "订单 12 和 345";
Console.WriteLine(Regex.Replace(s, @"\d+", "#")); // 订单 # 和 #
Console.WriteLine(string.Join(" ", Regex.Matches(s, @"\d+").Select(m => m.Value))); // 12 345

分割:StringSplitOptions 去掉空项、去掉每项两端的空白

Console.WriteLine(string.Join("|", "x,y,,z".Split(','))); // x|y||z
var options = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries;
Console.WriteLine(string.Join("|", " a , b ,, c ".Split(',', options))); // a|b|c

大小写:用 Invariant 版本,结果不随区域设置变

var s = "hello World";
Console.WriteLine(s.ToUpperInvariant()); // HELLO WORLD
Console.WriteLine(s.ToLowerInvariant()); // hello world

去掉两端空白,或只去开头、结尾;补齐到指定宽度;重复一个字符

var s = " hi ";
Console.WriteLine($"[{s.Trim()}][{s.TrimStart()}][{s.TrimEnd()}]"); // [hi][hi ][ hi]
Console.WriteLine("7".PadLeft(3, '0')); // 007
Console.WriteLine(new string('-', 5)); // -----

多行字符串用原始字符串字面量 """:结尾 """ 前面的缩进会从每一行去掉

var sql = """
SELECT *
FROM users
""";
Console.WriteLine(sql);
// SELECT *
// FROM users

Length 数的是 UTF-16 码元,不是字符;按码点数用 EnumerateRunes,按用户看到的字符数用 StringInfo,详见第 14 章

using System.Globalization;
using System.Text;
var s = "中😀";
Console.WriteLine(s.Length); // 3
Console.WriteLine(s.EnumerateRunes().Count()); // 2
Console.WriteLine(new StringInfo(s).LengthInTextElements); // 2
Console.WriteLine(Encoding.UTF8.GetByteCount(s)); // 7

字符串和 UTF-8 字节互转

using System.Text;
byte[] bytes = Encoding.UTF8.GetBytes("你好");
Console.WriteLine(bytes.Length); // 6
Console.WriteLine(Encoding.UTF8.GetString(bytes)); // 你好

按中文拼音排序:用 zh-CN 区域设置的比较器

using System.Globalization;
string[] fruits = ["香蕉", "苹果", "橙子"];
var zh = StringComparer.Create(CultureInfo.GetCultureInfo("zh-CN"), ignoreCase: false);
Console.WriteLine(string.Join(" ", fruits.Order(zh))); // 橙子 苹果 香蕉

列表 List<T>:集合表达式 [...] 创建;Add 加到末尾,Insert 插入,RemoveAt 按下标删,Remove 按值删

List<int> nums = [3, 1, 2];
nums.Add(4);
nums.Insert(0, 0);
nums.RemoveAt(1); // 删下标 1 的元素
nums.Remove(4); // 删第一个等于 4 的元素
Console.WriteLine(string.Join(" ", nums)); // 0 1 2

列表:索引、修改、长度、包含、切片;数组长度固定,用 Length,new int[3] 的元素是默认值 0

List<int> nums = [10, 20, 30, 40];
nums[0] = 11;
Console.WriteLine($"{nums[^1]} {nums.Count} {nums.Contains(20)}"); // 40 4 True
Console.WriteLine(string.Join(" ", nums[1..3])); // 20 30
int[] zeros = new int[3];
Console.WriteLine($"{zeros.Length} {zeros[0]}"); // 3 0

排序:Sort 原地排;LINQ 的 Order、OrderBy、ThenBy 返回新序列,原集合不动

List<int> nums = [10, 9, 1];
nums.Sort();
Console.WriteLine(string.Join(" ", nums)); // 1 9 10
string[] words = ["banana", "Apple", "cherry"];
var sorted = words.OrderByDescending(w => w.Length).ThenBy(w => w, StringComparer.Ordinal);
Console.WriteLine(string.Join(" ", sorted)); // banana cherry Apple

变换和筛选:LINQ 的 Where、Select(相当于推导式),ToList 收集结果;Enumerable.Range 按规则生成序列,详见第 13 章

List<int> nums = [1, 2, 3, 4];
List<int> squares = nums.Where(n => n % 2 == 0).Select(n => n * n).ToList();
Console.WriteLine(string.Join(" ", squares)); // 4 16
Console.WriteLine(string.Join(" ", Enumerable.Range(0, 4).Select(i => i * i))); // 0 1 4 9

查找:First 找不到抛异常,FirstOrDefault 找不到返回默认值;Any、All 判断有没有、是不是全部

List<int> nums = [3, 8, 12];
Console.WriteLine(nums.First(n => n > 5)); // 8
Console.WriteLine(nums.FindIndex(n => n > 5)); // 1
Console.WriteLine(nums.FirstOrDefault(n => n > 100)); // 0
Console.WriteLine($"{nums.Any(n => n > 10)} {nums.All(n => n > 0)}"); // True True

汇总:Sum、Max、Min,通用的累积用 Aggregate

List<int> nums = [3, 1, 4];
Console.WriteLine($"{nums.Sum()} {nums.Max()} {nums.Min()}"); // 8 4 1
Console.WriteLine(nums.Aggregate(1, (acc, n) => acc * n)); // 12

字典 Dictionary<K, V>:索引器赋值是新增或覆盖,Remove 删除,遍历时解构成键和值

var ages = new Dictionary<string, int> { ["小明"] = 18, ["小红"] = 20 };
ages["小刚"] = 19;
ages.Remove("小红");
Console.WriteLine($"{ages["小明"]} {ages.ContainsKey("小刚")} {ages.Count}"); // 18 True 2
foreach (var (name, age) in ages)
{
Console.WriteLine($"{name} {age}"); // 依次打印 小明 18、小刚 19
}

字典取值:键不存在时索引器抛 KeyNotFoundException,用 TryGetValue 或 GetValueOrDefault

var ages = new Dictionary<string, int> { ["小明"] = 18 };
Console.WriteLine(ages.TryGetValue("小李", out var age) ? age : -1); // -1
Console.WriteLine(ages.GetValueOrDefault("小李", 0)); // 0

Dictionary 不保证遍历顺序;要按插入顺序遍历用 OrderedDictionary<K, V>(.NET 9 起),详见第 12 章

var steps = new OrderedDictionary<string, int> { ["b"] = 1, ["a"] = 2 };
steps.Remove("b");
steps["b"] = 3;
Console.WriteLine(string.Join(" ", steps.Keys)); // a b

分组:GroupBy

string[] words = ["a", "bb", "cc", "d"];
foreach (var g in words.GroupBy(w => w.Length))
{
Console.WriteLine($"{g.Key}: {string.Join(" ", g)}"); // 依次打印 1: a d、2: bb cc
}

计数:CountBy(.NET 9 起)

foreach (var (ch, count) in "banana".CountBy(c => c))
{
Console.WriteLine($"{ch} {count}"); // 依次打印 b 1、a 3、n 2
}

集合 HashSet<T>:去重;LINQ 的 Union、Intersect、Except 返回新序列,原地修改用 UnionWith 等

HashSet<int> a = [1, 2, 3];
HashSet<int> b = [2, 3, 4];
a.Add(5);
Console.WriteLine(string.Join(" ", a.Union(b))); // 1 2 3 5 4
Console.WriteLine(string.Join(" ", a.Intersect(b))); // 2 3
Console.WriteLine(string.Join(" ", a.Except(b))); // 1 5
int[] dup = [1, 1, 2];
Console.WriteLine(string.Join(" ", dup.Distinct())); // 1 2

集合表达式里用 .. 展开另一个集合

int[] a = [1, 2];
int[] b = [3];
int[] all = [.. a, .. b, 4];
Console.WriteLine(string.Join(" ", all)); // 1 2 3 4

不可变集合:修改操作返回新集合,原集合不变,详见第 12 章

using System.Collections.Immutable;
ImmutableList<int> xs = [1, 2];
var ys = xs.Add(3);
Console.WriteLine($"{xs.Count} {ys.Count}"); // 2 3

元组:临时把几个值放在一起,字段可以起名

(string Name, int Age) pair = ("小明", 18);
Console.WriteLine($"{pair.Name} {pair.Age}"); // 小明 18
Console.WriteLine(pair); // (小明, 18)

if / else if / else,条件要加括号

int score = 85;
string grade;
if (score >= 90)
{
grade = "A";
}
else if (score >= 80)
{
grade = "B";
}
else
{
grade = "C";
}
Console.WriteLine(grade); // B

三元表达式

int n = 7;
Console.WriteLine(n % 2 == 1 ? "奇数" : "偶数"); // 奇数

== 对数字、字符串、record 比较值,对一般的类比较引用,详见第 5 章

string a = "hi";
string b = string.Concat("h", "i");
Console.WriteLine(a == b); // True

foreach 遍历数组、列表、字典、字符串等

foreach (var ch in "ab")
{
Console.WriteLine(ch); // 依次打印 a、b
}

计数循环

for (int i = 0; i < 10; i += 3)
{
Console.WriteLine(i); // 依次打印 0 3 6 9
}

同时要下标和元素:Index()(.NET 9 起)

string[] letters = ["a", "b"];
foreach (var (i, s) in letters.Index())
{
Console.WriteLine($"{i} {s}"); // 依次打印 0 a、1 b
}

while、break、continue

int n = 0;
while (true)
{
n += 1;
if (n % 2 == 0) continue;
if (n > 5) break;
Console.WriteLine(n); // 依次打印 1 3 5
}

break 只跳出一层;跳出多层循环时把循环放进局部函数,用 return

PrintUntilThree();
void PrintUntilThree()
{
int[][] rows = [[1, 2], [3, 4]];
foreach (var row in rows)
{
foreach (var x in row)
{
if (x == 3) return;
Console.WriteLine(x); // 依次打印 1 2
}
}
}

switch 语句:不会往下贯穿,每段要以 break 或 return 结束;几个 case 可以叠在一起

int code = 404;
switch (code)
{
case 200:
case 201:
Console.WriteLine("成功");
break;
case 404:
Console.WriteLine("找不到"); // 找不到
break;
default:
Console.WriteLine("其他");
break;
}

switch 表达式:每个分支写成 模式 => 值,_ 兜底,详见第 15 章

int score = 85;
string grade = score switch
{
>= 90 => "A",
>= 80 => "B",
_ => "C",
};
Console.WriteLine(grade); // B

按结构匹配:类型模式、属性模式、位置模式、列表模式,详见第 15 章

string Describe(object e) => e switch
{
Click { X: 0, Y: 0 } => "点击原点",
Click(var x, var y) => $"点击 ({x}, {y})",
int[] and [var dx, var dy] => $"移动 {dx},{dy}",
_ => "未知",
};
int[] move = [3, 0];
Console.WriteLine(Describe(new Click(1, 2))); // 点击 (1, 2)
Console.WriteLine(Describe(move)); // 移动 3,0
record Click(int X, int Y);

C# 14 没有可辨识联合,用抽象 record 加几个派生 record 代替,再用 switch 表达式分派;层级是开放的,不写 _ 兜底会得到警告 CS8509,详见第 15 章

Console.WriteLine(Area(new Square(3))); // 9
double Area(Shape s) => s switch
{
Circle c => Math.PI * c.R * c.R,
Square q => q.Side * q.Side,
_ => throw new ArgumentOutOfRangeException(nameof(s)),
};
abstract record Shape;
record Circle(double R) : Shape;
record Square(double Side) : Shape;

模式可以用 and、or、not 组合

char c = 'x';
Console.WriteLine(c is >= 'a' and <= 'z' or >= 'A' and <= 'Z'); // True
Console.WriteLine(c is not 'y'); // True

do...while:先执行一次再判断

int tries = 0;
do
{
tries += 1;
} while (tries < 3);
Console.WriteLine(tries); // 3

定义函数:顶层语句里写局部函数,参数可以有默认值

Console.WriteLine(Greet("小明")); // 你好,小明
Console.WriteLine(Greet("小明", "早上好")); // 早上好,小明
string Greet(string name, string greeting = "你好") => $"{greeting},{name}";

命名参数:调用时写 参数名:,可以不按顺序

Console.WriteLine(Greet(greeting: "晚上好", name: "小红")); // 晚上好,小红
string Greet(string name, string greeting = "你好") => $"{greeting},{name}";

默认值必须是编译期常量,编译进调用方;要默认一个新列表就默认 null,在函数里 ??=,详见第 7 章

Console.WriteLine(AddItem("a").Count); // 1
Console.WriteLine(AddItem("b").Count); // 1
List<string> AddItem(string item, List<string>? items = null)
{
items ??= [];
items.Add(item);
return items;
}

可变参数 params:C# 13 起可以用 ReadOnlySpan<T> 等集合类型,传数组也行

int[] more = [4, 5];
Console.WriteLine(Total(1, 2, 3)); // 6
Console.WriteLine(Total(more)); // 9
int Total(params ReadOnlySpan<int> nums)
{
int sum = 0;
foreach (var n in nums) sum += n;
return sum;
}

返回多个值:返回带名字的元组,调用方解构

var (lo, hi) = MinMax([3, 1, 4]);
Console.WriteLine($"{lo} {hi}"); // 1 4
(int Min, int Max) MinMax(int[] nums) => (nums.Min(), nums.Max());

out 参数:TryXxx 模式,返回是否成功,结果从 out 带出来

if (TryHalf(10, out int half))
{
Console.WriteLine(half); // 5
}
bool TryHalf(int n, out int result)
{
result = n / 2;
return n % 2 == 0;
}

lambda:Func<参数..., 返回值> 有返回值,Action<参数...> 没有;写明参数类型时 var 能推断出委托类型

Func<int, int> square = x => x * x;
var add = (int a, int b) => a + b;
Action<string> log = msg => Console.WriteLine($"[日志] {msg}");
log($"{square(5)} {add(1, 2)}"); // [日志] 25 3

闭包:lambda 记住并能修改外层变量,详见第 7 章

var next = MakeCounter();
next();
Console.WriteLine(next()); // 2
Func<int> MakeCounter()
{
int count = 0;
return () => ++count;
}

函数是普通的值:可以传入、返回、放进字典;方法名也能直接当委托传

var ops = new Dictionary<string, Func<int, int>> { ["double"] = x => x * 2, ["neg"] = x => -x };
int Apply(Func<int, int> f, int x) => f(x);
Console.WriteLine($"{Apply(ops["double"], 5)} {Apply(Math.Abs, -3)}"); // 10 3

重载:同名方法按参数类型区分(局部函数不能重载,要写在类里),详见第 7 章

Console.WriteLine(Fmt.Describe(1)); // 整数 1
Console.WriteLine(Fmt.Describe("a")); // 字符串 a
static class Fmt
{
public static string Describe(int n) => $"整数 {n}";
public static string Describe(string s) => $"字符串 {s}";
}

包装函数:用高阶函数给函数加功能

Func<int, int, int> Logged(string name, Func<int, int, int> fn) => (a, b) =>
{
Console.WriteLine($"调用 {name}({a}, {b})");
return fn(a, b);
};
var add = Logged("add", (a, b) => a + b);
Console.WriteLine(add(1, 2)); // 先打印"调用 add(1, 2)",再打印 3

表达式主体:只有一个表达式的方法、属性用 =>

Console.WriteLine(new Circle(2).Diameter); // 4
class Circle(int r)
{
public int Diameter => r * 2;
}

定义类:主构造函数(C# 12)的参数在整个类里可用;属性用 { get; },private set 只许类内修改

var acc = new Account("小明");
acc.Deposit(100);
Console.WriteLine($"{acc.Owner} {acc.Balance}"); // 小明 100
class Account(string owner)
{
public string Owner { get; } = owner;
public decimal Balance { get; private set; }
public void Deposit(decimal amount) => Balance += amount;
}

对象初始化器:init 属性只能在创建时赋值,required 要求创建时必须赋值(漏写编译错误 CS9035)

var u = new User { Name = "小明", Age = 18 };
Console.WriteLine($"{u.Name} {u.Age}"); // 小明 18
class User
{
public required string Name { get; init; }
public int Age { get; init; }
}

属性里做校验:C# 14 的 field 关键字指代编译器生成的后备字段;只读的计算属性用 =>

var t = new Temperature { Celsius = 100 };
Console.WriteLine(t.Fahrenheit); // 212
class Temperature
{
public double Celsius
{
get;
set => field = value >= -273.15 ? value : throw new ArgumentOutOfRangeException(nameof(value), "低于绝对零度");
}
public double Fahrenheit => Celsius * 9 / 5 + 32;
}

字符串表示:重写 ToString,打印和插值都会用它

var p = new Point(1, 2);
Console.WriteLine(p); // (1, 2)
Console.WriteLine($"点 {p}"); // 点 (1, 2)
class Point(int x, int y)
{
public override string ToString() => $"({x}, {y})";
}

只装数据用 record:自动生成 ToString、按值比较的 ==,with 复制一份改几处,详见第 5 章

var a = new User("小明", 18);
var b = a with { Age = 19 };
Console.WriteLine(a); // User { Name = 小明, Age = 18 }
Console.WriteLine(a == new User("小明", 18)); // True
Console.WriteLine(b.Age); // 19
record User(string Name, int Age);

成员不写修饰符就是 private;public、protected、internal 要写明,详见第 11 章

var c = new Counter();
c.Inc();
Console.WriteLine(c.Inc()); // 2
class Counter
{
int _count; // 不写修饰符就是 private
public int Inc() => ++_count;
}

继承用 :,构造参数传给父类;方法默认不能重写,父类写 virtual、子类写 override;sealed 禁止再被继承,详见第 8 章

Animal pet = new Dog("旺财");
Console.WriteLine(pet.Speak()); // 旺财:汪
class Animal(string name)
{
public string Name { get; } = name;
public virtual string Speak() => "...";
}
sealed class Dog(string name) : Animal(name)
{
public override string Speak() => $"{Name}:汪";
}

静态成员属于类本身,常用作备用构造方法

var p = Person.FromBirthYear("小明", 2000);
Console.WriteLine($"{p.Name} {Person.Count}"); // 小明 1
class Person
{
public static int Count { get; private set; }
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
Count++;
}
public static Person FromBirthYear(string name, int year) => new(name, DateTime.Now.Year - year);
}

struct 是值类型,赋值就复制一份;record struct 同时带上按值比较和 ToString,详见第 5 章

var a = new Point(1, 2);
var b = a; // 复制一份
b.X = 9;
Console.WriteLine($"{a} {b}"); // Point { X = 1, Y = 2 } Point { X = 9, Y = 2 }
record struct Point(int X, int Y);

扩展成员(C# 14):给已有类型加方法和属性,不用改它的源码

Console.WriteLine(" ".IsBlank); // True
Console.WriteLine("abc".Shout()); // ABC!
static class StringExtensions
{
extension(string s)
{
public bool IsBlank => string.IsNullOrWhiteSpace(s);
public string Shout() => s.ToUpperInvariant() + "!";
}
}

枚举 enum:本质是整数,能和名字互转,详见第 15 章

var c = Color.Green;
Console.WriteLine($"{c} {(int)c}"); // Green 1
Console.WriteLine(Enum.Parse<Color>("Blue")); // Blue
Console.WriteLine(string.Join(" ", Enum.GetValues<Color>())); // Red Green Blue
enum Color { Red, Green, Blue }

定义和实现接口:类要在声明里写明实现了哪个接口,方法对得上但没写明的不算,详见第 8 章

IShape[] shapes = [new Square(2), new Square(3)];
Console.WriteLine(shapes.Sum(s => s.Area())); // 13
interface IShape
{
double Area();
}
class Square(double side) : IShape
{
public double Area() => side * side;
}

接口里可以有属性;一个类可以实现多个接口

var f = new TextFile("a.txt");
Console.WriteLine(f.Name); // a.txt
f.Dispose(); // 已关闭 a.txt
interface INamed
{
string Name { get; }
}
class TextFile(string name) : INamed, IDisposable
{
public string Name => name;
public void Dispose() => Console.WriteLine($"已关闭 {Name}");
}

默认接口方法:接口给出实现,类可以不写;只能通过接口类型调用

IGreeter g = new Robot();
Console.WriteLine(g.Greet("小明")); // 你好,小明(来自机器人)
interface IGreeter
{
string Name { get; }
string Greet(string who) => $"你好,{who}(来自{Name})";
}
class Robot : IGreeter
{
public string Name => "机器人";
}

抽象类:可以带普通方法和状态,子类必须实现抽象方法;抽象类不能 new

var s = new MemoryStorage();
s.SaveAll(new Dictionary<string, string> { ["a"] = "1" });
Console.WriteLine(s.Count); // 1
abstract class Storage
{
public abstract void Save(string key, string value);
public void SaveAll(Dictionary<string, string> items)
{
foreach (var (k, v) in items) Save(k, v);
}
}
class MemoryStorage : Storage
{
private readonly Dictionary<string, string> _data = [];
public int Count => _data.Count;
public override void Save(string key, string value) => _data[key] = value;
}

运行时判断对象实现了某个接口:is 模式

object x = new MemoryStream();
if (x is IDisposable d)
{
d.Dispose();
Console.WriteLine("已释放"); // 已释放
}

实现标准接口 IComparable<T>,Sort、Order 就会用它排序

List<Money> list = [new(300), new(100)];
list.Sort();
Console.WriteLine(string.Join(" ", list)); // Money { Cents = 100 } Money { Cents = 300 }
record Money(int Cents) : IComparable<Money>
{
public int CompareTo(Money? other) => Cents.CompareTo(other?.Cents ?? 0);
}

没有结构化的接口,用适配器类代替:别人的类型方法对得上但没声明实现接口时,包一层再实现,详见第 8 章

ISpeaker s = new DuckAdapter(new Duck());
Console.WriteLine(s.Speak()); // 嘎
interface ISpeaker
{
string Speak();
}
class Duck
{
public string Speak() => "嘎";
}
class DuckAdapter(Duck duck) : ISpeaker
{
public string Speak() => duck.Speak();
}

泛型方法:类型参数写在方法名后的尖括号里,调用时一般能推断,详见第 9 章

int[] nums = [3, 1, 2];
string[] words = ["a", "b"];
Console.WriteLine(First(nums)); // 3
Console.WriteLine(First(words)); // a
T First<T>(IReadOnlyList<T> items) => items[0];

泛型类

var s = new SimpleStack<int>();
s.Push(1);
s.Push(2);
Console.WriteLine(s.Pop()); // 2
class SimpleStack<T>
{
private readonly List<T> _items = [];
public void Push(T item) => _items.Add(item);
public T Pop()
{
var item = _items[^1];
_items.RemoveAt(_items.Count - 1);
return item;
}
}

约束:where 限定类型参数必须实现某个接口

Console.WriteLine(Larger(3, 7)); // 7
Console.WriteLine(Larger("pear", "apple")); // pear
T Larger<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;

多个类型参数;泛型 record

var p = new Pair<string, int>("小明", 18);
Console.WriteLine(p); // Pair { First = 小明, Second = 18 }
record Pair<TFirst, TSecond>(TFirst First, TSecond Second);

泛型数学:INumber<T> 约束让 +、T.Zero 对所有数字类型可用(接口里的静态抽象成员),详见第 9 章

using System.Numerics;
int[] ints = [1, 2, 3];
double[] doubles = [1.5, 2.5];
Console.WriteLine(Sum(ints)); // 6
Console.WriteLine(Sum(doubles)); // 4
T Sum<T>(IEnumerable<T> items) where T : INumber<T>
{
T total = T.Zero;
foreach (var x in items) total += x;
return total;
}

其他约束:new() 要求有无参构造方法,还有 class、struct、notnull

var list = Create<List<int>>();
Console.WriteLine(list.Count); // 0
T Create<T>() where T : new() => new T();

类型参数在运行时保留,typeof(T) 拿得到实际类型,详见第 9 章

Console.WriteLine(NameOf<int>()); // Int32
Console.WriteLine(NameOf<List<string>>()); // List`1
string NameOf<T>() => typeof(T).Name;

default 取类型参数的默认值:值类型是 0 一类,引用类型是 null

int[] empty = [];
string[] none = [];
Console.WriteLine(FirstOrDefault(empty)); // 0
Console.WriteLine(FirstOrDefault(none) is null); // True
T? FirstOrDefault<T>(T[] items) => items.Length > 0 ? items[0] : default;

协变:IEnumerable<string> 可以当 IEnumerable<object> 用(接口声明了 out T)

List<string> names = ["a", "b"];
IEnumerable<object> objs = names;
Console.WriteLine(objs.Count()); // 2

抛出异常:参数检查用 .NET 自带的 ThrowIf... 辅助方法

Console.WriteLine(Sqrt(9)); // 3
double Sqrt(double x)
{
ArgumentOutOfRangeException.ThrowIfNegative(x);
return Math.Sqrt(x);
}

try / catch / finally:finally 总会执行,详见第 10 章

try
{
int.Parse("abc");
}
catch (FormatException e)
{
Console.WriteLine(e.GetType().Name); // FormatException
}
finally
{
Console.WriteLine("结束"); // 结束
}

按类型分开 catch,从具体到一般;when 加过滤条件

using System.Net;
try
{
throw new HttpRequestException("服务不可用", null, HttpStatusCode.ServiceUnavailable);
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.ServiceUnavailable)
{
Console.WriteLine("稍后重试"); // 稍后重试
}
catch (Exception e)
{
Console.WriteLine($"其他错误:{e.Message}");
}

原样再抛出用 throw;,保留原来的调用栈

try
{
try
{
throw new InvalidOperationException("坏了");
}
catch (InvalidOperationException)
{
Console.WriteLine("记录日志"); // 记录日志
throw;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message); // 坏了
}

自定义异常:继承 Exception

try
{
throw new ConfigException("app.json", "端口超出范围");
}
catch (ConfigException e)
{
Console.WriteLine($"{e.Path} {e.Message}"); // app.json app.json:端口超出范围
}
class ConfigException(string path, string reason) : Exception($"{path}:{reason}")
{
public string Path { get; } = path;
}

包一层再抛:把原始异常作为 innerException 传进去

try
{
LoadPort("abc");
}
catch (InvalidOperationException e)
{
Console.WriteLine($"{e.Message} 原因:{e.InnerException?.GetType().Name}"); // 端口配置无效:abc 原因:FormatException
}
int LoadPort(string text)
{
try
{
return int.Parse(text);
}
catch (FormatException e)
{
throw new InvalidOperationException($"端口配置无效:{text}", e);
}
}

资源清理:using 声明,离开所在的块时自动调用 Dispose(),出错也一样,详见第 10 章

Run();
void Run()
{
using var section = new Section("求和");
Console.WriteLine("计算中");
}
// 先打印"计算中",再打印"求和结束"
class Section(string label) : IDisposable
{
public void Dispose() => Console.WriteLine($"{label}结束");
}

异步资源用 await using 和 IAsyncDisposable

await QueryAsync();
async Task QueryAsync()
{
await using var conn = new Conn();
Console.WriteLine("查询中");
}
// 先打印"查询中",再打印"连接已关闭"
class Conn : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
Console.WriteLine("连接已关闭");
return ValueTask.CompletedTask;
}
}

异步方法里的异常在 await 处抛出,照样用 try / catch

try
{
await FetchUserAsync();
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message); // 网络断了
}
async Task FetchUserAsync()
{
await Task.Delay(10);
throw new InvalidOperationException("网络断了");
}

Task.WhenAll 里几个任务都失败时,await 只抛第一个;全部异常在任务的 Exception.InnerExceptions 里

var all = Task.WhenAll(FailAsync("a"), FailAsync("b"));
try
{
await all;
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message); // a
Console.WriteLine(all.Exception!.InnerExceptions.Count); // 2
}
async Task FailAsync(string message)
{
await Task.Delay(10);
throw new InvalidOperationException(message);
}

可空引用类型:string? 可以是 null,string 不该是;把 null 交给 string 只出编译警告,不报错,详见第 6 章

string? nickname = null;
string name = "小明";
Console.WriteLine($"{nickname is null} {name.Length}"); // True 2

可空值类型 int?:HasValue 判断,Value 取值

int? age = null;
Console.WriteLine(age.HasValue); // False
age = 18;
Console.WriteLine(age.Value); // 18

?. 和 ?[]:左边是 null 就整体得 null,不抛异常

var user = new User(null);
string[]? tags = null;
Console.WriteLine(user.Profile?.Name ?? "无"); // 无
Console.WriteLine(tags?[0] ?? "无"); // 无
record User(Profile? Profile);
record Profile(string Name);

?? 为 null 时用默认值;??= 为 null 时才赋值

string? input = null;
Console.WriteLine(input ?? "默认"); // 默认
List<int>? cache = null;
cache ??= [];
cache.Add(1);
Console.WriteLine(cache.Count); // 1

C# 14 起 ?. 可以出现在赋值左边:对象为 null 时整句跳过

Customer? nobody = null;
nobody?.Name = "小明"; // nobody 为 null,什么也不做
Customer? c = new();
c?.Name = "小红";
Console.WriteLine(c?.Name); // 小红
class Customer
{
public string Name { get; set; } = "";
}

用 is null、is not null 判断,结果不受重载的 == 影响

string? FindUser(int id) => id == 1 ? "小明" : null;
if (FindUser(1) is string name)
{
Console.WriteLine(name); // 小明
}
Console.WriteLine(FindUser(2) is null); // True

! 告诉编译器“这里不是 null”,只消掉警告,运行时不检查,错了照样抛 NullReferenceException

var map = new Dictionary<string, string?> { ["a"] = "1" };
string s = map["a"]!;
Console.WriteLine(s); // 1

对可能为 null 的变量直接取成员,编译只给警告 CS8602;在 .csproj 里把可空警告当成错误

<WarningsAsErrors>nullable</WarningsAsErrors>

参数不许为 null 时在开头检查

try
{
Greet(null!);
}
catch (ArgumentNullException e)
{
Console.WriteLine(e.ParamName); // name
}
void Greet(string name)
{
ArgumentNullException.ThrowIfNull(name);
Console.WriteLine($"你好,{name}");
}

FirstOrDefault、GetValueOrDefault 找不到时返回默认值:引用类型是 null,int 是 0

List<string> names = ["a"];
int[] nums = [1, 2];
Console.WriteLine(names.FirstOrDefault(n => n.Length > 5) ?? "没找到"); // 没找到
Console.WriteLine(nums.FirstOrDefault(n => n > 5)); // 0

判断字符串为空或全是空白

Console.WriteLine(string.IsNullOrEmpty("")); // True
Console.WriteLine(string.IsNullOrWhiteSpace(" ")); // True

导入命名空间:using 写在文件开头

using System.Text;
var sb = new StringBuilder("ab");
Console.WriteLine(sb.Length); // 2

这些命名空间默认已经导入(隐式 using),不用写:System、System.Collections.Generic、System.IO、System.Linq、System.Net.Http、System.Threading、System.Threading.Tasks

List<int> nums = [1, 2, 3];
Console.WriteLine(nums.Sum()); // 6

别名:给命名空间或类型起个短名(C# 12 起元组这类类型也行)

using Point = (int X, int Y);
Point p = (1, 2);
Console.WriteLine(p.X + p.Y); // 3

using static:直接用一个类的静态成员,不写类名

using static System.Math;
Console.WriteLine(Max(Abs(-3), 2)); // 3

项目里所有 .cs 文件一起编译,不用按文件导入;用文件级命名空间组织,用的地方 using 命名空间,详见第 11 章

MathUtils.cs
namespace Demo.Utils;
public static class MathUtils
{
public static int Add(int a, int b) => a + b;
}
// Program.cs
using Demo.Utils;
Console.WriteLine(MathUtils.Add(1, 2)); // 3

.NET 10 的单文件程序只编译它自己,同目录的其他 .cs 不参与;要拆成多个文件就先转成项目(生成 .csproj,#: 指令搬进去)

终端窗口
dotnet project convert app.cs

可见性:public 谁都能用,internal 只在同一个程序集(项目)里能用,private 只在类型内部;顶层类型默认 internal,成员默认 private,详见第 11 章

Console.WriteLine(Pricing.WithTax(100)); // 113
public static class Pricing
{
internal const int TaxRatePercent = 13;
private static int Percent(int value, int rate) => value * rate / 100;
public static int WithTax(int price) => price + Percent(price, TaxRatePercent);
}

file 修饰的类型只在所在的源文件里可见

Console.WriteLine(Helper.Twice(4)); // 8
file static class Helper
{
public static int Twice(int n) => n * 2;
}

项目里给所有文件加同一个 using:.csproj 里写 Using 项

<ItemGroup>
<Using Include="System.Text.Json" />
</ItemGroup>

测试项目是另一个程序集,看不到 internal;在被测项目的 .csproj 里放行,详见第 11 章

<ItemGroup>
<InternalsVisibleTo Include="Calc.Tests" />
</ItemGroup>

foreach 背后是 GetEnumerator():MoveNext() 前进一步并返回有没有下一个,Current 读当前值,详见第 13 章

List<int> nums = [1, 2];
using var e = nums.GetEnumerator();
while (e.MoveNext())
{
Console.WriteLine(e.Current); // 依次打印 1 2
}

迭代器方法:返回 IEnumerable<T>,用 yield return 逐个交出,调用方要一个才算一个

Console.WriteLine(string.Join(" ", Fib().Take(8))); // 0 1 1 2 3 5 8 13
IEnumerable<int> Fib()
{
var (a, b) = (0, 1);
while (true)
{
yield return a;
(a, b) = (b, a + b);
}
}

自定义可迭代类:实现 IEnumerable<T>,GetEnumerator 写成迭代器方法

using System.Collections;
Console.WriteLine(string.Join(" ", new Countdown(3))); // 3 2 1
class Countdown(int start) : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
for (int n = start; n > 0; n--) yield return n;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

yield break 提前结束

int[] data = [1, 3, 5, 2, 9];
Console.WriteLine(string.Join(" ", UpTo(data, 4))); // 1 3
IEnumerable<int> UpTo(int[] items, int limit)
{
foreach (var x in items)
{
if (x > limit) yield break;
yield return x;
}
}

没有 yield* 那样的委托写法,用 foreach 逐个 yield return 代替(yield return 的机制详见第 13 章)

object[] nested = [1, new object[] { 2, new object[] { 3, 4 } }, 5];
Console.WriteLine(string.Join(" ", Flatten(nested))); // 1 2 3 4 5
IEnumerable<int> Flatten(object[] items)
{
foreach (var item in items)
{
if (item is object[] inner)
{
foreach (var x in Flatten(inner)) yield return x;
}
else
{
yield return (int)item;
}
}
}

LINQ 是惰性的,可以接在无穷序列后面

Console.WriteLine(string.Join(" ", Naturals().Where(n => n % 2 == 0).Select(n => n * n).Take(5))); // 0 4 16 36 64
IEnumerable<int> Naturals()
{
for (int n = 0; ; n++) yield return n;
}

LINQ 查询在遍历时才执行,看到的是遍历那一刻的数据;要固定结果就 ToList(),详见第 13 章

List<int> nums = [1, 2];
var doubled = nums.Select(n => n * 2);
var snapshot = doubled.ToList();
nums.Add(3);
Console.WriteLine(string.Join(" ", doubled)); // 2 4 6
Console.WriteLine(string.Join(" ", snapshot)); // 2 4

迭代器方法每遍历一次就从头执行一次,可以重复遍历

int calls = 0;
IEnumerable<int> One()
{
calls++;
yield return 1;
}
var seq = One();
Console.WriteLine($"{seq.Count()} {seq.Count()} {calls}"); // 1 1 2

分块和配对:Chunk、Zip

int[] nums = [1, 2, 3, 4, 5];
string[] names = ["a", "b"];
Console.WriteLine(string.Join(" | ", nums.Chunk(2).Select(c => string.Join(",", c)))); // 1,2 | 3,4 | 5
Console.WriteLine(string.Join(" ", names.Zip(nums, (s, n) => $"{s}{n}"))); // a1 b2

异步迭代器:返回 IAsyncEnumerable<T>,用 await foreach 消费

await foreach (var t in TicksAsync(3))
{
Console.WriteLine(t); // 依次打印 1 2 3
}
async IAsyncEnumerable<int> TicksAsync(int n)
{
for (int i = 1; i <= n; i++)
{
await Task.Delay(10);
yield return i;
}
}

async 方法返回 Task<T>,await 等它完成;等待用 Task.Delay,详见第 19 章

Console.WriteLine(await HelloAsync()); // 你好
async Task<string> HelloAsync()
{
await Task.Delay(100);
return "你好";
}

Task.WhenAll:同时等待多个任务,结果按传入顺序

int[] results = await Task.WhenAll(FetchAsync(1), FetchAsync(2), FetchAsync(3));
Console.WriteLine(string.Join(" ", results)); // 10 20 30
async Task<int> FetchAsync(int n)
{
await Task.Delay(100);
return n * 10;
}

Task.WhenAny:取最先完成的那个任务,再 await 它拿结果

Task<string> first = await Task.WhenAny(DelayedAsync("快", 50), DelayedAsync("慢", 200));
Console.WriteLine(await first); // 快
async Task<string> DelayedAsync(string value, int ms)
{
await Task.Delay(ms);
return value;
}

CPU 密集的工作用 Task.Run 放到线程池上,详见第 17 章

long result = await Task.Run(() => Fib(30));
Console.WriteLine(result); // 832040
long Fib(int n) => n < 2 ? n : Fib(n - 1) + Fib(n - 2);

限制并发数地处理一批数据:Parallel.ForEachAsync

var options = new ParallelOptions { MaxDegreeOfParallelism = 2 };
int total = 0;
await Parallel.ForEachAsync(Enumerable.Range(1, 10), options, async (n, token) =>
{
await Task.Delay(10, token);
Interlocked.Add(ref total, n);
});
Console.WriteLine(total); // 55

多个线程改同一个变量要加锁:lock 配 .NET 9 起的 Lock 类型,详见第 17 章

Lock gate = new();
int count = 0;
Parallel.For(0, 1000, _ =>
{
lock (gate)
{
count++;
}
});
Console.WriteLine(count); // 1000

简单的计数用原子操作 Interlocked

int hits = 0;
Parallel.For(0, 1000, _ => Interlocked.Increment(ref hits));
Console.WriteLine(hits); // 1000

lock 里不能 await(编译错误 CS1996);异步代码里互斥用 SemaphoreSlim,详见第 17 章

var mutex = new SemaphoreSlim(1, 1);
int balance = 0;
await Task.WhenAll(Enumerable.Range(0, 10).Select(async _ =>
{
await mutex.WaitAsync();
try
{
int current = balance;
await Task.Delay(1);
balance = current + 10;
}
finally
{
mutex.Release();
}
}));
Console.WriteLine(balance); // 100

超时:CancellationTokenSource 到时间自动取消,把 Token 传给支持取消的方法,详见第 21 章

using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
try
{
await Task.Delay(1000, cts.Token);
}
catch (OperationCanceledException e)
{
Console.WriteLine(e.GetType().Name); // TaskCanceledException
}

主动取消:调用 Cancel(),工作方在循环里检查 Token,详见第 21 章

using var cts = new CancellationTokenSource();
var work = CountAsync(cts.Token);
await Task.Delay(50);
cts.Cancel();
try
{
await work;
}
catch (OperationCanceledException)
{
Console.WriteLine("已取消"); // 已取消
}
async Task CountAsync(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
await Task.Delay(10, token);
}
}

给不支持取消的任务加等待上限:WaitAsync,超时抛 TimeoutException,原来的任务并不会停

try
{
await Task.Delay(1000).WaitAsync(TimeSpan.FromMilliseconds(100));
}
catch (TimeoutException)
{
Console.WriteLine("超时"); // 超时
}

生产者和消费者之间传数据用 Channel<T>,详见第 20 章

using System.Threading.Channels;
var channel = Channel.CreateBounded<int>(10);
var producer = Task.Run(async () =>
{
for (int i = 1; i <= 3; i++) await channel.Writer.WriteAsync(i);
channel.Writer.Complete();
});
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine(item); // 依次打印 1 2 3
}
await producer;

多线程共用的字典用 ConcurrentDictionary

using System.Collections.Concurrent;
var counts = new ConcurrentDictionary<string, int>();
Parallel.For(0, 100, i => counts.AddOrUpdate(i % 2 == 0 ? "偶" : "奇", 1, (_, c) => c + 1));
Console.WriteLine($"{counts["偶"]} {counts["奇"]}"); // 50 50

运行外部程序并读它的输出:Process,详见第 18 章

using System.Diagnostics;
var psi = new ProcessStartInfo("dotnet", "--version") { RedirectStandardOutput = true };
using var process = Process.Start(psi)!;
string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
Console.WriteLine(output.StartsWith("10.", StringComparison.Ordinal)); // True

整个读写文本文件:File 的异步方法,默认按 UTF-8(不带 BOM)

await File.WriteAllTextAsync("note.txt", "第一行\n第二行\n");
string[] lines = await File.ReadAllLinesAsync("note.txt");
Console.WriteLine(string.Join(" ", lines)); // 第一行 第二行

逐行读大文件:File.ReadLinesAsync 配 await foreach,不把整个文件读进内存

await File.WriteAllTextAsync("log.txt", "a\nb\n");
await foreach (var line in File.ReadLinesAsync("log.txt"))
{
Console.WriteLine(line); // 依次打印 a、b
}

追加写入

await File.AppendAllTextAsync("log.txt", "新的一行\n");

路径拼接用 Path.Combine,拆出文件名、扩展名、父目录

var p = Path.Combine("data", "2026", "report.csv");
Console.WriteLine(Path.GetFileName(p)); // report.csv
Console.WriteLine(Path.GetFileNameWithoutExtension(p)); // report
Console.WriteLine(Path.GetExtension(p)); // .csv
Console.WriteLine(Path.GetFileName(Path.GetDirectoryName(p))); // 2026

判断存在、建多级目录、删除目录

Directory.CreateDirectory("out/logs"); // 多级目录一次建好,已存在也不报错
await File.WriteAllTextAsync("out/logs/x.txt", "x");
Console.WriteLine(File.Exists("out/logs/x.txt")); // True
Directory.Delete("out", recursive: true);
Console.WriteLine(Directory.Exists("out")); // False

列出目录下的直接子项

foreach (var entry in new DirectoryInfo(".").EnumerateFileSystemInfos())
{
Console.WriteLine($"{entry.Name} {(entry is DirectoryInfo ? "目录" : "文件")}");
}

递归列出所有文件,按通配符筛选

foreach (var file in Directory.EnumerateFiles(".", "*.txt", SearchOption.AllDirectories))
{
Console.WriteLine(file);
}

对象和 JSON 互转:System.Text.Json 配源生成器 JsonSerializerContext;单文件程序默认做 AOT 分析,靠反射的 JsonSerializer.Serialize(obj) 会出 IL2026 警告,详见第 22 章

using System.Text.Json;
using System.Text.Json.Serialization;
var u = new User("Ada", ["a", "b"]);
string json = JsonSerializer.Serialize(u, AppJson.Default.User);
Console.WriteLine(json); // {"name":"Ada","tags":["a","b"]}
var back = JsonSerializer.Deserialize(json, AppJson.Default.User)!;
Console.WriteLine(back.Tags.Count); // 2
record User(string Name, List<string> Tags);
[JsonSerializable(typeof(User))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
partial class AppJson : JsonSerializerContext;

缩进输出、中文不转义:把 JsonSerializerOptions 传给上下文

using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Unicode;
var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
};
Console.WriteLine(JsonSerializer.Serialize(new User("小明", 18), new AppJson(options).User));
// {
// "Name": "小明",
// "Age": 18
// }
record User(string Name, int Age);
[JsonSerializable(typeof(User))]
partial class AppJson : JsonSerializerContext;

结构不固定的 JSON 用 JsonNode 按键和下标取值

using System.Text.Json.Nodes;
var node = JsonNode.Parse("""{"n": 1, "items": [1, 2], "x": null}""")!;
Console.WriteLine(node["items"]![1]!.GetValue<int>()); // 2
Console.WriteLine(node["x"] is null); // True

读写 JSON 文件:直接对文件流序列化

using System.Text.Json;
using System.Text.Json.Serialization;
await using (var output = File.Create("config.json"))
{
await JsonSerializer.SerializeAsync(output, new Config(8080), AppJson.Default.Config);
}
await using var input = File.OpenRead("config.json");
var config = await JsonSerializer.DeserializeAsync(input, AppJson.Default.Config);
Console.WriteLine(config!.Port); // 8080
record Config(int Port);
[JsonSerializable(typeof(Config))]
partial class AppJson : JsonSerializerContext;

临时目录:Directory.CreateTempSubdirectory 建,用完自己删

var dir = Directory.CreateTempSubdirectory("demo-");
await File.WriteAllTextAsync(Path.Combine(dir.FullName, "t.txt"), "临时");
Console.WriteLine(dir.Exists); // True
dir.Delete(recursive: true);
Console.WriteLine(Directory.Exists(dir.FullName)); // False

测试框架用微软的 MSTest:新建测试项目,引用被测项目

终端窗口
dotnet new mstest -o Calc.Tests
dotnet add Calc.Tests reference Calc

最小测试文件:[TestClass] 标测试类,[TestMethod] 标测试方法,断言用 Assert

Calc/Calculator.cs
namespace Calc;
public static class Calculator
{
public static int Add(int a, int b) => a + b;
}
// Calc.Tests/CalculatorTests.cs
namespace Calc.Tests;
[TestClass]
public sealed class CalculatorTests
{
[TestMethod]
public void Add_TwoNumbers_ReturnsSum()
{
Assert.AreEqual(3, Calculator.Add(1, 2));
}
}

运行测试:在测试项目或解决方案目录里

终端窗口
dotnet test

只跑名字里含某段文字的测试

终端窗口
dotnet test --filter "Name~Add"

一个测试方法跑多组数据:[DataRow],每组单独报告结果

Calc.Tests/SquareTests.cs
[TestClass]
public sealed class SquareTests
{
[TestMethod]
[DataRow(2, 4)]
[DataRow(3, 9)]
[DataRow(-1, 1)]
public void Square(int n, int expected)
{
Assert.AreEqual(expected, n * n);
}
}

断言会抛出某种异常:Assert.ThrowsExactly

Calc.Tests/ParseTests.cs
[TestClass]
public sealed class ParseTests
{
[TestMethod]
public void Parse_Garbage_Throws()
{
Assert.ThrowsExactly<FormatException>(() => int.Parse("abc"));
}
}

异步测试:测试方法返回 Task

Calc.Tests/AsyncTests.cs
[TestClass]
public sealed class AsyncTests
{
[TestMethod]
public async Task Compute_ReturnsAnswer()
{
int n = await Task.Run(() => 21 * 2);
Assert.AreEqual(42, n);
}
}

比较集合的内容用 CollectionAssert

Calc.Tests/ListTests.cs
[TestClass]
public sealed class ListTests
{
[TestMethod]
public void Sorted()
{
List<int> nums = [3, 1, 2];
nums.Sort();
CollectionAssert.AreEqual(new[] { 1, 2, 3 }, nums);
}
}

每个测试前准备数据:[TestInitialize]

Calc.Tests/StackTests.cs
[TestClass]
public sealed class StackTests
{
private List<int> _items = [];
[TestInitialize]
public void Setup() => _items = [1, 2, 3];
[TestMethod]
public void RemoveLast()
{
_items.RemoveAt(_items.Count - 1);
Assert.HasCount(2, _items);
}
[TestMethod]
public void CountIsThree()
{
Assert.HasCount(3, _items);
}
}

改了代码自动重跑测试

终端窗口
dotnet watch test

格式化:dotnet format 随 SDK 附带,按 .editorconfig 改写项目里的代码

终端窗口
dotnet format

只检查不改,有要改的地方就以非零退出码结束,适合放进 CI

终端窗口
dotnet format --verify-no-changes

只修空白,或只修代码风格、分析器的问题

终端窗口
dotnet format whitespace
dotnet format style
dotnet format analyzers

.editorconfig:放在仓库根目录,规定缩进和代码风格,还能调整某条分析器规则的级别

root = true
[*.cs]
indent_style = space
indent_size = 4
csharp_style_namespace_declarations = file_scoped:warning
dotnet_diagnostic.CA1305.severity = warning

lint 用编译器自带的分析器:.csproj 里提高分析级别,构建时检查代码风格,把警告当错误

<PropertyGroup>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

不改 .csproj,只在这次构建里把警告当错误

终端窗口
dotnet build -warnaserror

StartsWith、IndexOf、CompareTo 的字符串版本按当前区域设置比较 → 结果随区域设置变,看不见的字符(如软连字符 U+00AD)会被忽略 → 字符串参数一律配 StringComparison.Ordinal,排序传 StringComparer.Ordinal

var s = "co\u00ADop"; // 中间夹着一个看不见的软连字符
Console.WriteLine(s.IndexOf("coop")); // 0
Console.WriteLine(s.IndexOf("coop", StringComparison.Ordinal)); // -1

直接打印或拼接小数 → 按系统区域设置格式化,德语环境下 3.5 打印成 3,5 → 输出给程序读的数字一律用 CultureInfo.InvariantCulture

using System.Globalization;
double x = 3.5;
Console.WriteLine(x.ToString(CultureInfo.GetCultureInfo("de-DE"))); // 3,5
Console.WriteLine(x.ToString(CultureInfo.InvariantCulture)); // 3.5

for 循环里的 lambda 捕获循环变量 → 所有 lambda 共用同一个变量,都拿到最后的值 → 改用 foreach(每轮一个新变量),或先复制到循环体内的局部变量,详见第 7 章

List<Func<int>> fs = [];
for (int i = 0; i < 3; i++) fs.Add(() => i);
Console.WriteLine(string.Join(" ", fs.Select(f => f()))); // 3 3 3
List<Func<int>> gs = [];
foreach (var j in Enumerable.Range(0, 3)) gs.Add(() => j);
Console.WriteLine(string.Join(" ", gs.Select(f => f()))); // 0 1 2

写 async void → 调用方没法 await,里面的异常接不住,通常让进程崩溃 → 除了事件处理方法,一律返回 Task,详见第 19 章

try
{
await SaveAsync();
}
catch (IOException e)
{
Console.WriteLine($"已处理:{e.Message}"); // 已处理:写入失败
}
async Task SaveAsync() // 不要写成 async void
{
await Task.Delay(10);
throw new IOException("写入失败");
}

foreach 遍历列表时增删元素 → 抛 InvalidOperationException → 用 RemoveAll,或遍历副本

List<int> nums = [1, 2, 3, 4];
try
{
foreach (var n in nums)
{
if (n % 2 == 0) nums.Remove(n);
}
}
catch (InvalidOperationException)
{
Console.WriteLine("遍历时改了集合"); // 遍历时改了集合
}
nums.RemoveAll(n => n % 2 == 0);
Console.WriteLine(string.Join(" ", nums)); // 1 3

object 类型的变量装着数字时用 == → 比较的是两个装箱对象的引用,得 False → 用 Equals,或先转回具体类型,详见第 5 章

object a = 1;
object b = 1;
Console.WriteLine(a == b); // False
Console.WriteLine(a.Equals(b)); // True

List 里放 struct,想改 list[0].X → 索引器返回的是副本,编译错误 CS1612 → 整个换掉:list[0] = list[0] with { X = 9 },详见第 5 章

List<Point> points = [new(1, 2)];
points[0] = points[0] with { X = 9 };
Console.WriteLine(points[0]); // Point { X = 9, Y = 2 }
record struct Point(int X, int Y);

System.Text.Json 默认把中文转义成 \uXXXX → 文件里看不懂,和别的程序的输出对不上 → 设置 Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)(见“文件与 JSON”一节)

using System.Text.Json;
using System.Text.Json.Serialization;
Console.WriteLine(JsonSerializer.Serialize("小明", AppJson.Default.String)); // "\u5C0F\u660E"
[JsonSerializable(typeof(string))]
partial class AppJson : JsonSerializerContext;

单文件程序里对 obj.GetType() 的结果做反射 → dotnet run 打出 IL2075 裁剪警告 → 类型已知时用 typeof(T),确实要动态反射就加 #:property PublishAot=false,详见第 22 章

var names = typeof(User).GetProperties().Select(p => p.Name);
Console.WriteLine(string.Join(" ", names)); // Name Age
record User(string Name, int Age);

.Result、.Wait() 同步等任务 → 阻塞当前线程;有同步上下文的 UI 程序里,任务要回到这个线程才能完成,于是互相等着卡死 → 从调用链顶端到底一路 await,详见第 19 章

Console.WriteLine(await GetPortAsync()); // 8080
async Task<int> GetPortAsync()
{
await Task.Delay(10);
return 8080;
}