首页 / C# 入门教程 / 方法语法与扩展方法

C# 入门教程

方法语法与扩展方法

本教程共 100 篇 · 第 68 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程编程语言LINQ方法语法扩展方法

68. 方法语法与扩展方法

本节目标:学完你能用点号链式写法写 LINQ,并说出扩展方法的底层原理,以及它与继承、装饰器等扩展方式的区别。

上一章的查询语法更像 SQL。但在真实项目里,你见得更多的是点号串起来的「方法语法(method syntax)」。它和查询语法等价,只是换了个长相,而且更灵活。这一章还顺带揭开它背后的功臣:扩展方法(extension method)。

方法语法长什么样

同样筛偶数,用方法语法写是这样:

List<int> numbers = [1, 2, 3, 4, 5, 6];

var even = numbers.Where(n => n % 2 == 0);

foreach (var n in even)
{
    Console.WriteLine(n);
}

输出:2 4 6numbers.Where(...) 看起来像 numbers 自带的实例方法,其实它是 System.Linq 提供的扩展方法(extension method)。

Note

两种语法编译后本质相同。编译器会把查询语法翻译成方法语法。复杂查询用方法语法往往更灵活,所以日常代码以它为主。像 Count()Any()First() 这类只有方法语法才有,查询语法根本写不出来。

链式组合多个操作

方法语法的强项是「链式」:一个操作接一个操作,从左到右读。下面先筛偶数,再平方:

List<int> numbers = [1, 2, 3, 4, 5, 6];

var result = numbers
    .Where(n => n % 2 == 0)
    .Select(n => n * n);

foreach (var n in result)
{
    Console.WriteLine(n);
}

输出:4 16 36。每一环都接收一个序列、吐出一个序列,像管道一样串起来。这种「流式」写法非常符合直觉。读代码时就像读流水线:「先挑偶数,再平方」,业务逻辑直接写在脸上。

扩展方法是什么

Where 并不是 List<T> 类里的方法。它定义在 System.Linq.Enumerable 这个静态类里,通过扩展方法「挂」到了 IEnumerable<T> 上。

扩展方法三条铁律:

  1. 必须写在 static 类里;
  2. 方法本身必须 static
  3. 第一个参数用 this 修饰,表示「扩展谁」。

自己写一个试试,给 string 加个「数单词」的方法:

string text = "C# 扩展方法 很方便";
Console.WriteLine(text.WordCount());

static class StringExtensions
{
    public static int WordCount(this string s) =>
        s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}

输出:3WordCount 定义在静态类里,靠 this string s 挂到了 string 类型上,所以能像实例方法一样调用。

Tip

即使是 string 这种密封类(sealed)无法继承,扩展方法也能给它加功能,而不改动原代码。这正是 LINQ 能「无侵入」地增强所有集合的原因。

自己写一个 LINQ 风格的扩展

理解了原理,你也能写出和 Where 同款的扩展。比如给 IEnumerable<int> 加一个「取偶数索引位元素」的方法,模仿 LINQ 的延迟风格:

List<int> numbers = [10, 20, 30, 40, 50];

foreach (var n in numbers.EveryOther())
{
    Console.WriteLine(n);
}

static class EnumerableExtensions
{
    public static IEnumerable<int> EveryOther(this IEnumerable<int> source)
    {
        int i = 0;
        foreach (var item in source)
        {
            if (i % 2 == 0) yield return item;
            i++;
        }
    }
}

输出:10 30 50。注意这里用到了 yield return:方法返回 IEnumerable<int>,用 yield return 逐个产出,天然就是「延迟执行、按需计算」,和 LINQ 本身的行为一致。写扩展方法时,尽量保持「不立即算、返回序列」的约定,调用方才不会意外。

为什么 LINQ 依赖扩展方法

Enumerable 类里全是 WhereSelectOrderBy 这类静态扩展方法,第一个参数都是 this IEnumerable<T> source。于是任何实现了 IEnumerable<T> 的类型——List<T>、数组、甚至你上一章写的迭代器——都能直接点出这些方法。

