Rust Quiz 学习之第一题

文档链接

https://dtolnay.github.io/rust-quiz/1

实操

问题:

What is the output of this Rust program?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
macro_rules! m {
    ($($s:stmt)*) => {
        $(
            { stringify!($s); 1 }
        )<<*
    };
}

fn main() {
    print!(
        "{}{}{}",
        m! { return || true },
        m! { (return) || true },
        m! { {return} || true },
    );
}

The program exhibits undefined behavior

The program does not compile

The program is guaranteed to output:

回答

这个问题围绕着 Rust 语法在哪里放置语句边界。

宏的输入规则m!$($s:stmt)*匹配零个或多个 Rust 语句。规则的$()*部分是一个重复,它匹配重复零次或多次的内容,并且 是$s:stmt一个片段说明符,匹配stmt符合 Rust 语法规则的 Rust 语句 ( )。匹配的语句在扩展代码中可以作为片段变量使用$s

语句是函数体内允许的顶级语法单元。以下所有内容均为陈述示例。函数体的语法要求某些类型的语句后跟分号,但出于宏语法的目的,分号不是语句的一部分。

1
2
3
4
5
6
7
8
// Items are statements.
struct S { x: u64 }

// Let-bindings are statements.
let mut s = S { x: 1 }

// Expressions are statements.
s.x + 1

该宏m!扩展为零个或多个{ stringify!($s); 1 } 由标记分隔的副本<<。规则的$(…部分是用作分隔符的)<<*重复。<<

<<在宏的重复中用作分隔符是非常不寻常的。最常用的分隔符是逗号,写为$(),*,但此处允许使用任何其他单个标记。至关重要的是,macro_rules!将所有内置 Rust 运算符视为单个标记,即使是那些由多个字符组成的标记,如 <<.

{ stringify!($s); 1 }一个值始终为 1 的表达式。 的值stringify!($s)被丢弃,因此这相当于表达式{ 1 }。在那里的原因stringify!($s)是控制重复的重复次数,这取决于重复中使用的片段变量。编写一个重复而不在其中使用任何片段变量是不合法的。

假设我们使用上面显示的三个语句作为输入来调用该宏。

1
2
3
4
5
m! {
    struct S { x: u64 }
    let mut s = S { x: 1 }
    s.x + 1
}

该宏扩展为:

1
2
3
{ stringify!(struct S { x: u64 }); 1 }
    << { stringify!(let mut s = S { x: 1 }); 1 }
    << { stringify!(s.x + 1); 1 }

每个stringifys 都扩展为字符串文字:

1
2
3
{ "struct S { x: u64 }"; 1 }
    << { "let mut s = S { x: 1 }"; 1 }
    << { "s.x + 1"; 1 }

不使用字符串文字的值。在这种情况下,表达式相当于{ 1 } << { 1 } << { 1 }, 相当于1 << 1 << 1。该<<运算符是左结合的;该表达式的数值是 4。

总而言之,该宏的相关行为是,它计算出1 << 1 << 1 << ...1 的数量等于宏输入中 Rust 语句的数量。在封闭形式中,数值是1 << (n - 1) 其中n是语句的数量,除了零的情况,n即宏扩展为空并且我们在调用站点得到语法错误。

m!仍然需要确定测验代码中的三个调用中有多少个语句 。

  1. return || true

    这是一个返回闭包的返回表达式|| true。它相当于return (|| true). 它被解析为单个语句,因此 m!调用的计算结果为1

  2. (return) || true

    这是一个逻辑或表达式。是||一个二元运算符,其中左侧是表达式(return)(发散类型!),右侧是表达式true。该表达式是单个语句,因此m!再次计算为1

  3. {return} || true

    这是两种说法!块语句{return}后跟闭包表达式|| true

    Rust 语法区分需要分号才能独立作为语句的表达式和即使没有分号也可以作为语句的表达式。考虑两个例子:

    1
    2
    3
    4
    5
    6
    7
    
    // No trailing semicolon required.
    for t in vec {
        /* ... */
    }
    
    // Trailing semicolon required.
    self.skip_whitespace()?;
    

    不带分号的独立表达式类型列表 libsyntax 中定义。这种区别告知了一些不同的早期救援情况,其中解析器决定完成对当前表达式的解析。

    与我们的情况相关的是,{ /* ... */ }如果这样做在语法上是合理的,则块表达式会终止表达式。解析器不会急切地使用块表达式之后的二元运算符。因此,人们可能会这样写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    fn f() -> &'static &'static bool {
        // Block expression.
        {
            println!("What a silly function.");
        }
    
        // Reference to reference to true.
        &&true
    }
    

    为了解析后跟二元运算符的块,我们需要使解析器在语法上无法感知在右大括号处终止表达式。这通常通过括在括号中来完成。

    1
    2
    3
    
    fn f() -> bool {
        ({ true } && true)
    }
    

无论如何,程序的输出是112