println
后面有一个!
:fn main() {
println!("Hello, world!");
}
在大多数编程语言中,打印操作是一个函数。为什么在Rust中它是一个宏?
作为一个过程宏,println!()
获得以下能力:
自动引用其参数。例如,以下代码是有效的:
let x = "x".to_string();
println!("{}", x);
println!("{}", x); // Works even though you might expect `x` to have been moved on the previous line.
接受任意数量的参数。
在编译时验证格式化字符串占位符和参数是否匹配。这是C语言中printf()
常见的错误源头。
普通函数或方法都无法实现上述目标。
另请参阅:
好的,让我们暂时假设我们已经创建了那些函数。
fn println<T: Debug>(format: &str, args: &[T]) {}
我们会使用一些格式字符串和参数传递给它来传递格式。所以如果我们这样做:
println("hello {:?} is your value", &[3]);
println的代码将寻找并替换{:?}
为调试表示法中的3
。
这就是将这些操作作为函数执行的缺点之一-字符串替换需要在运行时完成。如果使用宏,可以想象它基本上与以下相同:
print("hello ");
print("3");
println(" is your value);
但当它是一个函数时,需要在运行时扫描和拆分字符串。
一般情况下,Rust 喜欢避免不必要的性能损失,所以这很糟糕。
接下来是函数版本中的 T
。
fn println<T: Debug>(format: &str, args: &[T]) {}
println("Hello {:?}, {:?}", &[99, "red balloons"]);
由于u32
和&'static str
不是相同的T
,因此它们在堆栈上的大小可能是不同的,所以这样做是无法工作的。要使它起作用,您需要对每个元素进行封箱并执行动态分派。
println("Hello {:?}, {:?}", &[Box::new(99), Box::new("red balloons")]);
这样你可以让每个元素都是Box<dyn Debug>
,但现在你有了更多不必要的性能损失,使用方式看起来有点复杂。
然后还需要支持打印Debug和Display实现两种。
println!("{}, {:?}", 10, 15);
目前还没有一种方法可以将这个表达式表示为普通的 Rust 函数。
我相信还有更多激励人心的原因,但这只是一个示例。
为了(好玩?)让我们比较一下在类似情况下 Java 中会发生什么。
在 Java 中,所有东西都可以是堆分配的。每个东西还可以从 Object 继承一个 toString 方法,这意味着您可以使用动态调度获取程序中任何内容的字符串表示形式。
因此,当您使用 String.format 时,您会得到与 println 上面所述类似的东西。
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
}
Object...
仅仅是一种特殊的语法,允许你在运行时将数组作为第二个参数传入,而Java编译器会让你在代码中使用{}
来代替该数组。
最大的区别在于,与Rust不同,Java中的不同类型总是*被封装在指针后面。因此,你不需要提前知道 T
,就可以生成字节码/机器码来实现这一点。
String.format("Hello %s, %s", 99, "red baloons");
除了忽略JIT,它在机械上做的与这个差不多。
println("Hello {:?}, {:?}", &[Box::new(99), Box::new("red balloons")]);
Rust的难题在于如何提供至少与Java版本同样好或更好的人机交互方式,而又不会产生不必要的堆分配或动态调度。宏为解决这个问题提供了一种机制。
(Java也可以解决Debug/Display等问题,因为你可以在运行时检查已实现的接口,但这不是本文的核心原因)
此外,使用宏而不是接受字符串和数组的函数意味着您可以针对不匹配或缺少参数提供编译时错误提示,这是相当稳健的设计选择。