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 structunsafe。理解这条光谱,才算理解现代 C#。

三、模块精讲

模块 1 开发环境与工具链

这一模块在干什么

写代码前先搞清楚"代码怎么被编译、怎么跑起来、依赖怎么进来"。C# 的开发体验高度依赖命令行工具链,理解了 csproj 和 NuGet,你就理解了整个 .NET 工程的骨架,而不是只会点绿色运行按钮。

核心内容

1. .NET 的三层结构:运行时(Runtime,执行 IL 的虚拟机 + 内存管理)+ 框架类库(BCL,System.Text.JsonSystem.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 wpfdotnet 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)\);用 StringBuilderstring.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,不要用 ==decimaldouble 混算必须显式转换。

模块 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>、避免装箱;普通业务代码不必过早优化。
  • EqualsGetHashCode 必须配套重写(字典/集合依赖),且语义用"唯一标识字段"而非整个对象。

模块 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();
}

约束关键字:classstructnotnullIComparable<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. 常用操作符分类

类别 操作符
过滤 WhereTakeSkipDistinct
投影 SelectSelectMany(扁平化嵌套集合)
排序 OrderByOrderByDescendingThenBy
分组/连接 GroupByJoin
聚合 CountSumMin/MaxAverageAggregate
判定/单值 AnyAllContainsFirst(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。

实战要点

  • 所有流都要 usingDispose 关闭句柄并刷新缓冲区,忘了用会文件占用、数据丢失。
  • 反序列化永远容忍坏数据:外层包 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# 写脚本(MonoBehaviourUpdate 生命周期),引擎底层是 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 → 死锁 全程 awaitConfigureAwait(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

下一步学习路线(按生产价值排序):

  1. API 后端:ASP.NET Core 最小 API → EF Core 关系建模与迁移 → JWT 认证 → 日志(Serilog)与配置(Options 模式)。
  2. 桌面端:WPF + MVVM(CommunityToolkit.Mvvm)→ 数据绑定 → 样式与模板。
  3. 进阶性能Span<T>/Memory<T>ref structChannel<T>、内存池;用 dotnet-counters 与 BenchmarkDotNet 实测 GC 与 JIT。
  4. 工程化:xUnit + FluentAssertions 单元测试、CI/CD、Dockerfile 发布、依赖注入容器原理。
  5. 语言深水区:表达式树(Expression)、dynamic/反射、可空分析原理、源生成器实战。

记住主线:任何新特性,先问"它在内存模型里做了什么、执行是延迟还是立即、谁负责释放资源"——三问过一遍,就不会迷路。

posted @ 2026-09-04 16:52  IcarusLee  阅读(5)  评论(0)    收藏  举报