CodeGym /课程 /C# SELF /高阶函数

高阶函数

C# SELF
第 51 级 , 课程 2
可用

1. 引言

你在学校大概听过别人说:“这是个关于函数的函数”。高阶函数(Higher-Order Functions, HOF)就是那些要么把函数作为参数,要么返回函数作为结果,或者两者都做的函数。

简单点说:如果你的方法能接收另一个函数(比如委托或 lambda)作为参数,或者把它作为返回结果——恭喜,你有了高阶函数!

高阶函数就像万能钥匙,不仅能自己开门,还能把另一把钥匙交给你,让你以后再去开需要的门。

现实中的应用

这听起来挺高级,但对日常 C# 开发者来说有啥用?最短的答案是:
高阶函数让代码更灵活、可复用、简洁。
它们是像 LINQ、集合的过滤和排序、构建数据处理管道、配置回调、事件甚至依赖注入之类功能的基础。

几个实际场景:

  • 通过传入逻辑(比如过滤、排序、转换)来限制方法的行为。
  • 写一个通用的数据处理器,通过委托“拧入”你需要的操作。
  • 构建处理链(“管道”),每个函数按自己的“配方”修改数据。
  • 做跨平台抽象:Windows 上做什么,Linux 上做什么?只要传入不同的函数就行。

2. 简单的高阶函数示例

接受另一个函数的函数

最经典的例子 —— 一个接受委托或 lambda 的方法。


// 高阶函数:接受名为 process 的函数作为参数
void ForEach<T>(IEnumerable<T> collection, Action<T> process)
{
    foreach (var item in collection)
    {
        process(item); // 调用传入的函数参数
    }
}

// 使用示例:
var numbers = new List<int> { 1, 2, 3 };
ForEach(numbers, n => Console.WriteLine($"元素: {n}"));

这里发生了什么?方法 ForEach 并不知道应该对每个元素做什么。它做的就是调用传入的处理器(process)。这个处理器可以是任何东西——打印到屏幕、保存到数据库、在界面上绘制等等。

没错,集合上的 ForEach 就是这么工作的,几乎所有的 LINQ 方法也是高阶函数!

返回函数的函数

再来一个稍重一点的例子——方法不仅接受函数,还返回函数。


// 高阶函数:返回另一个函数
Func<int, int> CreateMultiplier(int factor)
{
    // 返回使用外部变量 factor 的 lambda
    return x => x * factor;
}

// 使用示例:
var multiplyBy10 = CreateMultiplier(10);
Console.WriteLine(multiplyBy10(7)); // 70

这里 CreateMultiplier 返回一个函数,该函数把它的参数乘以预设的 factor。这已经不仅是 HOF,还是个“函数工厂”。

既接受又返回函数的函数


Func<int, int> Compose(Func<int, int> f, Func<int, int> g)
{
    // 返回一个先执行 g 然后执行 f 的函数:f(g(x))
    return x => f(g(x));
}

// 使用示例:
Func<int, int> increment = x => x + 1;
Func<int, int> doubleIt = x => x * 2;

var incrementThenDouble = Compose(doubleIt, increment);

Console.WriteLine(incrementThenDouble(5)); // (5 + 1) * 2 = 12

正是这样的组合构成了流式数据处理的基础——比如当你连续对数组调用 SelectWhereOrderBy 等方法时。

3. C# 如何支持高阶函数

