# 函数作为参数:把筛选规则交给调用方
学习目标:让遍历流程保持不变,分别传入“正数”和“偶数”规则,并看清回调的执行与失败责任。
前置知识:函数、循环、布尔表达式;lambda 是内联定义的小函数,本篇不要求理解捕获。
# 问题与边界
对有限整数序列计数:从左至右,对每个元素同步调用规则一次,返回满足规则的个数,不修改输入,不保存规则。输入 [-2, 0, 3, 4],正数规则得到 2,偶数规则得到 3;空输入返回 0 且不调用规则。不分配筛选结果容器,因为任务只需要数量。规则不能在遍历期间修改输入;如果规则失败,整个调用失败,没有有效计数结果,也不自动重试。
# 关键差异
| 选择 | 在本例的作用 | 成本或限制 |
|---|---|---|
| Rust 泛型 FnMut / C++ 模板 | 调用可内联的具体可调用类型 | 不同类型可产生不同实例 |
| Java IntPredicate | 统一函数式接口,直接接收 int | 不是任意同名方法对象 |
| Go 函数值 / Kotlin 函数类型 | 参数明确表示可调用签名 | 具体运行表示由实现决定 |
| JS 动态回调 / TS 签名 | 运行逻辑相同 | TS 注解不验证回调真值语义 |
| Python Callable | 可读的静态契约 | 注解不拦截运行时异常 |
这里“高阶”仅指接收函数,不意味着并发、惰性求值或函数式程序必然没有状态。
# 各语言实现
# Rust
成功时无输出,所有语义检查通过。
Rustfn count_if(items: &[i32], mut rule: impl FnMut(i32) -> bool) -> usize {
let mut count = 0;
for &item in items { if rule(item) { count += 1; } }
count
}
fn positive(n: i32) -> bool { n > 0 }
fn main() {
let items = [-2, 0, 3, 4];
assert_eq!(count_if(&items, positive), 2);
assert_eq!(count_if(&items, |n| n % 2 == 0), 3);
assert_eq!(count_if(&[], |_| panic!("must not run")), 0);
}
FnMut 接受可变捕获状态的回调,也能接收普通函数。Fn/FnMut/FnOnce 的差别不是返回值类型;本例循环多次调用,不应仅要求 FnOnce。Rust 闭包 trait。
# Go
成功时无输出,所有语义检查通过。
Gopackage main
func countIf(items []int, rule func(int) bool) int {
count := 0
for _, item := range items { if rule(item) { count++ } }
return count
}
func positive(n int) bool { return n > 0 }
func main() {
items := []int{-2, 0, 3, 4}
if countIf(items, positive) != 2 || countIf(items, func(n int) bool { return n%2 == 0 }) != 3 { panic("count") }
if countIf(nil, func(int) bool { panic("must not run") }) != 0 { panic("empty") }
}
传函数值时没有调用括号;positive() 会立即调用且缺少实参。nil 函数值不是可调用规则,调用方必须传入真实函数。Go 函数类型。
# Java
成功时预期输出为 "rule failed\n"(\n 表示换行)。
Javaimport java.util.function.IntPredicate;
public class Main {
static int countIf(int[] items, IntPredicate rule) {
int count = 0;
for (int item : items) if (rule.test(item)) count++;
return count;
}
static boolean positive(int n) { return n > 0; }
public static void main(String[] args) {
int[] items = {-2, 0, 3, 4};
if (countIf(items, Main::positive) != 2 || countIf(items, n -> n%2 == 0) != 3) throw new AssertionError();
if (countIf(new int[0], n -> { throw new AssertionError("must not run"); }) != 0) throw new AssertionError();
try { countIf(items, n -> { throw new IllegalStateException("rule failed"); }); throw new AssertionError(); }
catch (IllegalStateException e) { System.out.println(e.getMessage()); }
}
}
选择 IntPredicate 避免为每个 int 装箱成 Integer。方法引用与 lambda 都适配这个函数式接口;受检异常不能未经处理直接穿过此接口签名。调用方在失败后不使用不存在的计数。IntPredicate。
# JavaScript
成功时预期输出为 "rule failed\n"(\n 表示换行)。
JavaScriptfunction countIf(items, rule) {
let count = 0;
for (const item of items) if (rule(item)) count++;
return count;
}
function positive(n) { return n > 0; }
const items = [-2, 0, 3, 4];
if (countIf(items, positive) !== 2 || countIf(items, n => n % 2 === 0) !== 3) throw new Error("count");
if (countIf([], () => { throw new Error("must not run"); }) !== 0) throw new Error("empty");
try {
countIf(items, () => { throw new Error("rule failed"); });
throw new Error("unexpected success");
} catch (e) {
if (!(e instanceof Error) || e.message !== "rule failed") throw e;
console.log(e.message);
}
同步回调不是 Promise 协议:传入 async 函数会返回真值对象,使这个计数函数错误地把元素计入;异步筛选应另定义等待策略。这里的规则必须返回 boolean。ECMAScript 函数调用。
# TypeScript
TypeScriptfunction countIf(items: readonly number[], rule: (n: number) => boolean): number {
let count = 0;
for (const item of items) if (rule(item)) count++;
return count;
}
function positive(n: number): boolean { return n > 0; }
const items = [-2, 0, 3, 4];
if (countIf(items, positive) !== 2 || countIf(items, n => n % 2 === 0) !== 3) throw new Error("count");
if (countIf([], () => { throw new Error("must not run"); }) !== 0) throw new Error("empty");
try {
countIf(items, () => { throw new Error("rule failed"); });
throw new Error("unexpected success");
} catch (e) {
if (!(e instanceof Error) || e.message !== "rule failed") throw e;
console.log(e.message);
}
显式 boolean 返回类型使 async 回调不符合签名;这保护的是类型化调用,不是任意外部 JavaScript 调用。TypeScript 函数表达式类型。
# Kotlin
成功时无输出,所有语义检查通过。
Kotlinfun countIf(items: IntArray, rule: (Int) -> Boolean): Int {
var count = 0
for (item in items) if (rule(item)) count++
return count
}
fun positive(n: Int): Boolean = n > 0
fun main() {
val items = intArrayOf(-2, 0, 3, 4)
check(countIf(items, ::positive) == 2)
check(countIf(items) { it % 2 == 0 } == 3)
check(countIf(intArrayOf()) { error("must not run") } == 0)
}
尾随 lambda 只是把最后一个函数参数移到括号外,不改变同步执行时机。本例不加 inline;若性能分析证明回调开销重要,再评估内联及非局部返回等语义影响。Kotlin 高阶函数。
# Python
成功时预期输出为 "rule failed\n"(\n 表示换行)。
Pythonfrom collections.abc import Callable, Sequence
def count_if(items: Sequence[int], rule: Callable[[int], bool]) -> int:
count = 0
for item in items:
if rule(item):
count += 1
return count
def positive(n: int) -> bool:
return n > 0
def failed(n: int) -> bool:
raise ValueError("rule failed")
items = [-2, 0, 3, 4]
assert count_if(items, positive) == 2
assert count_if(items, lambda n: n % 2 == 0) == 3
assert count_if([], failed) == 0
try:
count_if(items, failed)
except ValueError as e:
print(e)
else:
raise AssertionError("unexpected success")
函数对象可直接传递;lambda 并非唯一形式,复杂规则应使用命名函数。异常在调用回调的那次迭代传播,调用方负责报告而非返回部分计数。Python 函数定义。
# C++
成功时无输出,所有语义检查通过。
C++#include <cassert>
#include <vector>
template<class Rule>
std::size_t count_if(const std::vector<int>& items, Rule rule) {
std::size_t count = 0;
for (int item : items) if (rule(item)) ++count;
return count;
}
bool positive(int n) { return n > 0; }
int main() {
const std::vector<int> items{-2, 0, 3, 4};
assert(count_if(items, positive) == 2);
assert(count_if(items, [](int n) { return n % 2 == 0; }) == 3);
assert(count_if({}, [](int) -> bool { throw 1; }) == 0);
}
按值接收本例的函数指针或无捕获 lambda 不需要动态分配。并非所有场合都需要 std::function;当必须存储异构可调用值时才考虑类型擦除及其成本。C++ 工作草案:lambda持续更新,示例使用 C++17。
# 迁移误区与适用限制
countIf(items, rule())与传rule不等价:前者在进入遍历前就执行,且结果未必可调用。- 异步规则不能偷换进同步接口;JS 真值转换会掩盖错误,TS 签名则可提前阻止类型化调用。
- 为重试规则异常而重复调用回调会改变“每项一次”契约,带副作用的规则可能重复扣费或记录日志。本函数从不重试。
- 计数函数不缓存回调,所以生命周期只覆盖当前调用。若改为保存回调,捕获对象的寿命与共享状态会成为新责任,见下一篇闭包。
# 运行与验证
每种语言在独立目录按上节文件名和命令执行。Rust/C++ 的可执行程序在 PowerShell 中用 .\main.exe,POSIX shell 中用 ./main。基线见主题首页,实际执行证据集中记入验证记录。本文给出预期语义与检查,不把编写或阅读核对宣称为运行通过;各程序应由验证流程执行,类型错误反例的文字说明不等于已运行负向编译测试。