本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《C#编程基础:深入理解第九章》是王文峰老师课程中的关键章节,涵盖C#面向对象编程的高级特性。本预习资料包“15_王文峰C#ch09预习.zip”包含课件、笔记、练习题及录屏讲解,系统梳理了委托与事件、Lambda表达式、LINQ、匿名类型、扩展方法、异步编程、多线程、泛型、集合框架和异常处理等核心内容。通过本章学习,学生将掌握C#中实现高效、安全、可维护代码的关键技术,为后续复杂项目开发打下坚实基础。

委托、事件与Lambda:C#中函数式编程的基石

想象这样一个场景:你正在开发一个智能家居系统,主控模块需要根据传感器状态动态调用不同的处理逻辑。温度过高时启动风扇,湿度过大时开启除湿机——但这些行为在编译时根本无法确定。这时候你会怎么做?硬编码一堆 if-else ?还是让每个设备都实现统一接口然后反射调用?

其实,C#早就为我们准备了更优雅的解法: 把方法当成数据来传递

这听起来有点玄乎,但在现代C#开发中早已司空见惯。我们每天都在使用的 button.Click += (s,e) => ShowMessage(); ,背后就是一套精巧的机制在支撑。而这套机制的核心,正是 委托(Delegate)


委托不是“引用”,而是“类型化的函数指针” 🎯

很多人第一次接触委托时,总会把它理解成“方法的引用”。这种说法不算错,但容易让人误以为它和普通变量引用是一回事。实际上,委托是一种 类型安全的函数指针 ——关键词是“类型安全”。

什么意思呢?看这个例子:

public delegate int Calculator(int x, int y);

这行代码定义了一个名为 Calculator 的委托类型,它只能指向那些 返回 int 且接受两个 int 参数 的方法。就像你不能把一只猫塞进狗窝一样,编译器会阻止任何不匹配的赋值。

比如下面这段代码就能顺利通过:

private static int Add(int a, int b) => a + b;
private static int Multiply(int a, int b) => a * b;

// ✅ 合法:签名完全匹配
Calculator calc = Add;
Console.WriteLine(calc(3, 4)); // 输出 7

calc = Multiply;
Console.WriteLine(calc(3, 4)); // 输出 12

但如果我试图绑定一个参数数量或类型不符的方法:

private static string GetName() => "Alice";
// ❌ 编译错误!无法将 string () 转为 int (int, int)
Calculator wrong = GetName; 

立马就会被拦下来。这就是所谓的“类型安全”——它不是运行时才发现问题,而是在写代码的时候就被揪出来了。

那么,委托到底怎么工作的?本质上,当你实例化一个委托时,CLR会创建一个对象,里面存了两样东西:

  1. 目标方法的内存地址
  2. 所属实例的引用(如果是实例方法的话)

你可以把它想象成一张“任务卡”:上面写着“去哪执行”、“用谁的数据”、“传什么参数”。当你调用 calc(3,4) 时,这张卡片就会被交给运行时系统,由它精确地跳转到对应的方法并传参执行。

值得一提的是,除了直接调用,你还可以显式使用 Invoke()

calc.Invoke(3, 4); // 和 calc(3,4) 效果相同

不过没人会这么干——太啰嗦了 😅。倒是有个冷知识:如果你要在一个高并发环境下频繁调用委托,可以考虑缓存其 Method Target 属性以避免重复查表,但这属于极致优化范畴了。


事件:封装好的观察者模式 🔔

如果说委托是“方法的搬运工”,那事件就是“受保护的委托”——它是基于委托构建的一种语言级封装,专为实现 发布-订阅模式 而生。

最常见的应用场景莫过于 GUI 编程中的按钮点击事件:

button.Click += OnButtonClick;

这里的 Click 就是一个事件。关键点在于: 外部代码只能通过 += -= 来订阅或取消订阅,而不能直接触发它,也不能清空整个事件链

这是怎么做到的?答案就在 event 关键字上。

当我们这样声明一个事件:

public event EventHandler<DataEventArgs> DataUpdated;

编译器会自动生成两个隐藏的访问器方法: add_DataUpdated remove_DataUpdated 。它们的作用就像门卫一样,只允许你“添加监听者”或“移除监听者”,而不让你随意篡改内部的委托链。

这也意味着,以下操作都是非法的:

// ❌ 错误:不能直接赋值给事件
logger.DataUpdated = null;

// ❌ 错误:不能直接调用事件(除非在同一类内)
DataUpdated(this, args); 

