我们也有命名参数了——家庭版

Lobsters Hottest 新闻

摘要

文章探讨了Rust通过类型系统和结构体模拟命名参数的方法,这是一种无需新增语言特性的便捷替代方案。

<p><a href="https://lobste.rs/s/4cbau4/we_have_named_arguments_at_home">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/24 19:03

# 我们家也有命名参数 来源:https://corrode.dev/blog/named-arguments-at-home/ Steve Klabnik 最近撰文讨论了(https://steveklabnik.com/writing/arguing-about-arguments/)命名参数、可选参数、默认参数、函数重载,以及为何在 Rust 中这些设计在历史上常让他感到不安。我认同 Steve 的观点。事实上,我认为我的认同程度甚至略强于 Steve。:) 我其实认为,无需添加任何新的语言特性,我们就能获得大部分想要的功能。相反,我们可以充分利用 Rust 现有的特性。这些方案都无法完全替代你在 Python、Ruby、C++ 或 Kotlin 中获得的体验,但这正是要点所在。相反,你无需在函数调用中引入任何“魔法”,就能获得约 80% 的易用性。反复出现的模式是,Rust 将其他语言置于**函数调用语义**中的某些内容,表示为其类型系统的正常部分,从而优雅地回避了上述设计问题。 ## 家里的命名参数(https://corrode.dev/blog/named-arguments-at-home/#named-arguments-at-home) 让我们回顾 Steve 使用 `image` crate 的例子: ```rust pub fn crop_imm( image: &I, x: u32, y: u32, width: u32, height: u32, ) -> SubImage<&I> { // ... } let cropped = image::imageops::crop_imm(&img, 10, 20, 200, 100); ``` 显而易见的问题在于,连续四个 `u32` 并非一个无需查阅文档就能可靠使用的绝佳 API。假设我们拥有命名参数: ```rust let cropped = image::imageops::crop_imm( image: &img, x: 10, y: 20, width: 200, height: 100, ); ``` 这显然更好,但稳定的 Rust 提供了另一种替代语法:结构体。 ```rust struct Crop { x: u32, y: u32, width: u32, height: u32, } fn crop_imm( image: &I, crop: Crop, ) -> SubImage<&I> { // ... } let cropped = image::imageops::crop_imm( &img, Crop { x: 10, y: 20, width: 200, height: 100 }, ); ``` 结构体就是一个带有一个额外类型名的命名参数。更重要的是,我们还获得了字段顺序的任意性: ```rust Crop { width: 200, height: 100, x: 10, y: 20 } ``` 我们还免费获得了拼写检查、自动补全和逐字段文档!而且,我们可以在类型上添加不变量,并将参数作为值传递。或许最重要的是,**名称属于类型**,而不是成为每个函数调用约定的一部分。这一特性巧妙地避免了真实命名参数的几个问题。考虑函数指针: ```rust fn resize(width: u32, height: u32) {} fn offset(dx: u32, dy: u32) {} let f: fn(u32, u32) = if resizing { resize } else { offset }; ``` `f` 的参数名会是什么?使用参数结构体,这个问题根本不会出现: ```rust struct Size { width: u32, height: u32 } fn resize(size: Size) {} // 再也不用争论参数了 let f: fn(Size) = resize; ``` 如果名称在语义上重要,那就为名称赋予一个类型。如果不重要,那就别这么做。 这里还有另一个令人愉快的优点。Steve 提到了求值顺序: ```rust consume(length: data.len(), data: data); ``` 即,参数的求值顺序应该按照它们在调用点出现的顺序,还是按照它们在声明中出现的顺序?这里,这意味着在移动 `data` 之前计算 `data.len()` 还是之后?Rust 已经为结构体回答了这个问题: ```rust let args = Args { length: data.len(), data }; ``` 表达式就在你编写它们的地方求值。无需新规则。这对我来说感觉非常地道:与其给函数调用引入第二套类似字段但语义微妙不同的语法,不如直接使用已有的字段语法。 ### 参数是你领域的一部分(https://corrode.dev/blog/named-arguments-at-home/#arguments-are-part-of-your-domain) 当然,为每个双参数函数都声明一个定制的参数类型是荒谬的。我不会这样写: ```rust struct PushArgs { value: T } vec.push(PushArgs { value: 42 }); ``` 那样就太傻了。诀窍在于,注意到命名参数最有用的地方正是参数组合在概念上变得有意义的地方,这与你寻求使用结构体的时机相同。 这些是不好的: ```rust draw(x1, y1, x2, y2, width, opacity); connect(host, port, timeout, retries, tls); ``` 而这些往往是更好的 API*本身*: ```rust draw(Line { start: Point { x: x1, y: y1 }, end: Point { x: x2, y: y2 }, width, opacity }); connect(ConnectionOptions { host, port, timeout, retries, tls }); ``` 设计压力迫使我们发现了缺失的领域概念。 ## 家里的可选参数(https://corrode.dev/blog/named-arguments-at-home/#optional-arguments-at-home) 可选参数在某种程度上,是可能存在也可能不存在的参数。Rust 有一个类型可以表示这一点。 ```rust fn connect(url: &str, timeout: Option<Duration>) { // ... } connect("https://example.com", None); connect( "https://example.com", Some(Duration::from_secs(5)), ); ``` 这不如以下方式那样舒适: ```rust connect("https://example.com") connect("https://example.com", timeout: 5s) ``` 但它具有一个有用的特性:可选性出现在函数的类型中。`connect` 没有隐藏的第二个调用约定。只有一个函数: ```rust fn(&str, Option<Duration>) ``` 并且每个调用者都提供两个参数。显然,当你有六个可选参数时,这并非你想要的效果: ```rust request( url, None, None, Some(timeout), None, None, None, ); ``` 我个人过去曾犯过这种模式的问题。问题在于这些参数已经不再是参数列表,而变成了配置。因此: ```rust struct RequestOptions { timeout: Option<Duration>, proxy: Option<Proxy>, redirect: Option<Redirect>, // ... } request( url, RequestOptions { timeout: Some(Duration::from_secs(5)), proxy: None, redirect: None, }, ); ``` 我们处理的不再是可选参数,而是数据。这增加了一个好的特性:“为这次调用语法上提供的参数”和“我在别处计算出的选项”之间没有特殊区别。 ```rust let options = RequestOptions { timeout: config.request_timeout, proxy: detect_proxy(), redirect: None, }; request(url, options); ``` 它组合得很好,因为它只是一个值。 ## 家里的默认参数(https://corrode.dev/blog/named-arguments-at-home/#default-arguments-at-home) 现在有明显的反对意见:写所有这些 `None` 很糟糕。正确。那就别写。这就是为什么我们有 `Default` 和结构体更新语法: ```rust #[derive(Default)] struct RequestOptions { timeout: Option<Duration>, proxy: Option<Proxy>, follow_redirects: bool, } request( url, RequestOptions { timeout: Some(Duration::from_secs(5)), ..Default::default() }, ); ``` 这已经非常接近: ```rust request(url, timeout: 5s) ``` 有一个小波折: ```rust Options { ... ..Default::default() } ``` 这并非无关紧要。但看看我们*没有*需要添加什么:哪些参数可以省略的规则,位置参数和命名参数如何交互,或者是否可以在中间省略某些内容。没有声明参数默认值的特殊语法,没有关于默认表达式是在声明时还是调用时运行的问题,也没有 `fn` 类型中的特殊表示。`Default` 只是一个 trait,函数调用保持原样。默认值现在可以独立于函数使用: ```rust let defaults = RequestOptions::default(); ``` 这本身通常很有用。对于库 API,我通常喜欢稍微更明确一些: ```rust struct RequestOptions { timeout: Duration, follow_redirects: bool, } impl Default for RequestOptions { fn default() -> Self { Self { timeout: Duration::from_secs(30), follow_redirects: true, } } } ``` 然后: ```rust request( url, RequestOptions { timeout: Duration::from_secs(5), ..Default::default() }, ); ``` 我认为这很好地解决了大部分重要问题。 ## 构建器模式用于真正复杂的情况(https://corrode.dev/blog/named-arguments-at-home/#builder-pattern-for-the-really-complex-cases) 有时即使选项结构体也太繁琐,通常当构造需要验证或转换时。那么,是的,构建器就派上用场了: ```rust let request = Request::builder(url) .timeout(Duration::from_secs(5)) .follow_redirects(false) .build()?; ``` Steve 说得对,构建器不应是默认选择。它们可能变成自己微型的编程语言。但一个小型构建器有一个非常有用的特性:每个“参数”都是一个普通的方法调用。这意味着我们可以做这样的事情: ```rust let mut request = Request::builder(url); if let Some(timeout) = config.timeout { request = request.timeout(timeout); } let request = request.build()?; ``` 在 Rust 中,使用语言级别的关键字参数做到这一点通常需要构建一个 map、使用展开操作符或其他机制。而在 Rust 中,这是方法调用。我个人觉得这读起来非常愉快。 ## 家里的函数重载(https://corrode.dev/blog/named-arguments-at-home/#function-overloading-at-home) 在 Java 中,你可以写: ```java void connect(String url, int timeout) { ... } void connect(String url) { ... } ``` Rust 不允许你定义两个同名的函数: ```rust fn connect(url: &str) {} fn connect(url: &str, timeout: Duration) {} ``` 我对此非常满意。但当人们说想要重载时,他们指的有几种不同的情况,而 Rust 已经分别涵盖了其中的大多数。 ### “我想要一个便捷形式和一个可配置形式”(https://corrode.dev/blog/named-arguments-at-home/#i-want-one-convenience-form-and-one-configurable-form) 给它们不同的名字: ```rust fn connect(url: &str) { connect_with_timeout(url, DEFAULT_TIMEOUT) } fn connect_with_timeout(url: &str, timeout: Duration) { // ... } ``` 标准库经常这样做。例如,参见 `Vec::new()` (https://doc.rust-lang.org/std/vec/struct.Vec.html#method.new) 和 `Vec::with_capacity()` (https://doc.rust-lang.org/std/vec/struct.Vec.html#method.with_capacity)。这为库作者增加了一个额外的名称(通常是 `with_...`),并让每个用户都省去了在脑中进行重载解析的麻烦。 ### “我想要多种输入类型”(https://corrode.dev/blog/named-arguments-at-home/#i-want-several-input-types) 使用一个 trait。标准库通过 `Into`、`AsRef` 和 `Borrow` 等 trait 一直在这样做。例如: ```rust fn greet(name: impl AsRef<str>) { println!("Hello, {}", name.as_ref()); } greet("Ferris"); greet(String::from("Ferris")); ``` 这给了我们另一个类似重载行为的有用部分:一个 API 可以接受不同的输入类型。对于拥有所有权的转换: ```rust fn set_name(name: impl Into<String>) { let name = name.into(); // ... } ``` 这只是一个具有单一参数列表和 trait 分派的函数。与无限制的重载不同,被接受类型之间的关系是显式的:只要满足约束,它们就可以工作。 ### “不同类型需要不同行为”(https://corrode.dev/blog/named-arguments-at-home/#different-types-need-different-behavior) 这也是一个 trait: ```rust trait Render { fn render(self, out: &mut Output); } impl Render for &str { fn render(self, out: &mut Output) { // ... } } impl Render for Image { fn render(self, out: &mut Output) { // ... } } fn render(value: impl Render, out: &mut Output) { value.render(out); } ``` 这是多态;我们只是把它放在了 trait 系统中,而不是在名称解析中。 ## 家里的灵活参数类型(https://corrode.dev/blog/named-arguments-at-home/#flexible-argument-types-at-home) Steve 的 Ruby 示例具有同样可爱又可怕的特质: ```ruby redirect_to "http://www.rubyonrails.org" redirect_to @post redirect_to action: "show", id: 5 ``` 这些调用看起来像是在调用同一个概念操作,但它们意味着截然不同的事情。在 Rust 中,我们可以直接建模: ```rust enum Redirect { Url(Url), Post(Post), Action { action: String, id: u64 }, } fn redirect_to(target: Redirect) { // ... } ``` 然后: ```rust redirect_to(Redirect::Url(url)); redirect_to(Redirect::Post(post)); redirect_to(Redirect::Action { action: "show".into(), id: 5 }); ``` 这更啰嗦,但啰嗦得好。我可以问:“嘿编辑器,我可以重定向到什么?”编辑器会回复 `Redirect` 的各个变体。这比“阅读文档并发现这个哈希接受哪些键和值”更有帮助。如果我们真的在乎消除歧义,可以添加 `From` 转换: ```rust impl From<Url> for Redirect { fn from(url: Url) -> Self { Self::Url(url) } } impl From<Post> for Redirect { fn from(post: Post) -> Self { Self::Post(post) } } fn redirect_to(target: impl Into<Redirect>) { let target = target.into(); // ... } ``` 现在: ```rust redirect_to(url); redirect_to(post); ``` 对于结构上有趣的情况: ```rust redirect_to(Redirect::Action { action: "show".into(), id: 5 }); ``` 记住,这没有运行时开销,并且是完全类型安全的。对于一门编译型语言来说,这很不错。 ## 家里的选项哈希(https://corrode.dev/blog/named-arguments-at-home/#options-hashes-at-home) “选项哈希”基本上是一个动态类型的匿名结构体。所以用 Rust 实现极其简单的方法是:使用一个静态类型、命名的结构体。 Ruby: ```ruby redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } ``` Rust: ```rust redirect_to( post_url(&post), RedirectOptions { status: StatusCode::MOVED_PERMANENTLY, flash: Some(Flash { updated_post_id: Some(post.id), ..Default::default() }), ..Default::default() }, ); ``` 是的,Rust 版本更冗长,但它也能在我们拼错 `status` 时检测到错误。不可能在应该放状态码的地方传递一个字符串,并且可以轻松列出所有支持的选项。我认为 Rust 不应该优化为让语法尽可能紧凑。相反,如果一组选项足够常见,值得使用便捷语法,那它很可能也常见到值得为其定义一个类型。 ```rust fn redirect_to(target: Url, options: RedirectOptions) ``` 现在,选项有了名称,字段可以集中在一处进行文档化。 ## 家里的可变参数(https://corrode.dev/blog/named-arguments-at-home/#variadic-arguments-at-home) Rust 没有通用的可变参数 Rust 函数。但同样,它已经有几种方式来表达相同的概念。如果所有参数都是同一类型,可以使用切片: ```rust fn sum(values: &[i32]) -> i32 { values.iter().sum() } sum(&[1, 2, 3, 4]); ``` 或者接受一个迭代器: ```rust fn sum(values: impl IntoIterator<Item = i32>) -> i32 { values.into_iter().sum() } sum([1, 2, 3, 4]); sum(vec![1, 2, 3, 4]); ``` 这可以说比以下形式更具可组合性: ```rust sum(1, 2, 3, 4) ``` 因为调用者可以自然地传递一个现有的集合。(当然,全部类型安全,并且运行时零开销。) 如果参数是异构的,最后的手段是编写一个自定义宏。需要明确的是,我不会仅仅为了模拟可变参数函数而使用宏,但我确实喜欢宏在稳定 Rust 中的使用方式,以及感叹号如何从普通函数调用中脱颖而出。 ## 家里的关键字外观语法(https://corrode.dev/blog/named-arguments-at-home/#keyword-looking-syntax-at-home) 在所有这些示例中,有一个小小的便利特性,我认为值得更多认可:字段初始化简写。Rust 允许你将: ```rust let options = RequestOptions { timeout: timeout, proxy: proxy, retries: retries, }; ``` 变成: ```rust let options = RequestOptions { timeout, proxy, retries }; ``` 这直接解决了 Steve 对关键字参数的一个抱怨: ```rust response_model=response_model, status_code=status_code, tags=tags, dependencies=dependencies, ``` Rust 的回答实际上是: ```rust Options { response_model, status_code, tags, dependencies } ``` 在我看来,这比关键字参数更好。这是因为标签仍然存在,重复消失了,并且我们调用函数的方式无需改变。 ## 组合是一种超能力(https://corrode.dev/blog/named-arguments-at-home/#composition-is-a-superpower) 我最喜欢 Rust 的地方是每个概念如何与其他概念很好地交互。这并非易事,Rust 值得一个

相似文章

命名参数和可选参数非常优秀

Lobsters Hottest

本文批判了Rust对命名参数和可选参数的缺失,对比了Dart、C#、TypeScript与Python的实现方式,并展望了Rust未来的潜在解决方案。

参数之争

Lobsters Hottest

本文探讨了在编程语言中使用命名参数和灵活参数风格的权衡,比较了 Rust 的简洁性与 Ruby 的简练性,并对在 Rust 中采用类似功能表示怀疑。

Rust 中参数解析的新旧结合做法

Lobsters Hottest

Julio Merino 介绍了 Rust 中参数解析的一种新旧结合做法,将 getopts crate 扩展为一个小型框架,优先考虑 Unix 风格工具集成,而非语言生态系统惯例。

在C++中模拟函数的关键字参数

Hacker News Top

本文演示了一种通过使用带有指定初始化器的结构体来模拟Python风格关键字参数的技术,无需宏或模板魔法即可提高代码可读性。

用 Rust 重写

Hacker News Top

本文评估了2026年的‘Rewrite It In Rust’运动,讨论了现实世界中的性能提升、诸如新错误和平台支持等挑战,并提倡增量重写而非完全重写。