未装箱的封盖类型各不相同

Types of unboxed closures being unique to each

这是 问题的上下文。答案指出 Rust 生成一个类型,每个闭包都是唯一的,因为它们中的每一个都可能从封闭范围捕获一组不同的变量。

这是我的问题。该 FizzBu​​zz 示例中的两种不同闭包类型标记不同,但看起来相同。编译器如何解决闭包类型差异,同时仍然查看类型参数的相同签名?

编译器所见与程序员所见之间的差距令人困惑。

谢谢。

编辑:顺便说一下,R​​ust 参考文档第 8.1.10 节还没有说明这一点。

编写的每个闭包都有唯一的类型。编译器基本上将它变成一个带有字段的结构来存储每个被关闭的变量,并使用适当的签名为它实现 Fn* 特征(Fn, FnMut and FnOnce)之一,例如Fn(i64) -> bool、a.k.a。 Fn<(i64,), bool>.

这就是它使用常规通用约束语法的原因,因为这正是正在发生的事情。

"signature" 一词暗示这两种闭包类型相同,但实际上并非如此。将它们视为约束更有用。供参考:

fn fizzbuzz<F1, F2>(n: i64, f: F1, fs: &str, b: F2, bs: &str)
    where F1: Fn(i64) -> bool,
          F2: Fn(i64) -> bool

这不是说“F1 是类型 Fn(i64) -> bool”,而是说:“F1 必须是实现 Fn(i64) -> bool 签名的类型”。这就像有许多不同的类型实现了 Iterator 特性,但它们都实现了相同的接口。

事实上,像 Fn(i64) -> bool 这样的东西实际上只是变相的特征,但是(如果我没记错的话),它的语法还没有完全准备好。在早期版本中,您可以将其写为 Fn<(i64,), bool>.

同样,我想从那个答案中的相同示例开始。比较一下:

fn show_both_1<S: Show>(x: S, y: S) {
    println!("{:?} {:?}", x, y);
}

还有这个:

fn show_both_2<S1: Show, S2: Show>(x: S1, y: S2) {
    println!("{:?} {:?}", x, y);
}

(由于最近的更改,现在使用 {:?} 而不是 {}

第一个函数要求两个参数必须具有相同的类型,即使这种类型可以是任意的,只要它实现了Show:

show_both_1::<i32>(1i32, 2i32);      // ok
show_both_1::<f64>(1.0f64, 2.0f64);  // ok
show_both_1::<???>(1i32, 2.0f64);    // not ok!

显然最后一个函数调用没有意义,因为参数的类型不同,但函数希望它们具有相同的类型。你甚至不能显式地写类型参数——应该是 i32 还是 f64?

第二个函数允许不同的类型,所以这些调用都可以:

show_both_2::<i32, i32>(1, 2);
show_both_2::<f64, f64>(1.0, 2.0);
show_both_2::<i32, f64>(1, 2.0);

现在每个参数都使用不同类型的参数,因此传递不同类型的值是完全没问题的,只要这两种类型都实现 Show.

完全相同的事情发生在闭包上。对于每个闭包,编译器都会生成 一个新的唯一类型 ,它实现了 Fn* 特征之一。这些类型是匿名的,所以你不能给它们命名:

let f: ??? = |&: x: i32, y: i32| x + y;

除了上面的 ???,你没有什么可以写的,但是没有必要,因为编译器知道它为闭包生成了哪种类型,因此它可以推断出 f 的类型。 真正重要的是这个匿名类型将始终实现其中一个特殊特征:Fn, FnMut or FnOnce。因此,如果您希望您的函数接受一个闭包,您需要向它传递一个实现这些特征之一的某种类型的实例。

但这是仿制药的自然工作!当您希望您的函数接受实现某些已知特征的任意类型时,通常会使用它们,并且闭包的情况完全相同。所以你有这个:

fn call_closure<F: FnMut(i64) -> bool>(f: F) -> bool {
    f(10)
}

因为此函数参数具有泛型类型,此函数可用于任何实现 FnMut(i64) -> bool 特征的类型(这只是 FnMut<(i64,), bool> 的 shorthand,包括匿名类型编译器生成的闭包:

call_closure(|x| x > 10);
call_closure(|x| x == 42);

编译器将为这些闭包中的每一个生成一个唯一的类型,但是由于这些生成的类型将实现 FnMut(i64) -> bool 特征,call_closure 将很乐意接受它们。

我一开始描述的不同类型参数的情况自然会延伸到闭包,因为这里使用了相同的机制,即特征。

fn call_closures_2<F: FnMut(i64) -> bool>(f1: F, f2: F) -> bool {
    f1(10) && f2(20)
}

此函数接受两个必须相同类型的参数,只要此类型实现FnMut(i64) -> bool特征。这意味着此调用将不起作用:

call_closures_2(|x| x > 9, |x| x == 20)

不行,因为这些闭包具有唯一性,即不同的类型,但是函数要求类型必须相同。例如,这确实有效:

fn call_closures_3<F: Fn(i64) -> bool>(f1: &F, f2: &F) -> bool {
    f1(10) && f2(20)
}

let f = |&: x: i64| x == 10;
call_closures_3(&f, &f);

请注意,函数参数必须仍然是同一类型(为了示例的方便,现在引用),但是由于我们使用对同一闭包的引用来调用它,因此它们的类型 一样,一切正常。这不是很有用,因为它有很大的局限性——通常你想为需要多个闭包的函数提供不同的闭包。

因此,该函数需要单独的 类型参数以接受不同的闭包:

fn call_closures_4<F1, F2>(f1: F1, f2: F2) -> bool
        where F1: FnMut(i64) -> bool,
              F2: FnMut(i64) -> bool {
    f1(10) && f2(20)
}

call_closures_4(|x| x >= 9, |x| x <= 42)

现在类型参数是独立的,即使闭包有不同的匿名类型,也可以用它们调用这个函数:F1 将成为第一个闭包的生成类型,F2将成为第二个闭包的生成类型。