唯一的例外是,在定义事件的那个类内部,你可以自由触发它:

protected virtual void OnDataUpdated(DataEventArgs e) 
    => DataUpdated?.Invoke(this, e);

这里用了个技巧:先判断 DataUpdated 是否为空(即有没有人订阅),再安全调用。这也是为什么推荐所有事件触发都写成 xxx?.Invoke(...) 的原因——防止空引用异常。

💡 小贴士 :为什么叫 OnXXX ?这是一种约定俗成的命名规范。 OnDataUpdated 表示“当数据更新时应该做什么”,通常用于封装事件触发逻辑,便于子类重写。


Lambda表达式:匿名函数的终极简化 💡

还记得早期Web开发中满屏的 delegate { ... } 吗?那种写法虽然功能完整,但实在不够清爽。直到 C# 3.0 引入了 Lambda 表达式,才真正让函数式风格流行起来。

来看对比:

// 旧式匿名方法
Func<int, int, int> add1 = delegate(int a, int b) { return a + b; };

// Lambda写法
Func<int, int, int> add2 = (a, b) => a + b;

光这一行就少了十几个字符,而且语义更清晰。 => 这个符号读作“goes to”,直白地说就是:“输入 (a,b) ,输出 a+b ”。

但别被它的简洁骗了——Lambda 不只是语法糖那么简单。它的强大之处在于:

  • 类型推断 :编译器能根据上下文自动推导参数类型
  • 闭包支持 :可以捕获外部变量
  • 表达式树转换 :某些情况下会被编译为 Expression<Func<T>> 而非纯方法

举个实用的例子:你想筛选出年龄大于18的学生名单。

var adults = students.Where(s => s.Age >= 18).ToList();

这短短一行里, s => s.Age >= 18 是一个典型的谓词函数,被传给了 Where 方法。如果没有 Lambda,就得提前定义一个委托实例或者写个完整的方法,代码瞬间臃肿起来。

更妙的是,Lambda 还能自动升级为表达式树,供 LINQ to SQL 等框架翻译成 SQL 查询:

// 下面这句会被 EF Core 翻译为 SELECT * FROM Students WHERE Age > 18
var query = dbContext.Students.Where(s => s.Age > 18);

如果换成普通方法或匿名委托,就没法做这种翻译了。所以说,Lambda 不仅提升了可读性,还打开了通往“代码即数据”的大门。


构建高效数据管道:LINQ与泛型集合的艺术

现在让我们换个战场:假设你是一家电商平台的技术负责人,每天要处理数百万条订单记录。老板要求你快速统计“本月销售额前10的已发货订单”,并且希望这个查询尽可能高效、易读、可维护。

传统做法可能是嵌套循环加条件判断,但那样既难调试又容易出错。幸运的是,C# 提供了一整套现代化的数据操作体系: 泛型集合 + LINQ = 数据处理神器

这套体系之所以强大,是因为它融合了三个核心理念:

  1. 类型安全 (泛型保障)
  2. 声明式编程 (LINQ提供)
  3. 延迟执行 (性能优化基础)

接下来我们就一层层揭开它的面纱。


LINQ的本质:从SQL幻觉到方法链真相 🪄

第一次看到 from...where...select 这种写法的人,往往会误以为 C# 内置了一个小型数据库引擎。其实不然—— 所有的 LINQ 查询最终都会被编译器转换为对标准查询操作符的调用

比如这个看起来很像 SQL 的查询:

var query = from student in students
            where student.Age >= 18
            orderby student.Name
            select new { student.Name, student.Age };

你以为它在执行 SQL?错!它等价于:

var query = students
    .Where(s => s.Age >= 18)
    .OrderBy(s => s.Name)
    .Select(s => new { s.Name, s.Age });

两者生成的 IL 指令几乎一模一样。唯一的区别是前者需要经过一次“语法重写”过程,后者则是直接调用扩展方法。

为了更清楚地展示这个过程,来看下面这个流程图 👇

graph TD
    A[原始查询表达式] --> B{解析 from 子句}
    B --> C[确定数据源 students]
    C --> D{处理 where 条件}
    D --> E[调用 Where(student => student.Age >= 18)]
    E --> F{处理 orderby 子句}
    F --> G[调用 OrderBy(student => student.Name)]
    G --> H{处理 select 投影}
    H --> I[调用 Select(student => new { Name = student.Name, Age = student.Age })]
    I --> J[返回最终查询对象]

