附录 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 --versiondotnet --list-sdksdotnet --list-runtimes查看完整的环境信息:SDK、运行时、操作系统、生效的 global.json
dotnet --info在代码里查看运行时版本
using System.Runtime.InteropServices;
Console.WriteLine(Environment.Version); // 10.0.12Console.WriteLine(RuntimeInformation.FrameworkDescription); // .NET 10.0.12Windows 上用 winget 安装 .NET 10 SDK
winget install Microsoft.DotNet.SDK.10macOS / Linux 上用官方安装脚本,装到用户目录,不需要管理员权限
curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.shbash 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 bConsole.WriteLine(string.Join(" ", args)); // a bConsole.WriteLine(args.Length); // 2单文件里引用 NuGet 包:#:package 包名@版本,写在文件开头
#:package Humanizer@3.0.10using Humanizer;
Console.WriteLine("some_property_name".Humanize()); // some property name解析命令行选项:标准库里没有现成的解析器,用 NuGet 包 System.CommandLine 代替
#:package System.CommandLine@2.0.12using System.CommandLine;
// 运行:dotnet run app.cs -- 小明 --times 2var 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=falseusing 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 rundotnet builddotnet 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;项目初始化与依赖管理
Section titled “项目初始化与依赖管理”新建控制台项目:生成 Demo/Demo.csproj 和 Demo/Program.cs
dotnet new console -o Democd 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 packagedotnet 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 Calcdotnet add Demo reference Calc用解决方案文件把几个项目放在一起(.NET 10 默认生成 .slnx 格式)
dotnet new sln -n Shopdotnet sln add Demo Calc没有 package.json 那样的 scripts,用 dotnet 的子命令代替:构建、运行、测试、格式化都是子命令,在解决方案目录里运行会作用于所有项目
dotnet builddotnet testdotnet formatvar 让编译器推断类型,推断出来就固定了;也可以写出类型,详见第 3 章
var name = "Ada"; // stringint 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 1const 是编译期常量,只能是数字、字符串、布尔这类字面量,编译时直接写进用到它的地方,详见第 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基本类型与转换
Section titled “基本类型与转换”字符串转整数:int.Parse 格式不对抛 FormatException;不确定格式时用 TryParse;Convert 可指定进制
Console.WriteLine(int.Parse("42")); // 42Console.WriteLine(int.TryParse("42px", out var n) ? n : -1); // -1Console.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); // 3Console.WriteLine((int)-3.9); // -3Console.WriteLine(Math.Floor(-3.9)); // -4整数除法:两个整数相除得整数,向零截断;% 的符号跟被除数;有一边是浮点数才得小数
Console.WriteLine(7 / 2); // 3Console.WriteLine(-7 / 2); // -3Console.WriteLine(-7 % 3); // -1Console.WriteLine(7 / 2.0 == 3.5); // True乘方用 Math.Pow,参数和结果都是 double
Console.WriteLine(Math.Pow(2, 10)); // 1024Math.Round 默认“四舍六入五成双”;要四舍五入写 MidpointRounding.AwayFromZero
Console.WriteLine(Math.Round(2.5)); // 2Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero)); // 3整数溢出默认静默回绕;放进 checked 里溢出抛 OverflowException
int max = int.MaxValue;Console.WriteLine(max + 1); // -2147483648try{ Console.WriteLine(checked(max + 1));}catch (OverflowException){ Console.WriteLine("溢出"); // 溢出}大整数用 BigInteger
using System.Numerics;
Console.WriteLine(BigInteger.Pow(2, 100)); // 1267650600228229401496703205376double 有二进制误差;金额用 decimal(字面量加 m)
Console.WriteLine(0.1 + 0.2 == 0.3); // FalseConsole.WriteLine(0.1m + 0.2m == 0.3m); // Truebool 和整数不能互相隐式转换,条件里必须是 bool;字符串转布尔用 bool.Parse(不分大小写)
int count = 3;Console.WriteLine(count != 0); // TrueConsole.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); // Int32Console.WriteLine(typeof(List<int>).Name); // List`1数字字面量:下划线分隔、十六进制、二进制;long 加后缀 L
Console.WriteLine(1_000_000); // 1000000Console.WriteLine(0xFF); // 255Console.WriteLine(0b1010); // 10Console.WriteLine(5_000_000_000L); // 5000000000char 是一个 UTF-16 码元,可以和整数互转
Console.WriteLine((int)'A'); // 65Console.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"); // abConsole.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")); // TrueConsole.WriteLine(s.IndexOf('o')); // 4Console.WriteLine(s.IndexOf("z", StringComparison.Ordinal)); // -1Console.WriteLine(s.StartsWith("he", StringComparison.Ordinal)); // TrueConsole.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||zvar 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 WORLDConsole.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')); // 007Console.WriteLine(new string('-', 5)); // -----多行字符串用原始字符串字面量 """:结尾 """ 前面的缩进会从每一行去掉
var sql = """ SELECT * FROM users """;Console.WriteLine(sql);// SELECT *// FROM usersLength 数的是 UTF-16 码元,不是字符;按码点数用 EnumerateRunes,按用户看到的字符数用 StringInfo,详见第 14 章
using System.Globalization;using System.Text;
var s = "中😀";Console.WriteLine(s.Length); // 3Console.WriteLine(s.EnumerateRunes().Count()); // 2Console.WriteLine(new StringInfo(s).LengthInTextElements); // 2Console.WriteLine(Encoding.UTF8.GetByteCount(s)); // 7字符串和 UTF-8 字节互转
using System.Text;
byte[] bytes = Encoding.UTF8.GetBytes("你好");Console.WriteLine(bytes.Length); // 6Console.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 TrueConsole.WriteLine(string.Join(" ", nums[1..3])); // 20 30int[] 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 10string[] 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 16Console.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)); // 8Console.WriteLine(nums.FindIndex(n => n > 5)); // 1Console.WriteLine(nums.FirstOrDefault(n => n > 100)); // 0Console.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 1Console.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 2foreach (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); // -1Console.WriteLine(ages.GetValueOrDefault("小李", 0)); // 0Dictionary 不保证遍历顺序;要按插入顺序遍历用 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 4Console.WriteLine(string.Join(" ", a.Intersect(b))); // 2 3Console.WriteLine(string.Join(" ", a.Except(b))); // 1 5int[] 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}"); // 小明 18Console.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); // Trueforeach 遍历数组、列表、字典、字符串等
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'); // TrueConsole.WriteLine(c is not 'y'); // Truedo...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); // 1Console.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)); // 6Console.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)); // 整数 1Console.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)); // TrueConsole.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); // TrueConsole.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 1Console.WriteLine(Enum.Parse<Color>("Blue")); // BlueConsole.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.txtf.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)); // 3Console.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)); // 7Console.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)); // 6Console.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>()); // Int32Console.WriteLine(NameOf<List<string>>()); // List`1
string NameOf<T>() => typeof(T).Name;default 取类型参数的默认值:值类型是 0 一类,引用类型是 null
int[] empty = [];string[] none = [];Console.WriteLine(FirstOrDefault(empty)); // 0Console.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); // Falseage = 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); // 1C# 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("")); // TrueConsole.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); // 3using static:直接用一个类的静态成员,不写类名
using static System.Math;
Console.WriteLine(Max(Abs(-3), 2)); // 3项目里所有 .cs 文件一起编译,不用按文件导入;用文件级命名空间组织,用的地方 using 命名空间,详见第 11 章
namespace Demo.Utils;
public static class MathUtils{ public static int Add(int a, int b) => a + b;}
// Program.csusing 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>迭代与生成器
Section titled “迭代与生成器”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 6Console.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 | 5Console.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); // 1000lock 里不能 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文件与 JSON
Section titled “文件与 JSON”整个读写文本文件: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.csvConsole.WriteLine(Path.GetFileNameWithoutExtension(p)); // reportConsole.WriteLine(Path.GetExtension(p)); // .csvConsole.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")); // TrueDirectory.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>()); // 2Console.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); // Truedir.Delete(recursive: true);Console.WriteLine(Directory.Exists(dir.FullName)); // False测试框架用微软的 MSTest:新建测试项目,引用被测项目
dotnet new mstest -o Calc.Testsdotnet add Calc.Tests reference Calc最小测试文件:[TestClass] 标测试类,[TestMethod] 标测试方法,断言用 Assert
namespace Calc;
public static class Calculator{ public static int Add(int a, int b) => a + b;}
// Calc.Tests/CalculatorTests.csnamespace 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],每组单独报告结果
[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
[TestClass]public sealed class ParseTests{ [TestMethod] public void Parse_Garbage_Throws() { Assert.ThrowsExactly<FormatException>(() => int.Parse("abc")); }}异步测试:测试方法返回 Task
[TestClass]public sealed class AsyncTests{ [TestMethod] public async Task Compute_ReturnsAnswer() { int n = await Task.Run(() => 21 * 2); Assert.AreEqual(42, n); }}比较集合的内容用 CollectionAssert
[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]
[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格式化与 lint
Section titled “格式化与 lint”格式化:dotnet format 随 SDK 附带,按 .editorconfig 改写项目里的代码
dotnet format只检查不改,有要改的地方就以非零退出码结束,适合放进 CI
dotnet format --verify-no-changes只修空白,或只修代码风格、分析器的问题
dotnet format whitespacedotnet format styledotnet format analyzers.editorconfig:放在仓库根目录,规定缩进和代码风格,还能调整某条分析器规则的级别
root = true
[*.cs]indent_style = spaceindent_size = 4csharp_style_namespace_declarations = file_scoped:warningdotnet_diagnostic.CA1305.severity = warninglint 用编译器自带的分析器:.csproj 里提高分析级别,构建时检查代码风格,把警告当错误
<PropertyGroup> <AnalysisLevel>latest-recommended</AnalysisLevel> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <TreatWarningsAsErrors>true</TreatWarningsAsErrors></PropertyGroup>不改 .csproj,只在这次构建里把警告当错误
dotnet build -warnaserrorStartsWith、IndexOf、CompareTo 的字符串版本按当前区域设置比较 → 结果随区域设置变,看不见的字符(如软连字符 U+00AD)会被忽略 → 字符串参数一律配 StringComparison.Ordinal,排序传 StringComparer.Ordinal
var s = "co\u00ADop"; // 中间夹着一个看不见的软连字符Console.WriteLine(s.IndexOf("coop")); // 0Console.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,5Console.WriteLine(x.ToString(CultureInfo.InvariantCulture)); // 3.5for 循环里的 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 3List<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 3object 类型的变量装着数字时用 == → 比较的是两个装箱对象的引用,得 False → 用 Equals,或先转回具体类型,详见第 5 章
object a = 1;object b = 1;Console.WriteLine(a == b); // FalseConsole.WriteLine(a.Equals(b)); // TrueList 里放 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;}