CodeGym /课程 /C# SELF /在 C# 中函数式编程的实践应用

在 C# 中函数式编程的实践应用

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

1. 介绍

从理论走向实践,合理会问一个问题:“作为 .NET / C# 的开发者,我为什么要学这些函数式编程的手法?”
确实,C# 不是像 F# 或 Haskell 那样的纯函数式语言。但从 3.0 版本开始一直到 C# 14,它加入了很多函数式的工具,这些东西能显著提升代码质量和表达力。

这些场景里它们尤其好用:

  • 集合处理LINQ, Map/Reduce, 过滤、聚合、排序等数据“魔法”。
  • 纯函数 — 更少因状态和副作用引起的 bug,调试更简单。
  • 高阶函数 — 写出通用、可复用的组件,用起来很爽。
  • 不可变性在并发中的作用 — 在并行和异步场景中,是保证代码安全的关键之一。
  • 函数组合 — 让复杂的业务逻辑变得简洁、可读、易测。

表格:C# 中 OOP 方法与函数式方法对比

任务 命令式(OOP / 老派) 函数式(FP)
列表过滤
foreach + if + Add
.Where(predicate)
列表转换
foreach + 计算 + Add
.Select(lambda)
按条件查找
foreach + if/return
.FirstOrDefault(predicate)
聚合
带有计数器的循环
.Aggregate(seed, func)
缓存
手动字典 + 校验
闭包函数

2. LINQ:C# 最函数化的部分

如果你觉得“函数式编程”就是处理列表、过滤器和那些 .Where, .Select, .Aggregate —— 恭喜你:确实是这样!LINQ 可以说是 C# 里的函数式精华。

回顾一下 LINQ 是怎么运作的

LINQ 用方法链来操作集合,这些方法通常接受函数做参数(比如 lambda)。例如:

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// 只取偶数并把它们翻倍
var result = numbers
    .Where(x => x % 2 == 0)
    .Select(x => x * 2);

foreach (var number in result)
    Console.WriteLine(number);

这里发生了什么?

  • .Where — 是个高阶函数:接受一个函数(x => x % 2 == 0)并返回另一个集合。
  • .Select — 同样接受一个函数(x => x * 2)。
  • 我们没有修改原始集合,而是得到一个新的结果。

这种风格易读且好扩展(可以接上 .OrderBy, .Take, .Distinct 等)。

注意! lambda 表达式是创建“临时 delegate”的便捷写法。没有对 FP 的支持,LINQ 很难成立。

示意图:集合的函数式处理


集合 --> Where(x => bool) --> Select(x => y) --> 新结果

3. 函数组合和数据处理管道

函数式编程经常用组合:把复杂操作拆成一串小函数,每个做一件事。

示例:字符串处理的流水线

有状态风格(OOP):

string s = "   hello world   ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!

更“函数式”的做法——把函数串成管道:

Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";

// 函数组合 — 按顺序应用它们
Func<string, string> pipeline = x => addBang(upper(trim(x)));

Console.WriteLine(pipeline("   hello world   ")); // HELLO WORLD!

一个最简单的组合子:

Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
    x => g(f(x));

// 现在用 Compose 来做 pipeline:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);

Console.WriteLine(pipeline2("   hello again    ")); // HELLO AGAIN!

4. 使用不可变性:防止 Bug

不可变性是 FP 的基石。我们不修改数据结构,而是返回新的。在并发程序里这点尤其重要。

示例:“不对的”(可变)

List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;

示例:“正确的”(函数式)

var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();

在现代 C# 里有 ImmutableList<T> 这样的集合,以及来自命名空间 System.Collections.Immutable 的类型:

using System.Collections.Immutable;

var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // 返回一个新列表!

5. 高阶函数在真实场景中的应用

高阶函数是写通用组件、避免大量条件分支的好办法。

示例:通用的用户过滤器

class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var users = new List<User>
{
    new User { Name = "Alice", Age = 26 },
    new User { Name = "Bob", Age = 17 },
    new User { Name = "Charlie", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
    return source.Where(u => predicate(u)).ToList();
}

// 使用:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);

6. 模式匹配和 switch 表达式

现代 C# 很多地方都用 pattern matching:switch 表达式常常替代繁重的 if 链。

object value = 123;

string description = value switch
{
    int i when i > 100 => "大数",
    string s when s.Length > 3 => "字符串很长",
    null => "空值",
    _ => "未知"
};

Console.WriteLine(description); // 大数

7. memoization:给函数结果做缓存

memoization 是对相同参数缓存函数结果的技术。在 C# 里自己实现它很简单。

Func<int, int> SlowFib = null; // 递归的斐波那契函数

var cache = new Dictionary<int, int>();

SlowFib = n =>
{
    if (cache.ContainsKey(n))
        return cache[n];
    if (n <= 1)
        cache[n] = n;
    else
        cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
    return cache[n];
};

Console.WriteLine(SlowFib(40)); // 飞快!

8. 柯里化和部分应用

部分应用是把函数的一部分参数固定住。在 C# 里用 lambda 写起来很方便。

Func<int, int, int> add = (a, b) => a + b;

// 固定第一个参数
Func<int, int> add10 = b => add(10, b);

Console.WriteLine(add10(5));   // 15
Console.WriteLine(add10(100)); // 110

9. 使用函数写声明式风格

命令式:

var result = new List<int>();
foreach (var n in numbers)
{
    if (n > 0)
        result.Add(n * n);
}

声明式:

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

10. 实践任务

实现一个“学生任务管理器”里的任务过滤模块。

模型:

class StudentTask
{
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
    public int Priority { get; set; }
}

初始数据:

var tasks = new List<StudentTask>
{
    new StudentTask { Title = "做作业", IsCompleted = false, Priority = 2 },
    new StudentTask { Title = "喝杯咖啡", IsCompleted = true, Priority = 3 },
    new StudentTask { Title = "看讲座", IsCompleted = false, Priority = 1 }
};

通用过滤器:

List<StudentTask> FilterTasks(
    List<StudentTask> all,
    Predicate<StudentTask> predicate)
{
    return all.Where(t => predicate(t)).ToList();
}

// 查找未完成且优先级 > 1 的任务
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);

// 输出结果
foreach (var task in importantTasks)
    Console.WriteLine(task.Title);

谓词组合子:

Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;

// 合并多个条件,方式 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));

// 方式 2:两个 predicate 的组合子函数
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);

var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));

11. 使用函数式方法在 C# 中的注意点和常见错误

首先,记住 C# 是强类型语言。有时候必须显式指定类型,尤其是当函数返回 delegate 或复杂的 lambda 时。否则因为类型推断失败可能会报编译错误。

其次,不要把有副作用的函数传到期望纯函数的地方。修改外部变量会破坏可预测性。尽量让你的函数不要在作用域外“污染”状态。

第三,对闭包捕获外部变量要小心,尤其是在异步或多线程代码里。被 lambda 捕获但之后值会变化的变量(比如在 LINQ 中)可能导致难以发现的 bug。

最后,过度使用函数式风格可能会让不熟悉它的团队难以维护代码。只有在它能真正简化问题时才用 FP,别为了“好看”而盲用。

2
任务
C# SELF, 第 51 级, 课程 4
已锁定
使用函数组合处理字符串
使用函数组合处理字符串
1
调查/小测验
函数式编程第 51 级,课程 4
不可用
函数式编程
函数式编程入门
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION