匹配表达式(模式匹配)如何不需要运行时类型信息来工作?

Lan*_*ard 0 compiler-construction pattern-matching rust

match该表达式是如何在高层次上实现的?编译器在幕后发生了什么才能知道如何将某些代码片段定向到一个分支与另一个分支,并在编译时弄清楚它?我不明白如果不存储运行时使用的类型信息,这怎么可能。

\n

像这个例子:

\n
fn tree_weight_v1(t: BinaryTree) -> i32 {\n    match t {\n        BinaryTree::Leaf(payload) => payload,\n        BinaryTree::Node(left, payload, right) => {\n            tree_weight_v1(*left) + payload + tree_weight_v1(*right)\n        }\n    }\n}\n\n/// Returns tree that Looks like:\n///\n///      +----(4)---+\n///      |          |\n///   +-(2)-+      [5]\n///   |     |   \n///  [1]   [3]\n///\nfn sample_tree() -> BinaryTree {\n    let l1 = Box::new(BinaryTree::Leaf(1));\n    let l3 = Box::new(BinaryTree::Leaf(3));\n    let n2 = Box::new(BinaryTree::Node(l1, 2, l3));\n    let l5 = Box::new(BinaryTree::Leaf(5));\n\n    BinaryTree::Node(n2, 4, l5)\n}\n\n#[test]\nfn tree_demo_1() {\n    let tree = sample_tree();\n    assert_eq!(tree_weight_v1(tree), (1 + 2 + 3) + 4 + 5);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

通过运行时类型信息,我的意思是您实际上存储了一个指向类型定义的指针或类似的东西,因此当运行时执行到达时match,它只是检查value.type == given_pattern_expression或它的某些变体。据我了解,事实并非如此。也就是说,Rust 不会在运行时将类型与结构/记录一起存储。根据我的理解,它是在编译时计算的。

\n

.type如果我在每个结构/记录上存储,我可能可以实现这种功能。然后您只需查找类型并查看是否匹配即可。但是,我看不到在编译器级别实现此功能的方法,因此它会在运行时之前确定匹配。

\n

我认为它发生在编译时,因为像Is it possible to do a compile time type check on a generic in Rust? 这样的帖子 ,或运行时特征实现检查?,在许多其他帖子中,这些帖子似乎表明一切都发生在编译时,除了极少数情况下您可以选择加入运行时检查。(这是我这几天总结几十篇文章的理解)。或者另一句话是:

\n
\n

人们确实需要检查给定的二叉树是叶子还是节点,但编译器静态地确保完成此类检查:您不能意外地将叶子的数据解释为节点,反之亦然。

\n
\n

对我来说,编译器静态地计算出我上面发布的案例中的模式匹配BinaryTree。不知何故。

\n

Integrating Pattern Matching with Type Checking是一篇关于编程语言的随机文章,它与我的理解一致,即您需要运行时类型信息来完成此操作。所以我很困惑。

\n
\n

如果类型是模式,则需要能够在运行时确定值的类型。一般来说,这需要运行时类型信息(许多语言有意删除);但它还需要一些包含这两种情况的超类型(许多语言故意没有)。

\n
\n

为 Rust \xef\xb8\x8f 构建运行时反射系统(第 1 部分)还解释了 Rust 为什么没有运行时反射,这有助于我认为一切都发生在编译时。

\n

https://oswalt.dev/2021/06/polymorphism-in-rust/

\n

She*_*ter 6

表达式match不需要运行时类型信息;由于 amatch只接受单个已知类型的单个表达式,根据定义它可以利用编译时信息。

\n

也可以看看:

\n\n

match编译时与运行时

\n

编译时,每个match表达式都将被验证为详尽的:处理值的所有可能形状。

\n

运行时,代码将确定执行哪个特定的匹配臂。您可以将 a 视为match通过花式链实现ifelse

\n

由于我们人类在沟通时往往不是非常精确,因此某些资源很可能模糊了这两个方面之间的界限。

\n

具体关注枚举

\n

枚举变体不是独立类型。也就是说,给定一个 enum FooFoo::Bar不是类型 \xe2\x80\x94 它是 type 的值Foofalse这与 不是类型 \xe2\x80\x94 它是类型 的值相同bool。相同的逻辑适用于i32(类型)和42(值)。

\n

在大多数情况下,枚举被实现为与每个枚举变体可能的值相对应的字节海洋,每个变体的数据彼此分层。这就是所谓的工会

\n

