5 个版本

使用旧的 Rust 2015

0.1.1474613452 2016年9月23日
0.1.5 2016年9月23日
0.1.3 2016年9月18日
0.1.2 2016年9月16日
0.1.1 2016年9月16日

过程宏 中排名第 395

MIT 许可证

2KB

crates.io badge

Rust的命名参数

一个 实验性(可能已损坏)的过程宏。而不是这样的代码:

parser.parse_item(vec![], true, false)

这个宏允许你这样写代码!

parser.parse_item(attrs: vec![], macros_allowed: true, attributes_allowed: false)

部分作为未来Rust功能的可行性证明,但也为了实际可用性。理论上。

语法

受Swift强烈启发。

与Swift类似,参数名称是函数签名的 强制性 部分,且不可重新排序;此外,一个参数可以有分开的'外部'和'内部'名称,其中前者用于调用函数时使用,后者是结果的变量名称。

实际上,声明语法与Swift非常相似,但有两个形式交换了位置

变体 Rust Swift 调用时 变量名
未命名 fn foo(bar:String) funcfoo(_bar: String) foo(string) bar
基本命名 fn foo(_ bar:String) funcfoo(bar: String) foo(bar:string) bar
不同的名称 fn foo(barbaz:String) funcfoo(bar baz: String) foo(bar:string) baz

Swift 默认使用命名参数,并使用 _ 作为未命名参数,而扩展Rust语法默认为 未命名(与现有Rust代码兼容),并使用 _ 来指示 命名 参数,具有等效的外部和内部名称。对于不同的外部和内部名称,请用空格分隔它们。

命名参数通过 在定义和调用位置处将参数名称附加到函数名称上 来实现。作为自然的结果

  • 你可以 重载 名称相同但参数标签不同的函数。这不像C++/Java等中纯基于类型的重载那样有缺陷,因此没有理由不使用它。例如,考虑构造函数。对于零个和一参数构造函数,Rust现有的 ::new()::with_foo(foo) 的工作方式相当不错

    HashMap::new()
    HashMap::with_capacity(100)
    

    但是,对于两个或更多参数,它开始变得难看

    HashMap::with_capacity_and_hasher(100, Default::default())
    

    在设计带有命名参数的API时,这种进步可以更加自然

    HashMap::new()
    HashMap::new(capacity: 100)
    HashMap::new(capacity: 100, hasher: Default::default())
    

    (注意:不同的未命名参数数量也可以区分,但只有在第一个命名参数之后。)

  • 命名参数语法既适用于裸函数也适用于特例方法(在特例定义和实现中)。但在lambda函数或函数类型(无论是裸函数fn(...)还是Fn/FnMut/FnOnce(...)特例)中都不适用;类型会丢弃参数名称。

    要使用命名参数而不调用函数,请使用此语法(灵感来源于Objective-C)

    调用时 命名为
    foo() / foo(123) foo
    foo(bar: 123) foo{bar:}
    foo(123,baz: 456, 789) foo{:baz::}

    然后可以不带标签调用它或将其强制转换为适当的fn类型。

  • ...错误信息可能会有些令人困惑。我是字面意义上的“附加到函数名称”。就像任何其他的Rust过程宏一样,这个宏执行一个纯粹的语法转换,然后将一些输出代码交给编译器,之后几乎没有控制编译的能力。以下输入

    fn foo(_ bar: u32) {}
    foo(bar: 5);
    

    被转换成

    // In "nightly" mode:
    fn foo{bar:}(bar: u32) {}
    // ^^^^^^^^^ this whole thing is stuffed into an Ident token so it's treated as
    //           a name, even though it's not a valid ident in vanilla Rust)
    // ditto for the call:
    foo{bar:}(bar: 5);
    
    // In "macros 1.1" mode, we have to use valid code:
    fn foo_namedarg_bar(bar: u32) {}
    foo_namedarg_bar(5);
    

    nightly版本生成的错误信息看起来好一些,但无论如何,如果你不小心调用了foo(5)(没有参数名称),错误信息将只是说unresolved name `foo`。它不知道你只是搞错了标签。(所以别忘了!)

    注意:如果我能用lint改进这一点就好了,但早期lint太早了,而如果编译失败,则不会运行晚期lint。

模式

在Rust中,与Swift不同,参数实际上并没有'名称';它们有'模式',就像letmatch一样。所以这是有效的标准Rust

fn foo(Config { a, b }: Config) {}
fn foo(TupleStruct(a, b): TupleStruct) {}
fn foo((a, b): (i32, i32)) {}

使用这个宏,模式也可以与命名参数一起使用...有时

fn foo(config Config { a, b }: Config) {} // OK
fn foo(ts TupleStruct(a, b): TupleStruct) {} // OK
fn foo(pair (a, b): (i32, i32)) {} // No good...

