C#编程基础
C# 编程基础与实战
这是一份面向实际开发而不是考试的学习笔记。C# 是一个"类型与内存规则极其讲究、但又被现代语法包装得极其好用"的语言:它在值类型与引用类型之间划出严格分界线,把垃圾回收、线程池、编译器优化全部藏进运行时里,让你既贴近底层内存模型、又不需要手管内存。整份笔记的主线只有一条:先搞清楚"数据在内存里怎么放、谁负责还回去"(类型系统与 GC),再理解"代码怎么被组织、被调用、被延迟执行"(OOP、委托、LINQ、异步),最后把它们放进真实工程里(项目结构、依赖管理、文件与网络)。把所有特性挂在"内存模型"这根主轴上看,C# 就从一袋零散语法,变成一棵树。
学完你应该能回答三个开发问题:①为什么值类型和引用类型的对象"默认相等判断"行为不一样?这决定了你写
==和Equals时必须怎么想?(模块 3)②为什么 async/await 的代码可以像同步一样写,却不会卡死 UI 线程?背后的线程池与线程切换是什么?(模块 8)③用 EF Core、Dapper、ADO.NET 查同一张表,写出的代码差异那么大,背后的共同抽象 IEnumerable/IQueryable 到底是什么?(模块 5、7) 这三件事想通了,后面学 ASP.NET Core、WPF、Unity 都只是套壳。
一、内容大纲
| 模块 | 主题 | 核心内容 |
|---|---|---|
| 模块 1 | 开发环境与工具链 | .NET SDK 与运行时、csproj/解决方案、NuGet、CLI 工作流 |
| 模块 2 | C# 语言基础 | 类型系统、变量、控制流、字符串、异常 try/catch/finally |
| 模块 3 | 值类型 vs 引用类型 | stack/heap、装箱/拆箱、可空类型、GC 与资源释放 |
| 模块 4 | 面向对象 | 类、继承、多态、接口、抽象类、密封、访问修饰符、record/struct |
| 模块 5 | 泛型与集合 | List/Dictionary/HashSet、IEnumerable/IQueryable、泛型约束 |
| 模块 6 | 委托、事件与 lambda | Func/Action、事件发布订阅机制、闭包 |
| 模块 7 | LINQ | 查询语法与方法语法、延迟执行、常用操作符 |
| 模块 8 | 异步编程 | async/await、Task 与线程池、SynchronizationContext、死锁 |
| 模块 9 | 文件与序列化 | FileStream、文本读写、System.Text.Json |
| 模块 10 | 现代 C# 与生态 | 模式匹配、源生成器、ASP.NET Core/WPF/Unity 简述、陷阱清单 |
二、一条主线
C# 的所有机制都围绕一条链展开:值/引用类型的内存模型(栈与堆)→ 面向对象与接口(组织代码)→ 委托与事件(回调机制)→ LINQ 与异步(延迟执行与并发)→ 文件、序列化与框架(真实工程)。
值类型 vs 引用类型的内存模型 → 面向对象 → 委托/事件 → LINQ → 异步 → 生态
三个关键洞见:
- "值走栈、引用走堆"决定了大半个语言:为什么 struct 适合小对象、class 适合共享对象,为什么
==对值类型比较内容、对引用类型默认比较地址,为什么数组、字符串、List 都是"引用"——全都可以从内存模型推出来。 - 延迟执行(deferred execution)是 LINQ 和 async 的共同灵魂:
IEnumerable的查询不立刻执行、async方法不立刻运行,都是把"计算"延迟到真正需要结果的那一刻,从而获得组合性与性能。 - C# 在"安全"与"性能"之间给了你选择:默认有 GC 兜底、有 null 检查;想要极致的性能,还可以用
Span<T>、ref struct、unsafe。理解这条光谱,才算理解现代 C#。
三、模块精讲
模块 1 开发环境与工具链
这一模块在干什么
写代码前先搞清楚"代码怎么被编译、怎么跑起来、依赖怎么进来"。C# 的开发体验高度依赖命令行工具链,理解了 csproj 和 NuGet,你就理解了整个 .NET 工程的骨架,而不是只会点绿色运行按钮。
核心内容
1. .NET 的三层结构:运行时(Runtime,执行 IL 的虚拟机 + 内存管理)+ 框架类库(BCL,System.Text.Json、System.IO 等现成类)+ 你的代码。dotnet 命令直接操作这三层:new 建工程、build 编译、run 运行、add package 引 NuGet、test 跑测试。
2. 现代 SDK 风格 csproj:自动收集源文件、一行一个包引用。旧式 .NET Framework 项目则要手动逐个 <Compile Include>。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
3. NuGet 与解决方案:NuGet 是包管理器,默认源 nuget.org,还原后缓存在本地全局包目录;dotnet add package X 等价于手写 <PackageReference>。.sln 只是多个 csproj 的容器,dotnet build 在 sln 上按依赖顺序编译所有项目。
实战要点
dotnet new console -n MyApp && cd MyApp && dotnet run // 脚手架 + 运行
开发期用 dotnet watch run 热重载;生产多用 dotnet new web(API)、dotnet new wpf、dotnet new xunit。
模块 2 C# 语言基础
这一模块在干什么
用最短路径把语法扫一遍。重点不是罗列关键字,而是建立两个直觉:表达式可以有返回值、异常是控制流的正道而非特例。很多新手把该抛异常的场景用 return null 处理,正是缺了这两个直觉。
核心内容
1. 类型与变量:C# 是强类型语言,var 只是"让编译器推断类型",不是弱类型。
int count = 42; // 32 位整数
decimal money = 19.99m; // 128 位十进制:货币用 decimal,绝不用 double
string name = "C#";
var list = new List<int>(); // var = 编译器推断出 List<int>
// 数值隐式转换方向:byte → int → long → float → double → decimal;反向要显式 (int)x
2. 控制流与表达式
// switch 表达式(C# 8+):一个分支就是一个返回值
string grade = score switch
{
>= 90 => "A",
>= 60 => "C",
_ => "F"
};
3. 字符串:引用类型但行为像值类型
string a = "hello";
string b = a; // 复制的是引用,但 string 不可变
b += " world"; // 产生新字符串,a 仍是 "hello"
Console.WriteLine(a == "hello"); // true:== 对 string 比较的是内容(重载过)
不要在循环里用 "a" + "b" 拼字符串——每次产生新对象,复杂度 \(O(n^2)\);用 StringBuilder 或 string.Join。
4. 异常:try/catch/finally
try
{
var data = File.ReadAllText("config.json");
}
catch (FileNotFoundException ex) { Log($"配置缺失: {ex.Message}"); } // 先抓具体异常
catch (Exception ex) { Log($"未知错误: {ex}"); } // 再抓兜底
finally
{
dbConnection?.Close(); // 无论成败都执行
}
三条实战原则:只在能"恢复"或"转换"的地方 catch;catch 后要么处理要么重新 throw(不要吞掉);资源释放优先用 using 而不是 finally——using 是编译期的 try/finally。
using var conn = new SqlConnection(connString); // 离开作用域自动 Dispose
using var fs = new FileStream("data.bin", FileMode.Open);
易错点
catch (Exception) {}空吞异常会让程序在错误状态里继续跑,bug 更难查。- 浮点数比较用容差
Math.Abs(a - b) < 1e-9,不要用==;decimal与double混算必须显式转换。
模块 3 值类型 vs 引用类型、内存模型与 GC
这一模块在干什么
这是整门课的地基。C# 把"数据存在哪里"用类型显式表达:值类型直接存数据,引用类型存的是指向堆的地址。GC、装箱、可空、== 的语义全都是这条规则的推论。理解它,你才能回答"为什么我改了对象,别处也变了"这类最常见的开发问题。
核心内容
1. stack 与 heap 模型
struct Point { public int X; public int Y; } // 值类型:内容直接内嵌
class Car { public string Model; } // 引用类型:变量存的是地址
Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1; // 复制完整值,p2 改不影响 p1
p2.X = 100; // p1.X 仍是 1
Car c1 = new Car { Model = "Tesla" };
Car c2 = c1; // 只复制引用(地址),两个变量指向同一对象
c2.Model = "BYD"; // c1.Model 也变成 "BYD"
| 值类型(struct/枚举) | 引用类型(class/interface/string) | |
|---|---|---|
| 数据位置 | 栈 / 内嵌在容器里 | 堆上对象,栈里只有引用 |
| 赋值语义 | 复制整个值 | 复制地址 |
== 默认 |
逐字段比较内容 | 比较引用(地址) |
| 垃圾回收 | 不参与,栈弹出即回收 | 由 GC 回收 |
2. 装箱与拆箱(boxing/unboxing)
值类型被塞进引用类型容器(object、非泛型 ArrayList)时发生装箱:在堆上复制一份值再存引用,有性能开销且产生新对象。泛型之后这种写法已基本淘汰。
int x = 42;
object o = x; // 装箱:堆上新建一个 42
int y = (int)o; // 拆箱:先校验类型再取值
var list = new List<int>(); // 泛型容器永远不装箱
3. 可空类型
int? age = null; // Nullable<int>:表达"没有值"的数据库语义
int realAge = age ?? 18; // ?? null 合并运算符
string? name = null; // 可空引用类型:编译器静态跟踪
if (name != null) // 判空后编译器才允许安全访问
Console.WriteLine(name.Length);
Nullable<T> 本质是带 HasValue 标志的 struct,?? 和 ?. 是它的语法糖。
4. GC 与资源释放
GC 只回收堆上的引用类型对象,采用分代回收(Gen0 新生、Gen1、Gen2 老生代),新生代回收频繁但成本低。因此准则:大对象少创建、短命对象尽快死。不要手写 GC.Collect(),不要依赖析构函数释放资源——用 IDisposable + using。
public class DbConnection : IDisposable
{
private IntPtr _native; // 非托管资源必须显式释放
public void Dispose() { _native = IntPtr.Zero; }
}
实战要点
- 高频场景(游戏、网关)考虑 struct、
Span<T>、避免装箱;普通业务代码不必过早优化。 Equals与GetHashCode必须配套重写(字典/集合依赖),且语义用"唯一标识字段"而非整个对象。
模块 4 面向对象
这一模块在干什么
OOP 解决"大型工程的代码组织":把数据和操作它的方法封装成整体,用继承/接口定义"约定",让调用方只依赖抽象。C# 的特色是组合优于继承、接口可以多实现并带默认实现。
核心内容
1. 封装与访问修饰符
| 修饰符 | 可见范围 |
|---|---|
public / private |
所有人 / 仅本类 |
protected |
本类及派生类 |
internal |
同一程序集 |
protected internal / private protected |
组合语义 |
public class BankAccount
{
private decimal _balance; // 私有字段:封装数据
public string Owner { get; init; } // init:构造后不可改(不可变对象)
public decimal Balance => _balance; // 只读计算属性
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("金额必须为正");
_balance += amount; // 余额只能经方法修改,不变量得以保持
}
}
2. 继承、多态与虚方法
public abstract class Shape // 抽象类:不能实例化,定义骨架
{
public abstract double Area { get; } // 抽象成员:子类必须实现
public virtual string Describe() => "图形"; // 虚方法:子类可重写
}
public sealed class Circle : Shape // sealed:禁止再被继承
{
private readonly double _r;
public Circle(double r) => _r = r;
public override double Area => Math.PI * _r * _r;
public override string Describe() => $"圆,半径 {_r}";
}
Shape s = new Circle(2.0); // 多态:运行时调用 Circle 的实现
Console.WriteLine(s.Area); // 12.566...
3. 接口 vs 抽象类:接口是纯粹的契约,可多实现(class A : IFoo, IBar);抽象类既约契约又约血缘(共享实现/字段),单继承。
public interface ILogger { void Log(string message); }
public class FileLogger : ILogger
{
public void Log(string message) =>
File.AppendAllText("app.log", $"[{DateTime.Now}] {message}\n");
}
ILogger logger = new FileLogger(); // 依赖抽象:换实现不用改调用方
4. record 与 struct
record(C# 9+):值语义的类,默认==比较内容、自动实现ToString与解构,适合 DTO / 值对象。record struct/ 只读 struct:不可变数据载体。
public record OrderItem(string ProductId, int Qty, decimal Price);
var a = new OrderItem("A1", 2, 10m);
var b = new OrderItem("A1", 2, 10m);
Console.WriteLine(a == b); // true:record 默认比较内容
var (pid, q, p) = a; // 解构
var c = a with { Qty = 3 }; // with:拷贝出新实例,不改原对象
实战要点
- 优先组合:继承层次超过两层就要警惕;"IS-A"用继承、"HAS-A"用组合。
- DTO、配置、参数对象优先
record;性能热点的小值用struct;其余默认class。 - 依赖注入(构造时注入实现)是生产标配:
services.AddSingleton<ILogger, FileLogger>()。
模块 5 泛型与集合
这一模块在干什么
集合是开发中使用频率最高的类型家族。泛型让它"只写一次逻辑、适配任意类型且不装箱"。更重要的是理解 IEnumerable<T> 这个"管道"抽象——它是 LINQ 与几乎所有数据访问的公共接口。
核心内容
1. 三大集合选型
var list = new List<int>(); // 有序、按下标访问 O(1)
var dict = new Dictionary<string, int>(); // 键值对、按键查找 O(1)(哈希表)
var set = new HashSet<int>(); // 去重、O(1) 判重
dict["apple"] = 3;
if (dict.TryGetValue("apple", out var n)) // 安全取值;索引器取不存在的键会抛异常
Console.WriteLine(n);
选型:按下标/顺序访问用 List;按键快速查找用 Dictionary;只做去重/存在性判断用 HashSet;FIFO/LIFO 用 Queue/Stack。
2. IEnumerable 与 IQueryable:两种查询管道
IEnumerable<Order> seq = orders.Where(o => o.Amount > 100); // 内存逐个枚举(LINQ to Objects)
IQueryable<Order> q = db.Orders.Where(o => o.Amount > 100); // 拼成 SQL 交给数据库(EF Core)
IEnumerable<T>:惰性迭代器,Where只返回一个枚举器,真正foreach/ToList()时才逐个执行——即延迟执行。IQueryable<T>:用表达式树翻译成 SQL,Where只改 SQL,最终才在数据库执行。这就是 EF Core 里必须保持 IQueryable 到底、不要先 ToList 再筛选的原因(否则全表拉回内存)。
var bad = db.Users.ToList().Where(u => u.Age > 18).ToList(); // 错:先取全表再过滤
var good = db.Users.Where(u => u.Age > 18).ToList(); // 对:SQL 里 WHERE
3. 泛型约束
public static T MaxOf<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
public class Repository<T> where T : class, new() // 引用类型 + 有无参构造
{
public T Create() => new T();
}
约束关键字:class、struct、notnull、IComparable<T>、new()、基类、default。
易错点
Dictionary索引器取不存在的键抛异常,用TryGetValue。foreach里修改集合抛InvalidOperationException,收集后统一改或用RemoveAll。- 延迟陷阱:
var q = list.Where(x => x > 3);之后再改list,枚举q拿到的是新结果——查询是延迟的。
模块 6 委托、事件与 lambda
这一模块在干什么
把"一段代码"当成值传递,这是 C# 事件驱动与回调的基础。委托就是"函数类型的变量",lambda 是它的字面量,事件是在委托外加一套"订阅纪律"。
核心内容
1. Func / Action / lambda
Func<int, int, int> add = (x, y) => x + y; // 有返回值:Func<输入..., 输出>
Action<string> log = s => Console.WriteLine(s); // 无返回值:Action<输入...>
Predicate<int> isEven = n => n % 2 == 0; // 返回 bool 的专用委托
var result = add(2, 3); // 5
list.FindAll(isEven); // 把谓词当参数传
2. 闭包:lambda 捕获外层变量时,编译器把它"提升"到堆上的隐藏类,所以闭包引用的是变量本身、不是复制值。
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int captured = i; // 每轮循环新建局部变量
actions.Add(() => Console.Write(captured));
}
foreach (var a in actions) a(); // 输出 012(若直接捕获 i 会是 333)
3. 事件:发布/订阅模型
public class Button
{
public event EventHandler<EventArgs>? Click; // 事件:外部只能 += / -=
public void Press() => Click?.Invoke(this, EventArgs.Empty);
}
var btn = new Button();
btn.Click += (s, e) => Console.WriteLine("按钮被点击"); // 订阅
btn.Press(); // 发布
- 事件与普通委托字段的区别:事件只能在声明它的类内部
Invoke,外部只能+=/-=,保护发布者不被随意触发。 EventHandler<T>约定签名(object? sender, T args)。- 订阅了不退订是经典内存泄漏:事件里的委托强引用订阅方,使其永远无法被 GC 回收。订阅生命周期要与对象生命周期匹配。
实战要点
- 现代代码优先 lambda +
Func/Action传递逻辑;事件只用于"一对多通知"(UI 控件、总线)。 - 用匿名 lambda 订阅后无法退订;对象会销毁时保存命名方法再
-=。
模块 7 LINQ
这一模块在干什么
LINQ 把"集合处理"从命令式循环变成声明式查询:你描述"要什么",不写"怎么拿"。它统一了内存集合、数据库(EF)、XML 等异构数据源的访问。核心两件事:流式方法链与延迟执行。
核心内容
1. 两种语法
var users = new[]
{
new { Name = "张三", Age = 30, City = "上海" },
new { Name = "李四", Age = 17, City = "北京" },
new { Name = "王五", Age = 25, City = "北京" },
};
// 查询语法(像 SQL)
var adultsInBeijing =
from u in users
where u.Age >= 18 && u.City == "北京"
orderby u.Age descending
select u.Name;
// 方法语法(表达式链,生产代码更常用)
var result = users.Where(u => u.Age >= 18 && u.City == "北京")
.OrderByDescending(u => u.Age)
.Select(u => u.Name)
.ToList();
2. 常用操作符分类
| 类别 | 操作符 |
|---|---|
| 过滤 | Where、Take、Skip、Distinct |
| 投影 | Select、SelectMany(扁平化嵌套集合) |
| 排序 | OrderBy、OrderByDescending、ThenBy |
| 分组/连接 | GroupBy、Join |
| 聚合 | Count、Sum、Min/Max、Average、Aggregate |
| 判定/单值 | Any、All、Contains、First(OrDefault)、Single(OrDefault) |
var groupByCity = users.GroupBy(u => u.City);
foreach (var g in groupByCity)
Console.WriteLine($"{g.Key}: {g.Count()} 人,平均年龄 {g.Average(u => u.Age):F1}");
var flat = orderList.SelectMany(o => o.Items); // 每个订单的明细拍平成一个序列
3. 延迟执行与立即执行
var big = new List<int>(Enumerable.Range(0, 1_000_000));
var query = big.Where(n => n % 7 == 0).Select(n => n * 2); // 此刻什么都没算
var first = query.First(); // 只枚举到第一个命中即停
var all = query.ToList(); // 才真正全部执行
- 延迟:
Where/Select/OrderBy/GroupBy只搭管道;立即:ToList/ToArray/Count/First/Any触发执行。 - 在
Select里塞副作用(写日志、写库)是反模式——你无法预知它何时、执行几次。 GroupBy不保证组内顺序,需要时再OrderBy。
实战要点
- 数据库场景保持
IQueryable组合到最后(模块 5);只读查询加AsNoTracking()。 Single有多个结果会抛异常,仅当语义上保证唯一时使用;FirstOrDefault注意 null 处理。- 大流用
foreach迭代IEnumerable,不要ToList全量进内存。
模块 8 异步编程 async/await
这一模块在干什么
I/O(网络、磁盘、数据库)比 CPU 慢几个数量级,同步等待会把线程白白占住。async/await 让代码以"顺序书写"表达"异步执行",既不阻塞线程、也没有回调地狱。核心一句:await 点就是让出点。
核心内容
1. Task 与线程池
Task表示"未来会完成的一件事",不是线程本身。async方法遇到await时立刻返回给调用方,原线程去干别的;I/O 完成后由线程池线程继续后面的代码。Task.Run(() => CpuBound())把 CPU 密集工作丢给线程池;await File.ReadAllTextAsync()根本不占线程(底层是 I/O 完成端口)。
public async Task DownloadAndSaveAsync(string url, string path)
{
using var client = new HttpClient();
var bytes = await client.GetByteArrayAsync(url); // await 让出:不阻塞调用线程
await File.WriteAllBytesAsync(path, bytes);
Log($"已保存 {bytes.Length} 字节");
}
// 并发下载多个文件:Task.WhenAll 并行等待
var tasks = urls.Select(u => DownloadAndSaveAsync(u, Path.GetFileName(u)));
await Task.WhenAll(tasks);
2. 线程池与 SynchronizationContext
- 线程池维护一批可复用线程,避免频繁创建销毁的开销;CPU 密集任务线程数≈核心数,I/O 密集线程主要阻塞在 I/O 完成上。
- SynchronizationContext 决定 await 之后代码回到哪个线程:WPF/WinForms 的 UI 线程有上下文,await 后自动回到 UI 线程(可安全更新控件);控制台/ASP.NET Core 通常没有。
// WPF 按钮点击:await 后仍在 UI 线程,可直接更新控件
private async void OnClick(object sender, RoutedEventArgs e)
{
var data = await FetchDataAsync();
txtResult.Text = data;
}
3. 死锁与配置
经典死锁:UI 线程用 .Result/.Wait() 同步阻塞等待 async 方法,而该方法 await 后要回到 UI 线程才能完成 → 互相等待。
var result = MyAsyncMethod().Result; // 错误:阻塞 + UI 上下文 = 死锁
var result = await MyAsyncMethod(); // 正确 1:全程异步
// 正确 2:调用方无法改异步时,切断上下文
var result = MyAsyncMethod().ConfigureAwait(false).GetAwaiter().GetResult();
4. 取消
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try { var data = await client.GetStringAsync(url, cts.Token); }
catch (OperationCanceledException) { Log("已取消"); }
实战要点与易错点
async方法返回Task/Task<T>;只有事件处理器可用async void,库代码用async void会吞异常。- 不要在循环里
await顺序等待独立 I/O,用Task.WhenAll/WhenAny并行。 - 不要用
Task.Run包 I/O 操作——白耗一次线程切换;Task.Run只用于 CPU 密集。 - 库代码统一
ConfigureAwait(false);UI 顶层保留默认同步上下文。
模块 9 文件与序列化
这一模块在干什么
程序与外部交换数据的两个基本渠道:文件流(字节/字符流)与序列化(内存对象 ↔ 可传输格式)。核心是"流(Stream)"抽象——把文件、网络、内存统一成可读/可写的字节管道。
核心内容
1. 流与文件
using var fs = new FileStream("data.bin", FileMode.OpenOrCreate, FileAccess.Write);
using var bw = new BinaryWriter(fs); // 把基本类型写成二进制
bw.Write(42);
bw.Write("hello");
using var reader = new StreamReader("data.txt", System.Text.Encoding.UTF8);
string? line;
while ((line = reader.ReadLine()) != null)
Console.WriteLine(line);
// 小文件用 File 静态类的快捷方式
var text = await File.ReadAllTextAsync("config.json");
await File.WriteAllTextAsync("out.json", text);
2. System.Text.Json
public class Order
{
public int Id { get; set; }
public string? Customer { get; set; }
public decimal Total { get; set; }
public DateTime CreatedAt { get; set; }
}
var order = new Order { Id = 1, Customer = "张三", Total = 99.5m, CreatedAt = DateTime.UtcNow };
string json = JsonSerializer.Serialize(order); // 序列化
var back = JsonSerializer.Deserialize<Order>(json); // 反序列化
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 前端习惯小驼峰
WriteIndented = true, // 日志友好
PropertyNameCaseInsensitive = true // 反序列化容错
};
3. 序列化选型:Web/API/配置用 System.Text.Json;老项目或复杂契约用 Newtonsoft.Json;跨语言高性能二进制用 protobuf;紧凑缓存用 MessagePack。
实战要点
- 所有流都要
using:Dispose关闭句柄并刷新缓冲区,忘了用会文件占用、数据丢失。 - 反序列化永远容忍坏数据:外层包
try/catch,用JsonException细分。 - 大文件(几百 MB 以上)不要
ReadAllText,用StreamReader逐行/分块读。
模块 10 现代 C# 与生态
这一模块在干什么
把前面所有知识放进真实生态:现代语法糖(模式匹配、源生成器)如何提升生产力,三大框架各自的定位,以及最常见的坑。这部分不是新语法课,而是"工程地图"。
核心内容
1. 模式匹配
string Describe(object obj) => obj switch
{
int n when n < 0 => "负整数",
int n => $"整数 {n}",
string s => $"字符串长度 {s.Length}",
{ X: 1, Y: 0 } => "坐标 (1,0)", // 属性模式
_ => "其他"
};
2. 源生成器(Source Generator)概述:编译期运行代码、自动产出额外 C# 源码。典型如 System.Text.Json 的 [JsonSourceGenerationOptions](零反射、启动快)、EF Core 编译模型、AutoMapper 的 mapper。思维转变:能交给编译器生成的重活(反射、样板代码)就交给它。
3. 三大框架简述
- ASP.NET Core(Web/API):
builder模式 + 依赖注入 + EF Core 是后端主流:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDb>(o => o.UseSqlServer(connString));
var app = builder.Build();
app.MapControllers();
app.Run();
- WPF(桌面):MVVM +
INotifyPropertyChanged+{Binding},UI 与逻辑分离;模块 8 的 SynchronizationContext 在这里最有用。 - Unity(游戏):用 C# 写脚本(
MonoBehaviour、Update生命周期),引擎底层是 C++,因此 GC 暂停是 Unity 性能痛点——模块 3 的"值类型、装箱、对象池"正好派上用场。
4. 常见陷阱清单
| # | 陷阱 | 正确做法 |
|---|---|---|
| 1 | 用 double 存金额 |
货币用 decimal |
| 2 | 循环里 + 拼字符串 |
StringBuilder / string.Join |
| 3 | 吞异常 catch {} |
要么恢复要么 throw |
| 4 | 集合在 foreach 里改 | 收集后统一改 / RemoveAll |
| 5 | Dictionary 索引器取不存在键 |
TryGetValue |
| 6 | 事件订阅不退订 → 内存泄漏 | 生命周期匹配,及时 -= |
| 7 | .Result 阻塞 async → 死锁 |
全程 await 或 ConfigureAwait(false) |
| 8 | 库方法用 async void |
返回 Task,异常可捕获 |
| 9 | 忘记 using 释放文件/连接 |
using var |
| 10 | 大 JSON 全读进内存 | 流式读 + 分块反序列化 |
| 11 | EF 先 ToList 再筛选 |
保持 IQueryable 到最后 |
| 12 | 手写 GC.Collect() |
交给 GC 分代策略 |
四、总结与路线图
把整份笔记压成一张链:
[模块3 内存模型] 值类型→装箱→引用类型→GC
│ 数据组织
▼
[模块2 语言基础] + [模块4 OOP] + [模块5 泛型/集合]:异常/record/IEnumerable/选型
│ 行为抽象
▼
[模块6 委托/事件/lambda] + [模块7 LINQ]:回调 + 延迟执行 + IQueryable
│ 并发与 I/O
▼
[模块8 async/await] + [模块9 文件/序列化]:Task/线程池/上下文 + Stream/Json
│ 落地
▼
[模块1 工具链] + [模块10 生态]:csproj/NuGet + ASP.NET Core/WPF/Unity
下一步学习路线(按生产价值排序):
- API 后端:ASP.NET Core 最小 API → EF Core 关系建模与迁移 → JWT 认证 → 日志(Serilog)与配置(Options 模式)。
- 桌面端:WPF + MVVM(CommunityToolkit.Mvvm)→ 数据绑定 → 样式与模板。
- 进阶性能:
Span<T>/Memory<T>、ref struct、Channel<T>、内存池;用dotnet-counters与 BenchmarkDotNet 实测 GC 与 JIT。 - 工程化:xUnit + FluentAssertions 单元测试、CI/CD、Dockerfile 发布、依赖注入容器原理。
- 语言深水区:表达式树(
Expression)、dynamic/反射、可空分析原理、源生成器实战。
记住主线:任何新特性,先问"它在内存模型里做了什么、执行是延迟还是立即、谁负责释放资源"——三问过一遍,就不会迷路。

浙公网安备 33010602011771号