看到了吗?所谓“查询语法”,不过是编译器帮你把高层语义拆解成了底层方法调用而已。整个过程发生在编译期,零运行时开销。

这也解释了为什么有些复杂的查询用方法语法反而更容易理解。例如多表连接:

var result = from c in customers
             join o in orders on c.Id equals o.CustomerId
             where o.Total > 1000
             select new { c.Name, o.OrderDate, o.Total };

对应的链式调用是:

var result = customers
    .Join(orders,
          c => c.Id,
          o => o.CustomerId,
          (c, o) => new { c, o })
    .Where(x => x.o.Total > 1000)
    .Select(x => new { x.c.Name, x.o.OrderDate, x.o.Total });

虽然略显繁琐,但它揭示了一个重要事实: Join 其实是个四参数函数!中间那个 (c,o)=>new{} 是结果选择器,决定了合并后的结构。

所以什么时候该用哪种语法?我的建议是:

场景 推荐语法
简单过滤/映射 方法语法( .Where().Select()
多表连接、分组聚合 查询语法(可读性强)
动态拼接条件 方法语法(灵活控制流程)
团队成员有SQL背景 查询语法(降低认知成本)

没有绝对的好坏,只有是否适合当前上下文。


扩展方法:给IEnumerable “打补丁”的魔法 ✨

你有没有想过, List<T> 本身并没有 Where OrderBy 方法,为什么可以直接调用?

秘密就在于 扩展方法 。这些看似属于集合类的方法,其实是定义在静态类 System.Linq.Enumerable 中的:

public static class Enumerable
{
    public static IEnumerable<TSource> Where<TSource>(
        this IEnumerable<TSource> source,
        Func<TSource, bool> predicate)
    {
        // 实现细节...
    }
}

关键字 this IEnumerable<TSource> source 告诉编译器:“这个静态方法可以当作 IEnumerable<T> 的实例方法来用”。

这就像是给老房子装新电梯——不用改动原有结构,就能获得全新体验。

更重要的是,这些方法大多返回 IEnumerable<T> ,形成了流畅的 方法链 (fluent interface)。每一个操作都不立即执行,而是等待最后被消费时才开始流动。

这就是传说中的“ 延迟执行 ”(Deferred Execution)。


延迟执行:像水管一样输送数据 💧

想象一条长长的水管,源头是你的数据集合,末端是你调用 ToList() foreach 的地方。中间的各种 .Where() .OrderBy() 就像一个个过滤器和泵站。

只要你还没拧开水龙头(即枚举结果),水就不会流动。也就是说,下面这行代码其实啥也没干:

var query = dbContext.Products.Where(p => p.Price > 100);

它只是“描述”了一个查询意图,并没有真正去数据库拿数据。

只有当你写下:

var results = query.ToList(); // ← 此刻才执行

水流才会启动,请求发往数据库,结果返回填充列表。

这种设计的好处显而易见:

  • 节省资源 :避免不必要的计算
  • 组合灵活 :可以在任意阶段插入新条件
  • 提高性能 :尤其在 Entity Framework 中,多个 .Where() 会被合并为一条 SQL

但也带来一个陷阱: 同一个查询多次枚举会产生多次实际调用

Console.WriteLine(query.Count()); // 第一次访问,执行查询
Console.WriteLine(query.Any());   // 第二次访问,再次执行!

如果你连着查了五次,那就等于跑了五遍数据库……😱

解决办法很简单:一旦决定要复用结果,就立刻物化它:

var list = query.ToList(); // 只执行一次
Console.WriteLine(list.Count);
Console.WriteLine(list.Any);

记住一句话: 延迟执行是利器,也是双刃剑


标准查询操作符全解析 📊

为了让你们对 LINQ 的能力有全面认识,我整理了一份常用操作符对照表:

方法名 功能描述 是否延迟执行 返回类型
Where 按条件过滤 IEnumerable<T>
Select 元素映射 IEnumerable<R>
OrderBy 排序 IOrderedEnumerable<T>
GroupBy 按键分组 IEnumerable<IGrouping<K,T>>
First 获取首个元素 T
Any 判断是否存在匹配元素 bool
Count 统计元素数量 int
ToArray 强制立即执行并转数组 T[]

注意区分哪些是“终止操作”(eager evaluation),哪些是“中间操作”(lazy evaluation)。一般来说,凡是返回具体值而非 IEnumerable 的,基本都是立即执行的。

比如 First() ,哪怕数据源有十亿条记录,它也会马上拉取第一条并停止后续遍历。这在某些场景下非常有用,比如判断用户是否存在:

bool exists = users.Any(u => u.Email == email); // 比 FirstOrDefault() + null check 更高效

因为 Any() 一旦找到符合条件的项就立刻返回 true ,不会继续扫描。


异步编程:如何写出不会卡死UI的代码?

咱们来聊点刺激的:你刚上线了一个电商App,用户反馈“点击‘下单’按钮后界面卡住好几秒”。一查日志发现,原来是支付网关API响应慢导致的。

这时候你能怎么办?把所有代码改成多线程?手动创建 Thread ?拜托,那可是上世纪的做法了。

现代C#的正确姿势是: async/await 。它让你用同步的方式写异步代码,既不阻塞主线程,又能保持逻辑清晰。

但很多人只知道“加个 async await 就行了”,却不知道背后发生了什么。一旦遇到死锁或异常丢失,就傻眼了。

别担心,今天我就带你看穿它的本质。


async/await 不是多线程,而是“状态机驱动的任务切换” ⚙️

当你写下:

public async Task<int> CalculateAsync()
{
    await Task.Delay(1000);
    return 42;
}

看起来像是一段连续执行的代码,但实际上,编译器会把它重写成一个 有限状态机 (Finite State Machine)。

什么意思?简单说,就是把这个方法拆成多个片段,每遇到一个 await 就暂停一下,把控制权交回去。等异步操作完成后再恢复执行。

伪代码大概是这样:

[CompilerGenerated]
private sealed class <CalculateAsync>d__0 : IAsyncStateMachine
{
    public int <>1__state;
    public AsyncTaskMethodBuilder<int> <>t__builder;
    private TaskAwaiter <>u__1;

    private void MoveNext()
    {
        switch (<>1__state)
        {
            case 0:
                // 恢复点:延迟结束后继续执行
                break;
            default:
                // 初始执行
                <>u__1 = Task.Delay(1000).GetAwaiter();
                if (!<>u__1.IsCompleted)
                {
                    <>1__state = 0;
                    <>t__builder.AwaitOnCompleted(ref <>u__1, ref this);
                    return; // ← 关键!释放控制权
                }
                break;
        }

        // 继续执行后续代码
        <>t__builder.SetResult(42);
    }

    void SetStateMachine(IAsyncStateMachine stateMachine) =>
        <>t__builder.SetStateMachine(stateMachine);
}

看到了吗? await 并没有创建新线程,而是利用了 回调机制 。当 Task.Delay 完成后,会通知这个状态机重新调用 MoveNext() ,从而从断点处继续执行。

整个过程可以用下面的状态图表示:

stateDiagram-v2
    [*] --> Created
    Created --> Running: Start()
    Running --> Suspended: await incomplete Task
    Suspended --> Resumed: Task completed
    Resumed --> Running: MoveNext() called
    Running --> Completed: SetResult()
    Running --> Faulted: Exception thrown
    Completed --> [*]
    Faulted --> [*]
    note right of Suspended
      Callback registered via INotifyCompletion.
      Thread is released during suspension.
    end note

最关键的一点是: Suspended 状态下线程被释放了 !它可以去处理其他请求、响应UI事件,真正做到“非阻塞”。

这也是为什么 async/await 特别适合 I/O 密集型操作(如网络、文件、数据库),因为这些操作大部分时间都在等,根本不需要占用CPU。


Task异常传播:别让错误悄悄溜走 🐞

很多人以为只要用了 try-catch 包住 await ,就能抓住所有异常。可惜现实没那么简单。

来看这个例子:

try
{
    await Task.Run(() => throw new Exception("Boom!"));
}
catch (Exception ex)
{
    Console.WriteLine($"Caught: {ex.Message}");
}

没问题,能正常捕获。但如果换成多个任务并发呢?

var t1 = Task.Run(() => throw new InvalidOperationException("One"));
var t2 = Task.Run(() => throw new ArgumentException("Two"));

try
{
    await Task.WhenAll(t1, t2);
}
catch (AggregateException aggEx)
{
    foreach (var inner in aggEx.Flatten().InnerExceptions)
    {
        Console.WriteLine($"Inner: {inner.Message}");
    }
}

注意到没?抛出来的是 AggregateException !因为它要打包所有失败的任务异常。

如果不调用 Flatten() ,你还可能遇到嵌套的 AggregateException ——想想都头疼。

所以最佳实践是:

catch (AggregateException ex)
{
    var flattened = ex.Flatten();
    foreach (var err in flattened.InnerExceptions)
    {
        Log.Error(err, "Background task failed");
    }
}

另外还有一个隐患: 未观察的异常 (Unobserved Exceptions)。如果你启动了一个 Task 却从不 await 它,里面的异常可能会默默消失,直到进程退出时才触发崩溃。

为了避免这种情况,建议注册全局处理器:

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    Log.Fatal(e.Exception, "Unobserved exception in background task");
    e.SetObserved(); // 防止程序终止
};