在函数式语言(Haskell、F#)里所有函数本身默认就是高阶的。但 C#(从 2.0 起)也支持这种风格,靠的是委托和 lambda。

  • 委托(Func, Action, Predicate)——函数类型。
  • Lambda 表达式——在现场创建函数的语法糖。
  • 方法可以接收和返回委托——所以高阶函数是“开箱即用”的特性。

可视化示意

flowchart LR
    A[数据] --> B[函数 1]
    B --> C[函数 2]
    C --> D[结果]
    subgraph "处理管道(高阶函数)"
        B
        C
    end

4. 扩展我们的应用

继续完善我们的逐步演示应用——假设这是个“迷你字符串处理器”。

加入一个简单的高阶方法

假设我们有一个用户名字列表,想通过传入函数随意修改它们。


// 接受字符串列表和一个转换函数的方法
List<string> TransformNames(List<string> names, Func<string, string> transformer)
{
    var result = new List<string>();
    foreach (var name in names)
    {
        result.Add(transformer(name));
    }
    return result;
}

如何使用这个方法?


var names = new List<string> { "安娜", "鲍里斯", "谢尔盖" };

// 转成大写
var upperNames = TransformNames(names, n => n.ToUpper());

// 给每个名字加上“尊称”
var politeNames = TransformNames(names, n => "尊敬的(先生/女士) " + n);

foreach (var n in upperNames)
    Console.WriteLine(n); // ANNA, BORIS, SERGEI(实际取决于 ToUpper 的本地化)

foreach (var n in politeNames)
    Console.WriteLine(n); // 尊敬的(先生/女士) 安娜, ...

方法 TransformNames 很通用:它把转换逻辑委托给传入的参数(transformer),比如调用 ToUpper 或任何别的“配方”。

类型适配

示例可以很容易地改造成适配任意数据类型。


// 泛型通用方法 - 高阶函数,适用于任意 T
List<TResult> Map<T, TResult>(List<T> items, Func<T, TResult> transformer)
{
    var result = new List<TResult>();
    foreach (var item in items)
    {
        result.Add(transformer(item));
    }
    return result;
}

应用:


var numbers = new List<int> { 1, 2, 3 };
var doubled = Map(numbers, x => x * 2); // [2, 4, 6]
var strings = Map(numbers, x => $"数字: {x}"); // ["数字: 1", ...]

5. 通过高阶函数做过滤和聚合

过滤和查找的逻辑长期以来都是通过高阶函数来实现的。


// 过滤:高阶函数
List<T> Filter<T>(List<T> items, Predicate<T> criteria)
{
    var result = new List<T>();
    foreach (var item in items)
    {
        if (criteria(item)) // 调用判定函数
        {
            result.Add(item);
        }
    }
    return result;
}

如何使用:


var names = new List<string> { "安娜", "鲍里斯", "安德烈" };
var aNames = Filter(names, n => n.StartsWith("安"));
// 结果: "安娜", "安德烈"

6. 函数组合的概念 (function composition)

高阶函数不仅能用来组合单独的函数,还能把它们串成链条——即所谓的组合。在 C# 里可以实现成接受两个函数并返回它们组合的新函数。


// 函数组合器:返回一个先执行 g 再执行 f 的函数
Func<T, TResult> Compose<T, TIntermediate, TResult>(
    Func<TIntermediate, TResult> f,
    Func<T, TIntermediate> g)
{
    return x => f(g(x));
}

// 示例:
Func<int, int> plusOne = n => n + 1;
Func<int, int> timesTwo = n => n * 2;

var plusOneThenDouble = Compose(timesTwo, plusOne);
Console.WriteLine(plusOneThenDouble(3)); // (3 + 1) * 2 = 8

7. 有用的细节

问题的前世今生:为什么以前更复杂?

在有委托和 lambda 之前,开发者经常写一堆几乎相同的循环,复制代码片段去“过滤”、“转换”或“分组”数据。随着高阶函数的出现,可以把“可变的行为部分”抽取成独立的函数参数——从而极大减少重复并提升代码表达力。

一点语法糖:函数作为表达式

高阶函数经常用 expression-bodied 方法实现——简洁的单行方法:


List<string> FilterNames(Predicate<string> pred) =>
    Names.Where(name => pred(name)).ToList();

List<TResult> MapNames<TResult>(Func<string, TResult> transformer) =>
    Names.Select(transformer).ToList();

与 LINQ 机制的比较

来看下 LINQ 是如何用高阶函数的:

LINQ 方法 接受的委托类型 用途
Where
Func<T, bool>
过滤元素
Select
Func<T, TResult>
转换元素
OrderBy
Func<T, TKey>
按键排序
Aggregate
Func<TAcc, T, TAcc>
聚合(折叠)集合
Any
Func<T, bool>
根据条件检查是否存在元素

这些方法都是围绕高阶函数的思想构建的:你写自己的处理规则,标准库提供“基础设施”。

8. 可能的错误和坑

委托类型混淆。
一开始可能不太清楚什么时候用 Action,什么时候用 Func,什么时候用 Predicate
提示:如果函数返回 bool —— 多半是 Predicate。如果返回一个值 —— 用 Func,如果不返回 —— 用 Action

变量捕获(closures)。
如果返回的函数使用了外部作用域的变量,注意这些变量在调用时的值是否仍然是你期望的。变量不是复制的,而是被“捕获”了。

调试复杂链条。
当函数被组合成很长的管道时,很难定位哪一层处理得不对。加一些临时输出和注释帮助调试:


n => {
  Console.WriteLine("操作前:" + n);
  var res = n * 2;
  Console.WriteLine("操作后:" + res);
  return res;
}
2
任务
C# SELF, 第 51 级, 课程 2
已锁定
用高阶函数进行过滤
用高阶函数进行过滤
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION