使println成为一个宏有什么好处?

18
在这段代码中,在println后面有一个!
fn main() {
    println!("Hello, world!");
}

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

2个回答

23

作为一个过程宏,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.
  • 接受任意数量的参数。

  • 在编译时验证格式化字符串占位符和参数是否匹配。这是C语言中printf()常见的错误源头。

  • 普通函数或方法都无法实现上述目标。

    另请参阅:


    1
    这些都无法通过普通的函数或方法实现。我认为,如果语言不允许这样做,那就是语言本身的问题。 - Stefano Borini
    这些都不可能通过普通函数或方法实现。哇哦,Rust 中没有可变参数吗?
    - Pynchia

    7

    好的,让我们暂时假设我们已经创建了那些函数。

    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]) {}  
    

    我设计的签名表示,它期望一个实现Debug接口的事物切片。但是,它也意味着它期望切片中的所有元素都是相同类型的,因此这个:
    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等问题,因为你可以在运行时检查已实现的接口,但这不是本文的核心原因)

    此外,使用宏而不是接受字符串和数组的函数意味着您可以针对不匹配或缺少参数提供编译时错误提示,这是相当稳健的设计选择。


    最初发布于2年前的Reddit帖子https://www.reddit.com/r/rust/comments/ltvzuk/comment/iu65tkl/?context=3 - Ethan McCue

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接