虽然 .NET 默认会在最终化时忽略未观察异常,但最好还是主动监控,毕竟谁也不想半夜被报警电话吵醒 😅。


ConfigureAwait(false):打破死锁的钥匙 🔑

最经典的死锁案例出现在 WinForms/WPF 应用中:

private void button_Click(object sender, EventArgs e)
{
    var result = GetDataAsync().Result; // ⚠️ 死锁!
}

public async Task<string> GetDataAsync()
{
    await Task.Delay(1000);
    return "Hello";
}

表面看没问题,但运行起来你会发现:点了按钮后界面直接卡死!

原因如下:

  1. 主线程进入 GetDataAsync
  2. 遇到 await Task.Delay(1000) ,任务未完成,返回 Task
  3. 调用 .Result 阻塞主线程,等待 Task 完成
  4. 延迟结束,运行时尝试将后续代码 调度回UI线程
  5. 但UI线程正被 .Result 占着,无法处理消息循环
  6. → 形成死锁,永远等不到结果

破解方法只有一个: 告诉运行时不必要回到原上下文

public async Task<string> GetDataAsync()
{
    await Task.Delay(1000).ConfigureAwait(false); // ← 关键!
    return "Hello";
}

ConfigureAwait(false) 的作用就是切断对当前 SynchronizationContext 的依赖,让延续(continuation)可以在任意线程池线程上执行。

当然,也不是哪儿都能乱加。以下是使用建议:

场景 是否使用
类库项目 ✅ 强烈推荐
UI应用业务层 ✅ 推荐
更新UI控件的代码 ❌ 禁止
ASP.NET Core 可选(默认无上下文)
单元测试中 Wait()/Result ✅ 必须

总结一句话: 在类库中一律使用 ConfigureAwait(false) ,在应用层按需决定是否恢复上下文

下面是死锁全过程的序列图,帮助你直观理解:

sequenceDiagram
    participant UI as UI Thread
    participant Async as Async Method
    participant Pool as ThreadPool

    UI->>Async: Call GetDataAsync()
    Async->>Pool: Start Task.Delay (captures context)
    Pool-->>Async: Delay not complete
    Async-->>UI: Return incomplete Task
    UI->>UI: Block on .Result
    Pool->>Pool: Delay completes
    Pool->>UI: Post continuation to UI context
    Note over UI,Poll: UI thread is blocked!<br/>Cannot process posted message
    loop Waiting forever
        UI->>UI: Stuck in .Result
    end

看到了吗?问题出在“Post continuation”这一步。如果用了 ConfigureAwait(false) ,这一步就会直接扔给线程池执行,不再依赖UI线程,自然也就不会卡住了。


工程化实践:打造稳定可靠的大型系统

到了最后阶段,我们要把前面所有技术串起来,落地到真实项目中。

无论是企业级后台服务,还是复杂桌面应用,稳定性、可维护性和扩展性永远是第一位的。

下面这三个实战模式,我已经在多个千万级用户产品中验证过有效性。


模块解耦:用事件驱动取代硬依赖 🔄

在一个典型的WPF/MVVM项目中,ViewModel之间经常需要通信。比如“用户点击‘刷新’按钮后,日志面板要更新内容,状态栏要显示进度”。

如果采用传统方式,你会怎么做?让 MainViewModel 直接持有 LogPanelViewModel StatusBarViewModel 的引用?不行!这样一来模块之间就紧紧绑死了,改一个会影响一堆。

正确的做法是: 事件驱动 + 发布订阅

我们可以设计一个简单的事件聚合器,或者直接使用 Prism/MVVM Toolkit 提供的消息总线。

结构如下:

flowchart TD
    A[MainWindowVM] -- "DataLoaded" --> B(LogListVM)
    A -- "ProgressUpdated" --> C(StatusBarVM)
    D[SettingsVM] -- "ConfigChanged" --> A
    D -- "ThemeChanged" --> E(MainView)

