Named and Optional Arguments are Awesome
Named and Optional Arguments are Awesome
命名参数与可选参数真是太棒了
Whenever someone asks what my least favorite part of Rust is, my answer is always the same: it doesn’t have named or optional arguments. I have a small list of programming languages I tolerate: Rust, C#, TypeScript, Dart, Python. Of those languages, Rust is the hardest to get named arguments out of. I’m going to start this post by going over the other languages and how they’ve achieved named arguments. Then, we’ll look at Rust, the problems that have resulted from not having named arguments, and proposals for how Rust could get them in the future.
每当有人问我 Rust 最让我不满意的地方是什么时,我的回答总是如出一辙:它没有命名参数或可选参数。我能接受的编程语言列表很短:Rust、C#、TypeScript、Dart 和 Python。在这些语言中,Rust 是最难实现命名参数的。我将在这篇文章中先回顾其他语言是如何实现命名参数的,然后探讨 Rust 的情况、因缺乏命名参数而导致的问题,以及未来 Rust 可能引入该特性的提案。
How Other Languages Handle Named Parameters
其他语言如何处理命名参数
Dart
Dart
Dart is designed specifically for graphical user interfaces. GUI components tend to have lots of optional parameters, so Dart tried very hard to get them right. And I think it does the best job out of any language we’re going to talk about today. First, let’s look at a normal function, just to familiarize ourselves with the language:
Dart 是专门为图形用户界面(GUI)设计的。GUI 组件往往包含大量可选参数,因此 Dart 在这方面下了很大功夫。我认为在今天讨论的所有语言中,Dart 做得最好。首先,让我们看一个普通函数,以便熟悉一下这门语言:
Widget Text(String text) {
// ...
}
That’s pretty normal as far as languages go. You can probably guess what it does, even if you don’t know Dart. Dart was basically designed in a lab to be easy for programmers to learn. Now, let’s add a named parameter to set the size of the text, and default it to 16 pixels:
就编程语言而言,这很标准。即使你不了解 Dart,大概也能猜出它的作用。Dart 的设计初衷就是为了让程序员易于学习。现在,让我们添加一个命名参数来设置文本大小,并将其默认值设为 16 像素:
Widget Text(String text, {int fontSize = 16}) {
/* ... */
}
// usage: Text("Hello, world!", fontSize: 24)
Named parameters are wrapped in curly braces. This makes them look similar to a map. Named parameters can also be defaulted to null, or even required.
命名参数被包裹在花括号中,这让它们看起来很像一个映射(Map)。命名参数还可以设置默认值为 null,甚至可以设为必填项。
Widget Text({
int fontSize = 16,
Color? color,
required String text,
});
Dart also supports optional positional arguments by wrapping the parameter in square brackets.
Dart 还通过将参数包裹在方括号中来支持可选的位置参数。
// From the Dart documentation
String say(String from, String msg, [String device = 'carrier pigeon']) {
var result = '$from says $msg with a $device';
return result;
}
assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');
assert(say('Bob', 'Howdy', 'smoke signal') == 'Bob says Howdy with a smoke signal');
C#
C#
C#‘s handling of named parameters is also pretty good. In fact, any parameter can be named.
C# 处理命名参数的方式也非常出色。事实上,任何参数都可以被命名。
void ExampleMethod(string foo, string bar) {
Console.WriteLine($"{foo} {bar}")
}
ExampleMethod(bar: "Hello", foo: "world");
And any parameter can have a default value, as long as they’re specified after the required parameters.
任何参数都可以拥有默认值,只要它们定义在必填参数之后即可。
void ExampleMethod(string foo, string bar = "world") {
Console.WriteLine($"{foo} {bar}")
}
ExampleMethod("Howdy");
And that’s all there is to it. I like the simplicity of it.
这就是全部内容了。我喜欢这种简洁性。
TypeScript
TypeScript
TypeScript’s handling of named parameters is not very good, in my opinion. But optional positional parameters aren’t named, and they work pretty well, so let’s start with that.
在我看来,TypeScript 处理命名参数的方式并不理想。不过,可选位置参数虽然不是命名的,但它们工作得很好,所以我们先从这里说起。
function example(required: string, optional?: string = "world"): string {
return `${required} ${optional}`;
}
example("Hello") === "Hello world";
example("Hello", "TypeScript") === "Hello TypeScript";
That seems fine to me. If you don’t specify the default value, then it gets set to undefined. The problem is that there isn’t a good built-in syntax for named parameters, so we have to use other TypeScript features to hack it in.
这对我来说没问题。如果你不指定默认值,它会被设为 undefined。问题在于,TypeScript 没有内置良好的命名参数语法,所以我们必须利用其他 TypeScript 特性来“黑”出一个方案。
function example(
required: string,
{ requiredNamed, namedOptional = "world" }: { requiredNamed: string, namedOptional?: string }
): string {
return `${required} ${requiredNamed} ${namedOptional}`;
}
example("Hello", { requiredNamed: "beautiful" });
Here, we’re taking advantage of both anonymous types and destructuring. The second parameter of the function has an anonymous object type, which contains two fields: requiredNamed and namedOptional. Then we destructure this parameter so we can use the fields in the function. The most annoying part of this is that we have to define the field names twice. Once for the type, and once for the function parameters. This is unnecessary verbosity that any good language should try to avoid. The function call is also more verbose than necessary, because of the curly braces. Unfortunately, this syntax is used all of the time in React. Components are typically a function that takes one argument, which is an object. So when you define a component, you have to do this constantly.
在这里,我们利用了匿名类型和解构赋值。函数的第二个参数是一个匿名对象类型,包含两个字段:requiredNamed 和 namedOptional。然后我们对该参数进行解构,以便在函数中使用这些字段。最烦人的是,我们必须定义两次字段名:一次用于类型,一次用于函数参数。这是一种任何优秀语言都应避免的不必要的冗余。由于花括号的存在,函数调用也比预期的更繁琐。不幸的是,这种语法在 React 中被频繁使用。组件通常是一个接收单个对象参数的函数,因此在定义组件时,你必须不断地这样做。
Rust
Rust
Some people have tried to do named parameters in Rust. This generally doesn’t work very well. The most common approach is to define a struct for the function, and then have it implement the Default trait, so that we can end the struct initializer with ..Default::default().
有些人尝试在 Rust 中实现命名参数,但通常效果不佳。最常见的方法是为函数定义一个结构体,并让它实现 Default trait,这样我们就可以在结构体初始化器末尾加上 ..Default::default()。
struct ExampleParams {
foo: String,
bar: String,
}
impl Default for ExampleParams {
fn default() -> Self {
Self {
foo: "Hello".into(),
bar: "Hello".into(),
}
}
}
fn example(Example { foo, bar }) -> String {
format!("{foo} {bar}")
}
assert_eq!(
example(Example { foo: "Hello".into(), ..Default::default() }),
"Hello world".to_string()
);
I complained before about how verbose TypeScript’s version is, but this is even worse, because the types are not anonymous. And worse, if your default values are not the default of the types (e.g. empty string for String), then you need to write a whole function to set the default values. It also only works if every single parameter has a default value. If only some of the parameters have default values, then you need an even more verbose solution.
我之前抱怨过 TypeScript 的版本有多冗长,但这个方案更糟糕,因为类型不是匿名的。更糟糕的是,如果你的默认值不是类型的默认值(例如 String 的默认值是空字符串),那么你需要编写一个完整的函数来设置默认值。此外,它仅在每个参数都有默认值时才有效。如果只有部分参数有默认值,你需要一个更繁琐的解决方案。
Why Rust Needs Optional Parameters
为什么 Rust 需要可选参数
The standard library doesn’t tend to follow the above pattern (for good reason, in my opinion). But there are still several cases where the standard library probably would have benefitted from it. Take some of the factories of HashMap, for example.
标准库通常不遵循上述模式(我认为这是有充分理由的)。但仍有几种情况,标准库如果能支持该特性可能会受益。以 HashMap 的一些工厂方法为例:
impl<K, V> HashMap<K, V, RandomState> {
pub fn new() -> HashMap<K, V, RandomState>;
pub fn with_capacity(capacity: usize) -> HashMap<K, V, RandomState>;
}
impl<K, V, A: Allocator> HashMap<K, V, RandomState, A> {
pub fn new_in(alloc: A) -> Self;
pub fn with_capacity_in(capacity: usize, alloc: A) -> Self;
}
impl<K, V, S> HashMap<K, V, S> {
pub const fn with_hasher(hash_builder: S) -> HashMap<K, V, S>;
pub fn with_capacity_and_hasher(capacity: usize, hasher: S)