CodeGym /课程 /C# SELF /函数式编程入门

函数式编程入门

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

1. Introduction

函数式编程(FP)是一种编程范式,它的基本构建块不是对象也不是过程/方法,而是数学意义上的 函数。在函数式编程里,重点是描述 “计算什么”,而不是 “怎么计算”

你可能已经通过 lambda 表达式和 LINQ 接触过函数式的一些想法。那区别在哪儿?实践上:OOP 描述对象和它们的交互,过程式描述一系列步骤,而函数式关注于 函数的组合、把行为作为值传递、避免修改状态(immutability),并尽量消除副作用。

Why is a new paradigm even needed?

  • 代码更干净、可预测、易测。
  • 并发支持更简单(“没有状态——没有问题”)。
  • 简洁且富有表达力(代码越少,bug 越少)。
  • 高阶、易复用的抽象。

Analogy

想象餐厅接到一个订单:“做一个煎蛋”。命令式的厨师会按步骤执行:拿鸡蛋、打蛋、打发、煎。函数式的厨师会说: res = 煎蛋(鸡蛋) —— 他用的是函数,抽象掉厨房的内部状态(嗯,差不多吧)。

在 C# 里我们可以两种风格一起用。这让语言在真实项目里非常灵活且强大。

Key concepts FP

1. Higher-order functions

函数可以作为参数传递、从其他函数返回或存到变量里。你已经用 lambda 和委托做过这些。在函数式里,这种“对函数的操作”是基本功。

2. Pure functions

如果一个函数的结果仅依赖于其参数并且不修改外部任何东西(没有副作用),那它就是“纯函数”。相同参数的两个调用会返回完全相同的结果。

3. Immutability (Immutability)

数据不在原地被改:新状态就是新对象。这让思考程序更简单,也有助于并发。

4. Absence of side effects

函数不写文件、不改全局变量、不直接在屏幕上画东西——只返回结果。现实中副作用不可避免,但我们尽量把它们隔离在系统边缘。

5. Function composition

可以把函数像积木一样组装起来。例如:先过滤正数,取平方,再求和。每个操作都是独立函数,很容易组合(WhereSelectSum)。

2. FP in C#: from theory to practice

C# 是多范式语言:它既支持 OOP、过程式,也支持强大的函数式风格(有 lambda、委托、扩展方法和 LINQ)。

Let's analyze it with our training app as an example

假设我们在做一个操作数字和字符串列表的程序。任务是用函数式风格对这些数据做各种操作。

Example 1: Using higher-order functions


// 对列表的每个元素应用一个操作
public static void ForEach<T>(List<T> items, Action<T> action)
{
    foreach (var item in items)
    {
        action(item);
    }
}

使用示例:


var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // 函数作为参数

看到没?函数可以被“当作值”存到变量里或像普通值一样传来传去——就像厨房里的苹果一样!

Example 2: Pure function

不修改程序状态且仅依赖输入的函数:


int MultiplyByTwo(int x)
{
    return x * 2;
}
  • 不依赖任何外部东西。
  • 不修改外部任何东西。
  • 对于 x = 5 永远返回 10。

对比一下会使用并修改全局变量的函数:


int total = 0;
int AddToTotal(int x)
{
    total += x;
    return total;
}

这就不是纯函数——结果依赖外部状态,而且它会改变这个状态。

Пример 3: Immutability данных

不修改输入数据,而是创建新的:


List<int> AddOneToEach(List<int> numbers)
{
    return numbers.Select(n => n + 1).ToList();
}

原始列表完全不变。在多线程程序里这特别方便:更少的锁、更少的数据竞态。

Пример 4: Function composition

求所有偶数的平方和:


int SumOfEvenSquares(List<int> numbers)
{
    return numbers
        .Where(n => n % 2 == 0)     // 只保留偶数
        .Select(n => n * n)         // 平方
        .Sum();                     // 求和
}

可读且声明式:每一步都是独立的操作。

3. Useful nuances

FP, LINQ и C#

LINQ 对集合来说几乎就是“实践中的函数式编程”:你用高阶函数(Where, Select 等),得到新的序列,不去修改原始集合,每次转换都是一个独立的表达。结果是 IEnumerable<T>,它描述的是 要得到什么,而不是 怎样迭代

Analogy table

命令式(过程式/面向对象) 函数式(LINQ/函数式风格)
foreach (var x in xs) ...
xs.Select(...)
“修改”集合 得到一个新集合
状态 (total += x) 纯函数 (xs.Sum())
按“去做什么”来描述 按“我们想得到什么”来描述

FP vs ООП: two worlds — one C#

这不是互相对立的阵营。在真实的 C# 项目里它们常常混合:领域模型(domain model)用类(OOP)建模很方便,而对集合的处理、数据聚合和转换则用函数式风格的 LINQ、lambda 和扩展方法来写。

你对委托的理解直接有用:Func<T, TResult>, Predicate<T>, Action<T> —— 这些是函数式风格常用的构件。

通用的过滤函数:


List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
    var result = new List<T>();
    foreach (var item in items)
    {
        if (predicate(item))
            result.Add(item);
    }
    return result;
}

调用示例:


var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);

比起写一堆差不多的带不同条件的方法,使用一个通用函数通常更简洁。

Зачем работодателям и собеседованиям FP-разработчики?

  • 函数式有助于在不启动整个系统的情况下测试小代码块。
  • 逻辑更容易维护:状态越少,bug 来源越少。
  • 并行和异步代码更容易写——没有全局状态,数据竞态少。

How not to be a "fanatic"?

是的,函数式很强。但 C# 并不是纯函数式语言,也并非所有场景都需要完美的纯性。在合适的地方使用局部变量和谨慎的可变操作并没有问题。关键是可读性、可预测性和可测试性。把函数式当作工具,而不是教条。

4. Common beginner mistakes

很容易写出看起来像函数式但实际上并不函数式的代码。

比如一个函数返回了新集合,但在过程中却修改了原始列表 —— 这违背了不可变性的原则,会破坏调用者的预期。

再比如:lambda 捕获并修改了外部变量。在函数式范式里这被视为副作用,会让代码行为更难预测。

C# 编译器不会拦着你:语言允许这些写法。因此在实践函数式时要注意,保证函数“自治”,不要修改外部,也不要读取外部状态,除非通过参数传入。

2
任务
C# SELF, 第 51 级, 课程 0
已锁定
列表过滤函数
列表过滤函数
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION