C++ Lambda表达式:从语法到实战的完整指南
1. 项目概述:为什么我们需要 Lambda 表达式?
在 C++ 的世界里,尤其是 C++11 标准发布之前,如果你想让一段代码逻辑像数据一样被传递、存储和复用,通常会怎么做?答案往往是函数指针,或者定义一个完整的函数对象(Functor)。函数指针用起来语法晦涩,而且难以捕获上下文变量;函数对象则需要先定义一个完整的类,重载
operator()
,代码显得冗长且分散。这就像你想在工具箱里临时放一把小螺丝刀,结果必须先跑去工厂定制一个带锁的工具箱,再把螺丝刀放进去,过程繁琐得让人失去耐心。
C++11 引入的 Lambda 表达式,就是为了解决这个“临时性”和“就地性”的痛点。它允许你在需要函数对象的地方,直接内联地定义一个匿名函数。你可以把它理解为一个“即用即抛”的代码块,它既轻便,又能“记住”它被创建时所处的环境(通过捕获列表)。这极大地提升了代码的简洁性和表现力,尤其是在 STL 算法(如
std::sort
,
std::for_each
,
std::find_if
)中,你不再需要为了一个简单的比较或操作而专门去写一个函数或类。
简单来说,Lambda 让 C++ 具备了更现代的函数式编程特性,让代码的意图更清晰,写起来也更爽快。无论你是刚接触 C++11 的新手,还是想优化旧代码的老手,深入理解 Lambda 都是提升编码效率的关键一步。
2. Lambda 表达式的核心语法拆解
一个完整的 Lambda 表达式看起来可能有点复杂,但拆开来看,它由几个明确的部分构成:
[捕获列表] (参数列表) -> 返回类型 { 函数体 }
在实际使用中,只有捕获列表和函数体是必需的,参数列表和返回类型在某些情况下可以省略,编译器会进行推导。我们来逐一拆解。
2.1 捕获列表:Lambda 的“记忆”与“视野”
捕获列表定义了 Lambda 表达式如何访问其外部作用域中的变量。这是 Lambda 最强大也最容易出错的部分。它写在方括号
[]
里。
1. 值捕获
[var]
这种方式将外部变量的值拷贝一份到 Lambda 内部。在 Lambda 内部修改这个拷贝,不会影响外部的原始变量。
int a = 10;
auto lambda = [a]() { std::cout << a << std::endl; }; // 捕获 a 的值
a = 20; // 修改外部 a
lambda(); // 输出:10。内部使用的是捕获时的拷贝,值未变
注意 :值捕获发生在 Lambda 创建时 ,而非调用时。上例中,
lambda创建时a是 10,所以内部存储的就是 10,后续外部a变为 20 与之无关。
2. 引用捕获
[&var]
这种方式捕获的是外部变量的引用。在 Lambda 内部操作这个引用,直接影响外部变量。
int a = 10;
auto lambda = [&a]() { a *= 2; }; // 捕获 a 的引用
lambda();
std::cout << a << std::endl; // 输出:20。外部 a 被修改了
警告 :引用捕获要格外小心生命周期问题。如果 Lambda 被传递到创建它的作用域之外(例如,存储起来后续调用),而它捕获的引用变量已经销毁,那么调用 Lambda 会导致未定义行为(通常是程序崩溃)。这是一种典型的“悬垂引用”错误。
3. 隐式捕获 你可以让编译器自动推断需要捕获哪些变量。
-
[=]:以值捕获的方式捕获所有外部变量。 -
[&]:以引用捕获的方式捕获所有外部变量。
int x = 1, y = 2;
auto lambda1 = [=]() { return x + y; }; // 值捕获 x 和 y
auto lambda2 = [&]() { x++; y++; }; // 引用捕获 x 和 y,会修改外部变量
虽然方便,但
不推荐滥用隐式捕获
,尤其是
[&]
。因为它会让代码的依赖关系变得不清晰,你很难一眼看出 Lambda 到底依赖了哪些外部状态,增加了维护和调试的难度。显式列出需要捕获的变量是更好的实践。
4. 混合捕获与
mutable
关键字
你可以混合使用值和引用捕获,例如
[x, &y]
表示值捕获
x
,引用捕获
y
。
默认情况下,
以值方式捕获的变量在 Lambda 函数体内是
const
的,不能被修改
。如果你需要修改值捕获的变量,需要在参数列表后加上
mutable
关键字。
int a = 10;
// 错误:没有 mutable,不能修改值捕获的变量
// auto lambda = [a]() { a++; };
// 正确:使用 mutable
auto lambda = [a]() mutable {
a++; // 这里修改的是内部的拷贝
std::cout << "内部 a: " << a << std::endl;
};
lambda(); // 输出:内部 a: 11
std::cout << "外部 a: " << a << std::endl; // 输出:外部 a: 10。外部变量不变
记住,
mutable
允许你修改的是 Lambda
内部
的那个拷贝,对外部原始变量毫无影响。
2.2 参数列表与返回类型
参数列表
()
和普通函数的参数列表几乎一样,支持默认参数、可变参数模板等。如果不需要参数,可以留空
()
,在 C++14 及以后,甚至可以完全省略
()
。
返回类型写在
->
之后。在大多数情况下,编译器可以根据函数体中的
return
语句自动推导返回类型,此时
-> 返回类型
可以省略。但当函数体中有多个
return
语句且类型不一致,或者是一个复杂的表达式时,最好显式指定返回类型以保证清晰和正确。
// 自动推导返回类型为 int
auto lambda1 = [](int a, int b) { return a + b; };
// 显式指定返回类型为 double
auto lambda2 = [](int a, int b) -> double {
if (b != 0) return static_cast<double>(a) / b;
else return 0.0;
};
// C++14 支持泛型 Lambda,参数可以用 auto
auto lambda3 = [](auto x, auto y) { return x + y; }; // 可以处理 int, double 等
2.3 Lambda 的本质:函数对象
理解 Lambda 的关键在于,
它并不是一种全新的魔法,而是一个语法糖
。编译器在背后为我们生成了一个匿名的、重载了
operator()
的类(即函数对象)。捕获列表中的变量,变成了这个匿名类的成员变量。
例如,
[a, &b]() { return a + b; }
大致会被编译器翻译成类似下面的类:
class __SomeAnonymousType {
private:
int a_copy; // 值捕获的 a
int& b_ref; // 引用捕获的 b
public:
__SomeAnonymousType(int a, int& b) : a_copy(a), b_ref(b) {}
// 可能还有其他构造函数...
auto operator()() const { // 注意,默认是 const 的
return a_copy + b_ref;
}
};
当你写下
auto lambda = ...
时,
lambda
就是这个匿名类的一个实例。这解释了为什么值捕获的变量是
const
(对应
operator() const
),以及为什么需要
mutable
来移除这个
const
限定。
3. Lambda 的典型应用场景与实操
理论说再多,不如动手写一写。下面我们结合几个典型场景,看看 Lambda 如何让代码变得更优雅。
3.1 场景一:与 STL 算法珠联璧合
这是 Lambda 最经典的应用。STL 的许多算法函数接受一个可调用对象(Callable Object)作为参数,比如谓词(Predicate)或操作函数。
例1:自定义排序规则
假设我们有一个
Person
结构体,想按年龄升序排序,年龄相同则按姓名升序排序。
struct Person {
std::string name;
int age;
};
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}};
// 使用 Lambda 作为 std::sort 的比较函数
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
if (a.age != b.age) return a.age < b.age;
return a.name < b.name;
});
// 排序后: {"Bob", 25}, {"Alice", 30}, {"Charlie", 30}
如果不用 Lambda,你需要单独写一个比较函数或者定义一个函数对象类,代码会分散且不直观。
例2:条件查找与统计
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用 std::find_if 查找第一个大于5且是偶数的数
auto it = std::find_if(nums.begin(), nums.end(),
[](int n) { return n > 5 && n % 2 == 0; });
if (it != nums.end()) {
std::cout << "找到: " << *it << std::endl; // 输出:找到: 6
}
// 使用 std::count_if 统计奇数的个数
int odd_count = std::count_if(nums.begin(), nums.end(),
[](int n) { return n % 2 != 0; });
std::cout << "奇数个数: " << odd_count << std::endl; // 输出:奇数个数: 5
例3:遍历与变换
std::vector<std::string> words = {"hello", "world", "cpp", "lambda"};
// 使用 std::for_each 将每个单词转换为大写
std::for_each(words.begin(), words.end(),
[](std::string& w) {
for (auto& c : w) c = std::toupper(c);
});
// 现在 words 是 {"HELLO", "WORLD", "CPP", "LAMBDA"}
// 使用 std::transform 生成一个新向量,包含每个单词的长度
std::vector<size_t> lengths;
lengths.reserve(words.size());
std::transform(words.begin(), words.end(), std::back_inserter(lengths),
[](const std::string& w) { return w.size(); });
// lengths 是 {5, 5, 3, 6}
3.2 场景二:作为回调函数与延迟计算
Lambda 非常适合作为一次性使用的回调函数传递给异步操作、事件处理器等。
例4:线程任务
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<std::thread> workers;
for (int i = 0; i < 5; ++i) {
// 直接使用 Lambda 创建线程任务,并捕获循环变量 i
// 注意:这里必须用值捕获 [i],如果用引用捕获 [&i],i 在循环中变化,会导致数据竞争和未定义行为。
workers.emplace_back([i]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100 * i));
std::cout << "线程 " << i << " 正在工作\n";
});
}
for (auto& t : workers) {
t.join();
}
return 0;
}
这个例子清晰地展示了值捕获在并发环境下的重要性。如果错误地使用了
[&i]
,所有线程可能都会打印出
i
最终的值(5),或者产生混乱的输出。
例5:生成函数(工厂模式) Lambda 可以返回一个 Lambda,用于创建具有特定行为的函数。
// 一个生成“加法器”的函数
auto make_adder(int x) {
// 捕获传入的参数 x,返回一个将输入加上 x 的 Lambda
return [x](int y) { return x + y; };
}
int main() {
auto add5 = make_adder(5); // 得到一个加5的函数
auto add10 = make_adder(10); // 得到一个加10的函数
std::cout << add5(3) << std::endl; // 输出 8
std::cout << add10(3) << std::endl; // 输出 13
return 0;
}
这里,
make_adder
返回的 Lambda 捕获了变量
x
,形成了一个闭包(Closure)。
add5
和
add10
就是两个不同的闭包,各自记住了不同的
x
值。这在函数式编程中非常常见。
3.3 场景三:替代简单的函数对象
对于那些逻辑简单、只用一次的小函数,用 Lambda 替代完整的函数对象类,代码会简洁得多。
例6:自定义容器的排序谓词(不使用 Lambda 的旧方式)
// 旧方式:定义一个函数对象类
struct CompareByLength {
bool operator()(const std::string& a, const std::string& b) const {
return a.size() < b.size();
}
};
std::set<std::string, CompareByLength> old_set;
使用 Lambda(C++11 及以后)
在 C++11 中,Lambda 的类型是唯一的、匿名的,不能直接用作模板参数。但我们可以利用
decltype
和
std::function
。
// 方式1:使用 decltype 获取 Lambda 的类型(需要将 Lambda 赋值给一个变量)
auto comp = [](const std::string& a, const std::string& b) { return a.size() < b.size(); };
std::set<std::string, decltype(comp)> new_set(comp); // 注意:需要将 comp 作为构造函数参数传入
// 方式2:使用 std::function 包装(更通用,但有轻微性能开销)
std::set<std::string, std::function<bool(const std::string&, const std::string&)>>
func_set([](const std::string& a, const std::string& b) { return a.size() < b.size(); });
实操心得 :对于像
std::sort这种将可调用对象作为函数参数(而非模板参数)的情况,直接传入 Lambda 即可,最简单。对于需要作为类型参数(如容器比较器、智能指针删除器)的情况,优先考虑使用decltype的方式,它保留了 Lambda 的原始类型,通常没有额外开销。std::function是一个类型擦除的包装器,更灵活,但会引入间接调用和可能的内存分配,在性能敏感的场合需谨慎使用。
4. 进阶技巧与性能考量
当你熟悉了 Lambda 的基本用法后,可以关注一些进阶特性和性能相关的问题。
4.1 泛型 Lambda (C++14)
C++14 允许在 Lambda 的参数列表中使用
auto
,这创造了一个“泛型 Lambda”。它本质上是一个模板化的
operator()
。
// 一个可以接受任何支持 + 运算类型的 Lambda
auto add = [](auto x, auto y) { return x + y; };
std::cout << add(1, 2) << std::endl; // int, 输出 3
std::cout << add(3.14, 2.71) << std::endl; // double, 输出 5.85
std::string s1 = "Hello, ", s2 = "Lambda!";
std::cout << add(s1, s2) << std::endl; // std::string, 输出 "Hello, Lambda!"
这极大地增强了 Lambda 的灵活性,你可以用它写出非常通用的工具函数。
4.2 初始化捕获 (C++14) 与广义捕获 (C++20)
C++14 初始化捕获
:允许你在捕获列表中直接初始化成员变量。这对于移动捕获(捕获只能移动的对象,如
std::unique_ptr
)或为值捕获的变量起别名非常有用。
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// 错误:不能直接捕获 unique_ptr,因为它不可拷贝
// auto lambda = [ptr]() { ... };
// 正确:使用初始化捕获移动它
auto lambda = [data = std::move(ptr)]() { // data 是 Lambda 内部的成员变量
if (data) std::cout << *data << std::endl;
};
// 此时 ptr 变为 nullptr
C++20 广义 Lambda 捕获 :语法更统一,允许在捕获列表中直接写复杂的表达式。
int x = 10, y = 20;
// C++20: 在捕获列表中计算并初始化
auto lambda = [z = x + y]() { return z; };
std::cout << lambda() << std::endl; // 输出 30
4.3 Lambda 与
constexpr
、
consteval
(C++17/C++20)
从 C++17 开始,在满足一定条件(例如捕获列表为空)时,Lambda 可以是
constexpr
的,意味着它可以在编译期求值。C++20 进一步引入了
consteval
指定立即函数。
// C++17: constexpr Lambda
constexpr auto square = [](int n) constexpr { return n * n; };
static_assert(square(5) == 25); // 编译期计算
// C++20: 可以显式指定 consteval
auto cube = [](int n) consteval { return n * n * n; };
static_assert(cube(3) == 27);
这为编译期元编程和性能优化打开了新的大门。
4.4 性能考量:内联与开销
一个常见的误区是认为 Lambda 一定有性能开销。实际上,对于简单的、捕获列表不复杂的 Lambda,编译器会非常积极地进行内联优化,其性能与手写的循环或内联函数几乎没有区别,甚至更好,因为编译器能更清楚地看到上下文。
性能开销主要出现在以下情况:
- 捕获了大量或大对象 :值捕获会触发拷贝构造,引用捕获虽无拷贝但需注意生命周期。
-
通过
std::function存储和传递 :std::function使用了类型擦除和动态分配,会带来间接调用开销。在热循环中,直接使用auto变量存储 Lambda 或使用模板传递是更好的选择。 - 复杂的 Lambda :函数体过于复杂可能影响内联决策。
黄金法则
:在性能关键路径上,尽量使用简单的、捕获列表小的 Lambda,并避免不必要的
std::function
包装。使用
-O2
或
-O3
优化等级,让编译器充分发挥作用。
5. 常见问题与避坑指南实录
在实际项目中踩过不少坑,这里总结几个最常见的问题和排查思路。
5.1 生命周期陷阱:悬垂引用
这是引用捕获最危险的坑。
std::function<int()> create_lambda() {
int local_var = 100;
// 危险!捕获了局部变量 local_var 的引用
return [&local_var]() { return local_var; };
} // 函数结束,local_var 被销毁
int main() {
auto func = create_lambda();
int val = func(); // 未定义行为!访问已销毁的内存
return 0;
}
排查与解决 :
- 症状 :程序在调用 Lambda 时崩溃(段错误),或输出毫无意义的值。
-
检查
:仔细审查 Lambda 的捕获列表。如果使用了
[&]或[&var],问自己:这个 Lambda 对象的生命周期是否可能超过它所捕获的引用变量的生命周期? -
解决
:
- 如果 Lambda 只在当前作用域内使用,引用捕获是安全的。
-
如果 Lambda 需要被存储或传递到更长的生命周期中(如返回、放入容器、启动新线程),
对于局部变量,优先考虑值捕获
[var]或使用std::shared_ptr/std::unique_ptr管理资源并通过值捕获指针 。 -
对于类成员变量,捕获
this指针([this]或[=]会隐式捕获this)同样有生命周期问题。确保 Lambda 被调用时,所属的对象实例仍然存活。
5.2 默认捕获
[=]
和
[&]
的隐患
隐式捕获会让代码意图模糊,并可能引入意外的捕获。
class MyClass {
int member = 10;
public:
void do_work() {
int local = 20;
// 你以为只捕获了 local?不,[=] 隐式捕获了 this!
auto lambda = [=]() {
// 这里实际上可以访问 this->member,因为 [=] 捕获了 this 指针
std::cout << local + member << std::endl; // 访问了成员变量
};
lambda();
}
};
如果这个
lambda
被传递到对象生命周期之外调用,访问
member
就会出错。
建议
:始终使用显式捕获列表,明确列出所有需要捕获的变量。这就像写文档,让后来者(包括未来的你)一眼就能看懂 Lambda 的依赖关系。
5.3
mutable
的误用
新手常犯的一个错误是,以为加了
mutable
就能修改外部变量。
int a = 0;
auto lambda = [a]() mutable {
a = 42; // 修改的是内部拷贝
std::cout << "内部: " << a << std::endl;
};
lambda(); // 输出:内部: 42
std::cout << "外部: " << a << std::endl; // 输出:外部: 0
记住,
mutable
只影响值捕获变量在 Lambda
内部
的
const
属性。要影响外部变量,必须使用引用捕获
[&a]
。
5.4 Lambda 的类型与存储
每个 Lambda 表达式都会生成一个唯一的、匿名的类型。因此,两个看起来一模一样的 Lambda,其类型也是不同的。
auto lambda1 = [](){};
auto lambda2 = [](){};
// static_assert(std::is_same_v<decltype(lambda1), decltype(lambda2)>); // 编译错误!类型不同
这意味着你不能直接声明一个“Lambda 类型”的容器(如
std::vector<decltype([](){})>
)。如果需要存储不同类型的可调用对象,可以使用
std::function
,但要注意其性能开销。另一种方法是使用模板,让容器存储特定类型的 Lambda。
5.5 在构造函数初始化列表中捕获成员变量
这是一个有点隐蔽的坑。你无法在 Lambda 中直接捕获成员变量,因为 Lambda 定义时,类的
this
指针可能还未完全初始化(特别是在构造函数初始化列表中)。
class Widget {
int data;
public:
Widget(int x) : data(x) {
// 这里可以,对象已构造完成
auto lambda = [this]() { std::cout << data; };
}
// 错误!不能在初始化列表中捕获成员
// Widget(int x) : data(x), myLambda([this](){ std::cout << data; }) {}
// std::function<void()> myLambda;
};
解决
:如果必须在初始化阶段就建立与成员变量相关的回调,考虑在构造函数体内进行赋值,或者使用诸如
std::optional
或延迟初始化的模式。
6. 综合例题与实战演练
让我们通过几个稍微复杂一点的例题,把前面的知识点串联起来。
例题1:实现一个简单的命令模式 命令模式将请求封装为对象。我们可以用 Lambda 来轻松创建命令对象。
#include <iostream>
#include <vector>
#include <functional>
#include <memory>
class Command {
public:
virtual ~Command() = default;
virtual void execute() const = 0;
};
// 使用 Lambda 实现的具体命令类
class LambdaCommand : public Command {
std::function<void()> action;
public:
// 构造函数接受任何可调用对象
template<typename Func>
LambdaCommand(Func&& f) : action(std::forward<Func>(f)) {}
void execute() const override {
if (action) action();
}
};
int main() {
std::vector<std::unique_ptr<Command>> commands;
int counter = 0; // 外部状态
// 添加几个命令
commands.push_back(std::make_unique<LambdaCommand>(
[]() { std::cout << "命令A: 打印 Hello\n"; }
));
commands.push_back(std::make_unique<LambdaCommand>(
[&counter]() {
counter++;
std::cout << "命令B: 增加计数器,当前值: " << counter << "\n";
}
));
commands.push_back(std::make_unique<LambdaCommand>(
[&counter]() {
if (counter > 0) counter--;
std::cout << "命令C: 减少计数器,当前值: " << counter << "\n";
}
));
// 执行所有命令
for (const auto& cmd : commands) {
cmd->execute();
}
// 输出可能为:
// 命令A: 打印 Hello
// 命令B: 增加计数器,当前值: 1
// 命令C: 减少计数器,当前值: 0
return 0;
}
这个例子展示了如何用 Lambda 和
std::function
灵活地创建行为对象,并组合到设计模式中。注意命令B和C捕获了
counter
的引用,所以它们能修改并共享这个状态。
例题2:使用 Lambda 实现一个简单的“管道”操作
模拟函数式编程中的管道
|>
操作,将一个函数的输出作为另一个函数的输入。
#include <iostream>
#include <vector>
#include <algorithm>
template<typename T, typename Func>
auto operator|(T&& val, Func&& func) -> decltype(func(std::forward<T>(val))) {
return func(std::forward<T>(val));
}
int main() {
// 定义一系列处理 Lambda
auto double_val = [](int x) { return x * 2; };
auto add_five = [](int x) { return x + 5; };
auto to_string = [](int x) { return std::to_string(x); };
auto add_exclamation = [](const std::string& s) { return s + "!"; };
int initial_value = 10;
// 使用“管道”语法串联操作
std::string result = initial_value
| double_val // 20
| add_five // 25
| to_string // "25"
| add_exclamation; // "25!"
std::cout << result << std::endl; // 输出:25!
// 更复杂的例子:处理容器
std::vector<int> nums = {1, 2, 3, 4, 5};
auto filter_even = [](const std::vector<int>& v) {
std::vector<int> result;
std::copy_if(v.begin(), v.end(), std::back_inserter(result),
[](int n) { return n % 2 == 0; });
return result;
};
auto sum = [](const std::vector<int>& v) {
int total = 0;
for (int n : v) total += n;
return total;
};
int even_sum = nums | filter_even | sum;
std::cout << "偶数和: " << even_sum << std::endl; // 输出:偶数和: 6 (2+4)
return 0;
}
这个例子展示了 Lambda 如何帮助构建声明式、易读的代码链。每个 Lambda 都是一个小的、纯粹的数据转换单元,通过管道组合起来完成复杂任务。它体现了函数式编程的思想,在 C++ 中借助 Lambda 也能很好地实现。
例题3:Lambda 在异步编程中的状态管理 模拟一个简单的异步任务,展示如何安全地管理状态。
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
std::future<int> start_async_task(int base_value) {
// 使用 shared_ptr 安全地共享数据
auto shared_data = std::make_shared<int>(base_value);
// 将任务提交到异步线程
// 注意:Lambda 按值捕获 shared_ptr,确保数据生命周期
return std::async(std::launch::async, [shared_data]() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
(*shared_data) *= 2; // 修改数据
return *shared_data;
});
}
int main() {
auto fut = start_async_task(21);
// 主线程可以继续做其他事情...
std::cout << "任务已启动,等待结果...\n";
// 获取异步结果
int result = fut.get();
std::cout << "异步任务结果: " << result << std::endl; // 输出:42
return 0;
}
这个例子关键点在于,异步任务可能比创建它的函数活得更久。通过将需要共享的数据放在
std::shared_ptr
中,并由 Lambda 值捕获这个智能指针,我们确保了数据在所有使用它的地方都有效,避免了悬垂指针或引用。这是一种在多线程环境下使用 Lambda 的经典安全模式。
踩过 Lambda 的这些坑之后,我的体会是,它的便利性毋庸置疑,但“能力越大,责任越大”。清晰明确的捕获列表、对生命周期的警惕、以及对
std::function
性能开销的认知,是写出稳健高效 Lambda 代码的基石。尤其是在团队协作中,一个带隐式捕获的复杂 Lambda 可能会成为调试的噩梦。所以,我的习惯是:能用显式捕获就不用隐式,能用值捕获就不用引用(除非明确需要修改外部变量或处理移动语义),在性能热点路径上,反复权衡 Lambda 的简洁性与直接手写循环或函数对象的性能差异。
更多推荐
所有评论(0)