将 println 设为宏有什么好处?

Par*_* 23 5 println rust

在这段代码中,有一个!println

fn main() {
    println!("Hello, world!");
}
Run Code Online (Sandbox Code Playgroud)

在我见过的大多数语言中,打印操作是一个函数。为什么它是 Rust 中的宏?

She*_*ter 9

通过成为程序宏,println!()可以获得以下能力:

  1. 自动引用其参数。例如这是有效的:

    let x = "x".to_string();
    println!("{}", x);
    println!("{}", x); // Works even though you might expect `x` to have been moved on the previous line.
    
    Run Code Online (Sandbox Code Playgroud)
  2. 接受任意数量的参数。

  3. 在编译时验证格式字符串占位符和参数是否匹配。这是 C 的错误的常见来源printf()

对于普通的函数或方法,这些都不可能。

也可以看看:


Eth*_*Cue 8

好吧,让我们假设我们暂时创建了这些函数。

fn println<T: Debug>(format: &str, args: &[T]) {}  
Run Code Online (Sandbox Code Playgroud)

我们将接受一些格式字符串和参数来传递格式给它。所以如果我们这样做

println("hello {:?} is your value", &[3]);
Run Code Online (Sandbox Code Playgroud)

println 的代码将搜索 并将 替换{:?}为 的调试表示3

这是将这些作为函数执行的缺点之一 - 字符串替换需要在运行时完成。如果你有一个宏,你可以想象它本质上与

print("hello ");
print("3");
println(" is your value);
Run Code Online (Sandbox Code Playgroud)

但是当它是一个函数时,需要运行时扫描和字符串分割。

一般来说,Rust 喜欢避免不必要的性能影响,所以这是一个令人遗憾的事情。

接下来是T函数版本。

print("hello ");
print("3");
println(" is your value);
Run Code Online (Sandbox Code Playgroud)

我编写的这个签名表示它需要实现调试的部分内容。但这也意味着它期望切片中的所有元素都是相同的类型,所以这

fn println<T: Debug>(format: &str, args: &[T]) {}  
Run Code Online (Sandbox Code Playgroud)

不起作用,因为u32&'static str不同T,因此堆栈上的大小可能不同。为了让它工作,你需要做一些事情,比如装箱每个元素并进行动态调度。

println("Hello {:?}, {:?}", &[99, "red balloons"]);
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您可以将每个元素设置为Box<dyn Debug>,但现在您会遇到更多不必要的性能影响,并且用法开始看起来有点粗糙。

然后要求他们希望支持打印调试和显示实现。

println("Hello {:?}, {:?}", &[Box::new(99), Box::new("red balloons")]);
Run Code Online (Sandbox Code Playgroud)

目前还没有办法将其表达为普通的 Rust 函数。

我确信还有更多的激励原因,但这只是一个样本。

对于(有趣?)让我们将其与类似情况下 Java 中发生的情况进行比较。

在 Java 中,一切都是或者可以是堆分配的。所有内容都“继承”toString了 的方法Object,这意味着您可以使用动态调度获取程序中任何内容的字符串表示形式。

因此,当您使用 时String.format,您会得到类似于上面 println 的内容。

println!("{}, {:?}", 10, 15);
Run Code Online (Sandbox Code Playgroud)

Object...只是在运行时接受数组作为第二个参数的特殊语法,Java 编译器将允许您在不使用{}s 显式使用数组的情况下编写该语法。

最大的区别在于,与 rust 不同,不同的类型有不同的大小,Java 中的东西总是*在指针后面。因此,您不需要提前知道T如何创建字节码/机器代码来执行此操作。

    public static String format(String format, Object... args) {
        return new Formatter().format(format, args).toString();
    }
Run Code Online (Sandbox Code Playgroud)

这在机械上与此基本相同(忽略 JIT)

String.format("Hello %s, %s", 99, "red baloons");
Run Code Online (Sandbox Code Playgroud)

因此,Rust 的问题是,如何提供至少与 Java 版本一样好或更好的人体工学(这是许多人所习惯的),而又不会产生不必要的堆分配或动态分派。宏提供了该解决方案的机制。

(Java 还可以解决诸如调试/显示问题之类的问题,因为您可以在运行时检查已实现的接口,但这不是这里推理的核心)

另外,使用宏而不是采用字符串和数组的函数意味着您可以为不匹配或丢失的参数提供编译时错误,并且它是一个非常可靠的设计选择。