为了解释为什么
命名参数语法受限,我首先要指出在Rust 1.58允许访问变量之前,
"{ident}"
语法已经存在。它最初的设计是这样工作的:
println!("{name} bought {amount:.2} kg of {nouns}.",
name = "Richard",
amount = 5.0,
nouns = "apples",
);
所以语法并不新奇,只是为了寻找作为备选项的其他在范围内的标识符而放松了规定,如果未向宏传递命名参数。
实际上,他们不能使格式宏接受任意表达式。当然他们可以做任何他们想做的事情,但这将违反其他地方设定的前向兼容性限制。
假设我尝试创建一个类似于命名参数语法但允许任何表达式的宏,同时仍允许其他格式化参数。以下是一个微不足道的示例,但编译器会拒绝它:
macro_rules! my_format {
($e:expr : $width:literal) => {
};
}
fn main() {
my_format!(expr:2);
}
error: `$e:expr` is followed by `:`, which is not allowed for `expr` fragments
--> src/main.rs:2:14
|
2 | ($e:expr : $width:literal) => {
| ^ not allowed after `expr` fragments
|
= note: allowed there are: `=>`, `,` or `;`
为什么会这样呢?如果我们看一下Follow-set Ambiguity Restrictions:
宏系统使用的解析器相当强大,但为了防止当前或未来版本的语言中出现歧义而受到限制。特别是,除了关于模糊扩展的规则之外,由元变量匹配的非终端符号必须后跟一个已确定可以安全使用的标记,以便在这种匹配之后使用。
例如,像
$i:expr [ , ]
这样的宏匹配器在 Rust 中理论上可以被接受,因为
[,]
不能成为合法表达式的一部分,因此解析总是不含歧义的。然而,由于
[
可以开始尾随表达式,
[
不是一个可以安全地排除在表达式后面的字符。如果在 Rust 的后续版本中接受了
[,]
,则此匹配器将变得模糊或解析错误,从而破坏工作代码。像
$i:expr,
或
$i:expr;
这样的匹配器是合法的,因为
,
和
;
是合法的表达式分隔符。具体规则如下:
expr
和stmt
只能后跟以下之一:=>
、,
或;
。
因此,开发人员并没有排除:
在未来可能成为某种表达式尾缀或连接符,并希望保留该语法以备不时之需。即使您尝试使用syn在过程宏中解析它以绕过此限制,它实际上将尝试解析为ExprType
(例如foo: f64
),这是类型注释RFC的一部分。虽然该RFC已经大部分实现,但将被删除,但我提到它作为语法演变的例子。如果他们允许格式化参数是任意表达式,他们必须稳定:
是有效的表达式分隔符。
除非我错过了其他语法,否则:
可能会被稳定为分隔符,也许这是一个激励案例。但只有未来才能告诉我们。
另一个需要解决的障碍是块({ ... }
)也是表达式,但格式化宏中已经存在{{
和}}
的语法;它们用于转义。因此,如果允许任何表达式,则"{{expr}}"
将是模棱两可的。
println!
宏所能做的事情是有限的。随着时间的推移,它可能会变得更加强大,但在{}
内解析任意 Rust 语法是相当具有挑战性的,因此这可能还有一段路要走。 - tadman