Rust 中闭包类型的泛型意味着什么?

Max*_*ber 4 generics rust

当类型参数受类型限制时,这意味着什么Fn

fn call<F: Fn() -> u64>(f: F) -> u64 {
    f()
}
Run Code Online (Sandbox Code Playgroud)

Rust Book 说带有类型参数的函数是单态的:https://doc.rust-lang.org/stable/book/ch10-01-syntax.html ?highlight=generic%20function#performance-of-code-using-仿制药

如果是这种情况,那么我希望call为实例化的每个新闭包类型生成一个新版本。

根据 Rust 参考,即使函数体相同,每个闭包类型也是不同的: https: //doc.rust-lang.org/reference/types/closure.html

call因此,当我查看该程序的编译输出时,我预计当我使用1799999999999999999不同的实例进行调用时,与仅四个实例相比,编译工件会更大F,但事实并非如此。

(1)

// `call` is defined above

fn make_closure(n: u64) -> impl Fn() -> u64 {
   move || n 
}

fn main() {
    let results = (0..17999999999999999999).map(make_closure).map(call);
    for r in results {
        println!("{}", r)
    }
}

Run Code Online (Sandbox Code Playgroud)

那么什么才是正确的心智模型fn call<F: Fn() -> u64> ?我是否应该将代码膨胀的缺乏视为仅仅是一种优化?

然而,当闭包是根据用户输入构建时,我很难维持这种心理模型:

(2)

fn main() {
    let mut buf = String::new();
    std::io::stdin().read_line(&mut buf).unwrap();
    let n = buf.trim().parse::<u64>().unwrap();
    println!("{}", call(make_closure(n)));
}
Run Code Online (Sandbox Code Playgroud)

那么思考手段签名的正确方法是什么call

更新

添加更多信息,以便我们可以从评论中引用它:

(3)

Rust参考资料说

闭包表达式生成一个具有无法写出的唯一匿名类型的闭包值。闭包类型大致相当于包含捕获变量的结构。

(4) 下面第一行被 rustc 接受,第二行不被接受:

    vec![make_closure(1), make_closure(2)]; // OK
    vec![(|| 1), (|| 1)];                   // Error: mismatched types
Run Code Online (Sandbox Code Playgroud)

rod*_*igo 8

我认为你在这里混淆了概念。事实上,Rust 中的每个函数都有自己的类型,每个闭包也是如此,即使它们具有相同的主体。所以在这个例子中:

fn id0(x: u64) -> u64 { x }
fn id1(x: u64) -> u64 { x }
fn main() {
    let id2 = || 1;
    let id3 = || 1;
}
Run Code Online (Sandbox Code Playgroud)

每个id0id1id2id3都有不同的类型,它们都实现了该Fn() -> u64特征。如果您编写call(id0)call(id1)您将获得该函数的单态版本,该函数静态且直接地调用id0id1,而不进行任何间接函数指针调用。

由于它们有一个空的捕获集,因此它们也可以被强制转换为非泛型函数类型:fn() -> u64。但如果你强制这样做,那么你就会失去这种单态性:call(id0 as fn() -> u64)并且call(id1 as fn() -> u64)两者都调用该call()函数的相同实例,该实例使用给定的指针间接调用内部闭包。

然后,在你的例子中:

fn make_closure(n: u64) -> impl Fn() -> u64 {
   move || n 
}
Run Code Online (Sandbox Code Playgroud)

只有一个关闭。您可以使用任何数字调用此函数,并且该数字由闭包捕获,但这不会改变返回值的类型。该类型是在编译时确定的,始终相同。

如果您使用常量泛型参数,情况会有所不同:

fn make_closure_gen<const N: u64>() -> impl Fn() -> u64 {
   || N
}
Run Code Online (Sandbox Code Playgroud)

现在,对于每个N函数,您都有该通用函数的实例化,并且每个函数都有不同的返回类型。但是您不能创建实例化 1000000 个这些函数的运行时循环,因为N必须是常量。(不过,您可以尝试编译时循环)。

无论如何,即使您设法创建同一函数的数百万个实例,如果编译器检测到多个函数的内部代码相同,它也可以将它们合并为一个函数(有时称为代码重复数据删除),以便最终的结果可执行文件不会爆炸。详细信息因编译器版本而异,甚至可能取决于所使用的链接器。它甚至可以应用于非泛型函数!

  • @apilat:是的,我也这么认为,但想一想:如果它们有不同的类型,并且每个都通过调用自身来实现特征“Fn() -&gt; u64”,那么它们实际上是零大小(ZST)并且它们的调用可以单态化。双赢。关于参考,可以看一下[参考](https://doc.rust-lang.org/reference/types/function-item.html)或者这个[演示代码](https://play.rust- lang.org/?version=stable&amp;mode=debug&amp;edition=2021&amp;gist=1320cbd7be1c8a45b21b76ea736c7b71)。 (2认同)

Apl*_*123 6

就像普通类型一样,闭包类型也可以包含多个值。move || n具有相同的类型,无论 的值是什么n(闭包类型由闭包主体决定,而不是捕获的值是什么)。因此,17999999999999999999尽管 的捕获值不同,但您的闭包都是相同类型的n,因此只需要一个 的实例call。如果您实际上有 2 个不同的闭包,如以下代码所示:

fn make_closure(n: u64) -> impl Fn() -> u64 {
   move || n 
}

fn make_closure2(n: u64) -> impl Fn() -> u64 {
   move || n + 1
}

fn call<F: Fn() -> u64>(f: F) -> u64 {
    f()
}

pub fn main() {
    call(make_closure(5));
    call(make_closure2(5));
}
Run Code Online (Sandbox Code Playgroud)

example::call在反编译中可以看到两个不同版本的。