不幸的是,最后一个创建了一个语法歧义:它打算是一个名为pair的参数,它接受一个元组,但它已经是有效的Rust语法,用于接受类似元组的结构体pair的未命名参数

struct pair(i32, i32); // Lowercasing a struct name is bad style, but possible!
fn foo(pair(a, b): pair) {} // Only the space distinguishes this from the other example.

(嗯,还有类型,但使用类型区分不会起作用,因为类型别名。)

目前,唯一的解决方案是将解构移动到函数体中

fn foo(pair pair: (i32, i32)) { let (a, b) = pair; ... }

这足够好,但不够优雅。这个插件的未来版本可能会更改语法以消除歧义。(任何官方的Rust命名参数特性可能也应该这样做...)

与类型注解的交互

从理论上讲,命名参数调用语法可能与类型注解(不稳定)冲突。表达式foo(a: b)包含一个名为a的命名参数,其值为b,还是一个未命名的参数,其值是变量a被强制转换为类型b

在这个插件中,只有当函数参数的开始是一个标识符后跟冒号时,才会触发命名参数语法。因此,当其表达式不是普通变量时,类型注解不受影响:例如 foo(5: usize)foo(x.collect(): Vec<bool> 都可以正常工作。如果表达式是一个变量,你可以添加括号:foo((a: usize))

但是,如果你思考一下,为什么你最初要给变量添加类型注解呢?在几乎所有情况下,给变量定义添加类型注解都比使用类型注解更好。例外情况可能包括定义在复杂模式或匹配语句中的变量,这些变量无法添加类型,但即使在这种情况下,通常也有更好的编写方式。

用法

将其添加到你的 Cargo.toml 中

[dependencies]
namedarg = "0.1"
namedarg_rustc_macro = "0.1"

并用宏调用包装你的源文件

#![feature(rustc_macro, custom_derive)]
#[macro_use] extern crate namedarg;
namedarg! {

// your code here

}

是的,它有点丑。一旦宏 1.1 稳定下来,第一行可以省略。

"Macros 1.1" 模式在检测到 非夜间版 rustc 时自动启用,尽管在撰写本文时这毫无用处,因为它实际上还不稳定。它有一个 轻微的缺点,即会破坏 namedarg! 内部的所有行号,这是宏 1.1 设计中的固有缺陷。例如,所有错误都会指向包含 namedarg! 的行。这对开发来说不太实用,但(在稳定后)应该可以在夜间版上开发一个crate,并以此方式编译稳定版。

你可以使用 Cargo 功能强制启用宏 1.1 模式

[dependencies.namedarg]
version = "0.1"
features = ["force_macros11_mode"]
[dependencies.namedarg_rustc_macro]
version = "0.1"
features = ["force_macros11_mode"]

默认参数

还有一种 非常简单 且更具实验性的语法用于 默认参数,即当调用时可以省略的参数。我,comex,喜欢 Swift 的命名参数语法,但我不满意我所知的任何语言处理默认参数的方式。问题是当编写将参数转发到其他函数(通常是称为“便利函数”的小包装器)的函数时。例如,假设我们有以下 Swift 代码

struct Player {
    func doSomething(useFlubber: Bool = false) {}
}

func doSomethingToPlayer(username: String, useFlubber: Bool = false) {
    // Convenience function to lookup a Player by name and call doSomething
    Player(username: username)!.doSomething(useFlubber: useFlubber)
}

它工作,但我们必须写两次默认值,这是一个 DRY 违规。如果我们后来决定更改 doSomething 的默认值,我们可能想要将相同的更改应用到 doSomethingToPlayer 上 - 但如果我们忘记了,编译器就不会抱怨!在静态语言中,重构函数通常会在我们尚未修复的地方产生类型错误,但具有不同默认值永远不会是类型错误。

避免这种问题的一种方法可能是使用一个包装“配置”结构体,其构造函数负责处理默认值。(这也经常被建议作为命名参数的替代方案。)如果有很多参数,这是一个好解决方案 - 因为重复 参数名 本身就已经是一个 DRY 违规 - 但在只有一个或两个参数要转发的情况下,它感觉相当冗长。

相反,我在这个宏中实现的默认参数版本是基于Option的。这很简单:如果你在参数名前加上#[default],它将默认传递None。看起来是这样的

impl Player {
    fn do_something(&self, #[default] _ use_flubber: Option<bool>) {
        let use_flubber = use_flubber.unwrap_or(false);
    }
}
fn do_something_to_player(username: &str, #[default] _ use_flubber: Option<bool>) {
    Player::new(username: username).unwrap().do_something(use_flubber: use_flubber)
}
do_something_to_player("someone"); // implicitly passes None
do_something_to_player("someone", Some(true)); // kinda verbose but...

注意:未命名的参数也可以设置默认值。

当前实现的默认值有两个显著的限制。第一个在上面的代码片段中很明显:如果你想传递值而不是使用默认值,你必须明确写出Some,这更冗长。

第二个限制是,你不能跳过默认参数然后指定后面的关键字参数

fn foo(#[default] _ a: Option<bool>, #[default] _ b: Option<bool>) -> u32 { /* ... */ }
foo(b: Some(true)) // doesn't work!
foo(a: None, b: Some(true)) // you have to do this.

在解释这些限制的来源之前,我应该先说明这个转换是如何工作的。上面的定义会展开成一系列重载函数

fn foo() -> u32 { foo(a: None, b: None) }
fn foo(_ a: Option<bool>) -> u32 { foo(a: a, b: None) }
fn foo(_ a: Option<bool>, _ b: Option<bool>) -> u32 { /* ... */ }

会生成一系列占位函数,每个参数列表的前缀对应一个,然后转发到真正的函数。在函数重命名方案下,没有更好的方法(而且就这个话题来说,我认为没有特别好的函数重命名替代方案,尽管有一些涉及更复杂类型的理论可能性)。

因此,第二个限制:如果支持跳过默认参数,则必须为参数列表的每个子集生成一个占位符,而不是每个前缀,所以添加一个参数会使占位符的数量翻倍;这可能会很快变得无法控制。虽然我不建议一开始就有大量默认参数的函数(正如我之前提到的,最好使用结构体),但我不喜欢这种在这样函数上完全失败的实现。

第一个限制,需要写Some,更容易克服。首先,我们可以想象生成的重载函数不使用Option,例如

fn foo() -> u32 { foo(a: None, b: None) }
fn foo(_ a: bool) -> u32 { foo(a: Some(a), b: None) }
fn foo(_ a: bool, _ b: bool) -> u32 { foo_IMPL(a: Some(a), b: Some(b)) }
fn foo_IMPL(_ a: bool, _ b: bool) -> u32 { /* ... */ }

但然后我们如何在转发时传递Option呢?

fn x_foo(#[default] _ a: Option<bool>) {
  foo(a: a) // type error: expected bool, found Option<bool>
}

至于如何(明确地)跳过一个参数时传递None呢?

将不得不有一些特殊语法来表示“传递这个Option参数而不做任何改变”,尽管即使这样也很棘手(可能需要生成更多的占位符或更改命名参数的转换)。

另一种可能是依赖类型系统来区分Option和非Option参数

trait OptionOrJust<T> { ... }
impl<T> OptionOrJust<T> for T { ... }
impl<T> OptionOrJust<T> for Option<T> { ... }
fn foo<A: OptionOrJust<bool>>(_ a: A)

// both work!
foo(a: true);
foo(a: Some(true));

如果参数本身也是Option类型,这甚至可以正确工作...通常。但有时会变得很混乱

// original:
fn foo<T>(#[default] _ x: T) { ... }
// transformed by the plugin:
fn foo<T, X: OptionOrJust<T>>(_ x: X) { ... }
// but wait...
foo(x: Some(123)) // what is T??
foo::<u32>(x: Some(123)) // error: too few type parameters provided
foo::<u32, _>(x: Some(123)) // OK, but what is this phantom type parameter?

...而且在更好的情况下,这也可能使类型推断变得稍微差一些。

其他限制

  • macro_rules!namedarg!中不能正常工作,因为有一个rustc bug。通过将namedarg!放在宏展开中来解决这个问题,效果并不好——首先,由于实现基于自定义派生,namedarg!内的内容必须是一组完整的项(如fn、结构体声明等),而不是一个表达式。另外,由于namedarg!的展开包含extern crate,它只能指定一次,在crate根目录中。你可以通过使用namedarg_inner!来解决这个问题,它与namedarg!相同,但不包含extern crate:因此必须在该文件中有相同的namedarg!才能工作。但完整的项限制仍然存在。我应该添加某种逃逸机制来更好地解决rustc bug,允许任何宏都可以定义在namedarg!内。

  • 夜间构建实现是一个荒谬的 hacks。它曾经只是一个常规的夜间编译器插件,但这却带来了更差的用户体验:宏 macro_rules! 的展开无法导入编译器插件,因此用户代码必须包含决定是否使用夜间或 macros-1.1 实现的逻辑,这给本已复杂的需求增加了许多混乱。一个理智的人会向 rustc 提交一个 PR,以改善 macro_rules! 和插件之间的交互。相反,我注意到展开 可以 加载 macros-1.1 定制的 derives,并编写了一些非常不安全的代码,使一个不稳定的插件伪装成 macros-1.1 插件。

依赖