每个模块只关心自己感兴趣的事件,完全不知道是谁发布的。这种“松耦合”设计带来的好处包括:

  • 支持独立开发和测试
  • 易于替换实现(比如换主题引擎)
  • 可插拔架构(后期加监控模块不影响主流程)

当然,也要防范副作用:事件太多会导致“谁发了什么”变得难以追踪。建议配合事件命名规范 + 文档注释使用。


自定义 EventArgs:不只是传递数据,更是契约 📜

别小看 EventArgs 。它不仅是数据容器,更是一种 版本兼容性保障机制

比如你定义了一个日志事件:

public class LogEntryEventArgs : EventArgs
{
    public string Message { get; }
    public DateTime Timestamp { get; }
    public LogLevel Level { get; }
    public string Source { get; }

    public LogEntryEventArgs(string message, LogLevel level, string source)
    {
        Message = message;
        Level = level;
        Source = source;
        Timestamp = DateTime.Now;
    }
}

以后就算你要新增字段(比如 CorrelationId ),也不会破坏现有订阅者的兼容性。

相比之下,如果直接传 (string, LogLevel, string) 这样的 tuple,后期扩展几乎不可能。

此外,构造函数中设置 Timestamp 也体现了良好的设计习惯: 事件发生的时间应由发布者决定,而不是由接收者自行记录 ,否则会出现时钟偏差。


WeakEvent模式:终结内存泄漏的利器 🧹

GUI开发中最隐蔽的坑之一就是 事件导致的内存泄漏

比如你在页面加载时订阅了某个全局事件,但忘了在卸载时取消订阅。结果即使页面关闭了,GC 也无法回收它,因为事件源仍持有它的引用。

解决方案? Weak Event Pattern

原理很简单:不用强引用保存事件处理器,而是用 WeakReference<T> 包一层。

public class WeakEvent<TEventArgs> where TEventArgs : EventArgs
{
    private readonly List<WeakReference<EventHandler<TEventArgs>>> _listeners = 
        new List<WeakReference<EventHandler<TEventArgs>>>();

    public void Subscribe(EventHandler<TEventArgs> handler)
    {
        _listeners.Add(new WeakReference<EventHandler<TEventArgs>>(handler));
    }

    public void Raise(object sender, TEventArgs args)
    {
        var aliveHandlers = new List<EventHandler<TEventArgs>>();
        for (int i = _listeners.Count - 1; i >= 0; i--)
        {
            if (_listeners[i].TryGetTarget(out var target))
            {
                aliveHandlers.Add(target);
            }
            else
            {
                _listeners.RemoveAt(i); // 清理已释放的监听器
            }
        }

        foreach (var handler in aliveHandlers)
        {
            handler(sender, args);
        }
    }
}

这样即使订阅者被 GC 回收,也不会影响事件系统的正常运作。

特别适用于 WPF 中的 INotifyPropertyChanged 、MVVM 框架、插件系统等长生命周期场景。


总结:从“会用”到“懂原理”的跨越 🚀

今天我们聊了很多,从委托的本质到 async 状态机,从 LINQ 编译原理到工程化实践。你会发现,真正优秀的开发者,从来不满足于“能跑就行”。

他们想知道:

  • 为什么加了个 await 就不卡界面了?
  • 为什么有时候 .Result 会导致死锁?
  • 为什么同样的查询语法和方法语法性能一样?

这些问题的答案,不在文档的第一章,而在深夜调试崩溃的日志里,在反编译工具打开的 IL 代码中,在一次次踩坑后的反思中。

而你现在,已经比大多数人更接近真相了 💪。

记住: 掌握原理,才能驾驭变化 。语言会升级,框架会迭代,但底层思想永远闪光。

愿你在代码的世界里,既能仰望星空,也能脚踏实地 🌟

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《C#编程基础:深入理解第九章》是王文峰老师课程中的关键章节,涵盖C#面向对象编程的高级特性。本预习资料包“15_王文峰C#ch09预习.zip”包含课件、笔记、练习题及录屏讲解,系统梳理了委托与事件、Lambda表达式、LINQ、匿名类型、扩展方法、异步编程、多线程、泛型、集合框架和异常处理等核心内容。通过本章学习,学生将掌握C#中实现高效、安全、可维护代码的关键技术,为后续复杂项目开发打下坚实基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