int[] numbers = [5, 3, 8];
var top = numbers.Where(n => n > 4).OrderBy(n => n);
foreach (var n in top) Console.WriteLine(n);

输出:

5
8

数组也支持 LINQ,因为它同样实现了 IEnumerable<T>。这正是「扩展方法 + 接口」组合的威力:不用改 List<T>、不用改 Array,只要在 IEnumerable<T> 上挂一套方法,所有集合白赚 LINQ 能力。

扩展方法 vs 继承 vs 装饰器

你可能会想:给类型加功能,为什么不用继承?三者的取舍:

  • 继承:要求你能改类的继承体系,且只能作用于子类;stringint 这种密封类型根本没法继承。
  • 装饰器(Decorator 模式):包一层对象来加行为,灵活但要写一堆包装类,啰嗦。
  • 扩展方法:不动原类型、不建新类,直接「挂」方法上去;缺点是不能访问私有成员,且优先级低于实例方法。
Tip

给「自己控制不了的类型」(如框架里的 string、第三方类)加便捷方法,扩展方法是最轻量的选择。但记住它只是「语法糖」——本质还是个静态方法,进不去私有领地。

扩展方法的优先级

如果扩展方法和类里已有的实例方法同名,实例方法优先。扩展方法不能覆盖原有方法,只能「补充」。

// 假设 string 已有实例方法 Foo(),你又写了扩展 Foo()
// 调用 s.Foo() 时永远走实例方法,扩展方法被忽略
Note

调用扩展方法需要 using 对应命名空间。LINQ 的方法都在 System.Linq 里,.NET 10 的隐式全局 using 已默认包含,所以你通常不用手写。但如果你把自定义扩展写在自己的命名空间里,记得在调用处 using 那个命名空间,否则编译器「看不见」它。

适用场景:何时用方法语法

  1. 绝大多数日常查询:链式写法直观,且 Count/Any/First 等只有方法语法支持。
  2. 组合动态条件:可以在代码里用 if 决定是否追加 .Where(...),构建灵活查询(查询语法做不到这种动态拼装)。
  3. 参数化键:排序键、分组键可以是运行时算出的 lambda,方法语法直接传参最自然。
Warning

别因为「链式很酷」就为了链式而链式。当查询里包含复杂的 join 多表关联、group 多级分组时,查询语法的 join/group 子句往往更可读。语法只是工具,可读性优先。

新手容易踩的坑

第一,链式时容易漏掉「遍历」。和查询语法一样,方法语法也是延迟执行,必须 foreachToList() 才真正运行。

第二,把 this 参数当成「要传入的参数」。this 标记的是被扩展的对象,调用时不用传它。写 text.WordCount() 时,text 自动对应 this string s

第三,扩展类必须是 static 且非嵌套(或至少是静态类)。忘记 static 类会编译报错。

第四,别滥用扩展方法。给别人的类型加一堆同名方法会让代码难以追踪。工具类扩展可以,业务入侵要谨慎。

第五,误以为自定义扩展会自动有延迟执行。只有你像上面那样用 yield return 返回序列,才是延迟的;若你在扩展里直接 new List<T> 把所有结果塞进去返回,那就立刻执行了,和 LINQ 约定不符。

常见疑问解答

问:扩展方法会拖慢性能吗? 不会。编译后它就是个普通静态方法调用,Where(...) 等价于 Enumerable.Where(source, ...),零额外开销。

问:我写的扩展方法为什么调不出来? 多半是忘了 using 它所在的命名空间,或忘了把承载类写成 static。再检查第一条参数有没有 this

问:扩展方法能访问对象的私有字段吗? 不能。它本质是个外部静态方法,只能看到 public 成员。需要私有访问就老老实实加实例方法。

小结

方法语法用点号链式调用 LINQ 操作,灵活且主流。它背后是扩展方法:静态类 + 静态方法 + this 首参数,把新功能「挂」到已有类型上而不改源码。比起继承(改不了密封类)和装饰器(写一堆包装),扩展方法最轻量。理解这一点,就看懂了 LINQ 为什么能作用于一切可枚举类型,也为下一章深入 Where/Select 打好底。