然后在这堆字节旁边添加一个判别式。这是一个整数值,指定该值是哪个变体。添加判别式使其成为标记联合

\n

枚举上的匹配在概念上类似于这个伪 Rust:

\n
if discriminant(&enum_value) == VARIANT_1_DISCR {\n    let data = mem::transmute(data(&enum_value));\n} else if discriminant(&enum_value) == VARIANT_2_DISCR {\n    let data = mem::transmute(data(&enum_value));\n}\n
Run Code Online (Sandbox Code Playgroud)\n

也可以看看:

\n\n

反射

\n
\n

Rust 为什么没有运行时反射

\n
\n

我不同意它没有它,但默认情况下它肯定没有它。Rust 中的运行时类型信息通常会利用以下Any特征:

\n
if discriminant(&enum_value) == VARIANT_1_DISCR {\n    let data = mem::transmute(data(&enum_value));\n} else if discriminant(&enum_value) == VARIANT_2_DISCR {\n    let data = mem::transmute(data(&enum_value));\n}\n
Run Code Online (Sandbox Code Playgroud)\n

匹配时缺少特征Any是不存在运行时类型信息的额外提示。

\n

其他语言

\n
\n

您需要能够在运行时确定值的类型

\n
\n

仅当您允许值具有多种类型时,这才是正确的 \xe2\x80\x94 Rust 不这样做,因为它是静态类型语言。在动态类型语言中,您想要使用类型来匹配值,您确实需要拥有一定数量的运行时类型信息。

\n

  • @Lance,解决方案以某种形状或形式标记为联合。确保枚举的大小足以容纳其任何变体,然后在前面添加一个额外的字节来区分它们。对于某些特殊的边缘情况,这可能会发生变化,例如“Option<*mut T>”如何知道空指针将是“None”变体,因此在内存中它可以将其替换为“*mut T”。 (2认同)

Loc*_*cke 5

Rust 使用的可以保存数据的枚举类型通常被称为“标记联合”。一般概念是它们或多或少通过应用区分标签以及可能值的联合来工作。

Rust 中标记的联合

例如,这里我使用匹配打印 Rust 中的枚举值:

pub enum Foo {
    Bar(i32),
    Baz {
        a: i32,
        b: i8,
    }
}

match foo {
    Foo::Bar(x) => println!("Bar({})", x),
    Foo::Baz { a, b } => println!("Baz {{ a: {}, b: {} }}", a, b),
}
Run Code Online (Sandbox Code Playgroud)

内存中标记的联合

下面是如何使用没有标记联合的语言(如 C)执行相同的操作:

struct Foo {
    enum {
        Bar, Baz
    } tag;

    union {
        // Data for Bar
        int32_t Bar;
        
        // Data for Baz
        struct {
            int32_t a;
            int8_t b;
        } Baz;
    } data;
}

// Check the tag to distinguish variants before accessing data
switch foo.tag {
    case Bar:
        printf("Bar(%d)", foo.data.Bar);
        break;
    case Baz:
        printf("Baz { a: %d, b: %d }", foo.data.Baz.a, foo.data.Baz.b);
}
Run Code Online (Sandbox Code Playgroud)

现在,我可能在我的 C 版本中犯了一个或两个错误,有人可能能够指出一些改进,但我希望它通常显示标记联合如何在获取数据时区分变体。

Rust 中标记的联合

我不太了解 Rust 中如何实现标记联合,但很容易想象 Rust 可能会在这些系统上扩展,以进一步提高匹配效率。例如,如果您有一个枚举类型嵌套在另一个枚举类型中,您可以将它们的标签合并在一起,这样您就不需要进行多次比较(例如:编译器可能会压缩Option<Result<A, B>>OptionResultAB只需要单个标签的类型)。

另一个众所周知的例子是如何Option<NonNull<T>>具有相同的大小,*mut T因为 null 可以用作值None。还有一些其他类似的情况,例如函数指针。

参考

虽然我只是猜测 Rust 中标记联合的形式,但我确实找到了一个更好的研究解释,它很好地解释了这个概念:

  • 很好的答案。如果你想加强最后一部分,Rust 的效率来自其所谓的“利基优化”系统。编译器识别出某个类型包含未使用的值或利基,可以使用这些值将标签压缩到现有类型中,而无需更多空间。通常这是一个实现细节,尽管出于稳定性或 FFI 原因保证了一些利基优化。保证使用“None”的空指针利基将“Option&lt;NonNull&lt;T&gt;&gt;”优化为单个指针。 (3认同)