println!错误:需要字面量/格式参数必须是字符串字面量

89

这个非常简单的 Rust 程序:

fn main() {
    let c = "hello";
    println!(c);
}

会抛出以下编译时错误:

error: expected a literal
 --> src/main.rs:3:14
  |
3 |     println!(c);
  |              ^
在Rust的早期版本中,错误信息为:
error: format argument must be a string literal.
     println!(c);
              ^

用以下程序替换原程序:

fn main() {
    println!("Hello");    
}

正常工作。

我不清楚这个错误的含义,谷歌搜索也没有真正解决它。为什么将c 传递给println!宏会导致编译时错误?这似乎是相当不寻常的行为。

4个回答

74

TL;DR 如果你不关心“为什么”,只想修复它,请参见兄弟答案


原因是:

fn main() {
    let c = "hello";
    println!(c);
}

无法工作的原因是println!宏在编译时查看字符串,并验证参数和参数说明符的数量和类型是否匹配(这是非常好的!)。 在宏评估期间,此时无法确定c来自文字还是函数或其他。下面是宏扩展的示例:
let c = "hello";
match (&c,) {
    (__arg0,) => {
        #[inline]
        #[allow(dead_code)]
        static __STATIC_FMTSTR: &'static [&'static str] = &[""];
        ::std::io::stdio::println_args(&::std::fmt::Arguments::new(
            __STATIC_FMTSTR,
            &[::std::fmt::argument(::std::fmt::Show::fmt, __arg0)]
        ))
    }
};

我不认为编译器无法弄清楚这个问题,但要做这件事情可能需要付出大量的工作且得到的回报很少。宏作用于AST的一部分,AST仅具有类型信息。为了使其在此情况下起作用,AST必须包括标识符的源代码以及足够的信息以确定其可用作格式字符串。此外,它可能会与类型推断产生冲突 - 您需要在选定类型之前知道类型!
错误消息要求“字符串文字”。 “字面量是什么意思?” 请参考这里,其中链接到维基百科条目
“字面量是表示源代码中固定值的符号” "foo" 是一个字符串字面量,8 是数字字面量。 let s = "foo" 是将字符串字面量的值赋给标识符(变量)的语句。 println!(s) 是提供标识符给宏的语句。

2
为什么在编译时无法确定c是否来自编译期间的文字? - Mike Vella
我认为真正的问题在于,如果 c = "{}" 并且宏在编译时被解析,那么编译器很难确定是否需要进一步的参数,因此无法保证在编译时进行验证... - evotopid
4
我认为这并非永远不可能,只是在当前宏的实现方式下不可行。宏获取抽象语法树(AST)的部分内容,我认为只包含类型信息。你的问题需要知道类型的来源以及足够的信息来确定它是否“安全”。此外,它可能与类型推断产生冲突 - 你需要在类型被选定之前就知道类型! - Shepmaster
谢谢,我现在明白了(或者说我不那么困惑了)- @Shepmaster,你能在你的回答中加入关于宏和AST的那一部分吗?非常有用的信息。 - Mike Vella
我认为通过使这项工作实现,可以获得一些收益。log4rs使用相同的宏方案。每当从函数返回结果时出现错误字符串,并且您想要使用error!(err)记录它时,您无法这样做。您必须使用error!("{}", err) hack。这不是世界末日,但确实是一个轻微的刺激。 :) - marathon

72

这应该能够工作:

fn main() {
    let c = "hello";
    println!("{}", c);
}

字符串"{}"是一个模板,其中{}将被替换为传递给println!的下一个参数。


5
虽然这样做起来有点丑陋,但确实可行!为什么会出现原始错误呢?以这种方式打印字符串几乎是现代语言中非常常见的任务,并且给出的错误信息真的很模糊。 - Mike Vella
3
这句话并不含糊,但前提是你要知道什么是“字面量”。"foo" 是一个字符串字面量。 8 是一个数字字面量。 let s = "foo" 把一个字符串字面量的值赋给了变量s,而 println!(s) 则把这个变量传递给了宏。 - Shepmaster
4
谢谢@Shepmaster。你能否提供更详细的答案?我想许多其他使用Rust的用户将会遇到来自Python等语言的此类错误,有你的解释会很有帮助。 - Mike Vella
宏检查是指文本(字符串、数字等)和使用let分配的变量不属于这些类型。 - paul
2
不应该期望从一门新语言开始你就能猜测函数的工作方式。我的意思是,比较一下C++的std::cout,它有着截然不同的语法。学习一门新语言意味着阅读教程和文档,并且只使用你所学到的知识。否则你可能会遇到意外的行为,甚至更糟糕的是安全漏洞(或者像使用可变数组函数的返回值这样的奇怪错误,而没有意识到它改变了原始数组。Rust的mut可以解决这个问题,但其他语言没有mut)。 - Nicholas Pipitone

9

如果你真的想要在一个地方定义println!的第一个参数,我找到了一种方法。你可以使用宏:

macro_rules! hello {() => ("hello")};
println!(hello!());

在这里看起来似乎没有什么用处,但我想在几个地方使用相同的格式,而在这种情况下该方法非常有帮助:

macro_rules! cell_format {() => ("{:<10}")};  // Pads with spaces on right
                                              // to fill up 10 characters
println!(cell_format!(), "Foo");
println!(cell_format!(), 456);

宏在我的代码中免去了重复的格式选项。
显然,您也可以使宏更加花哨,并在必要时使用参数打印不同的内容。

2
如果你要采用宏的方法,那么最好将 println! 直接放在宏内部,像这样 macro_rules! cell_format { ( $e:expr ) => ( println!("{:<10}", $e) ); } - mattforni

4

如果您的格式字符串仅被重复使用了适度次数,并且只有一些变量数据会被更改,那么一个小函数可能比宏更好:

fn pr(x: &str) {
    println!("Some stuff that will always repeat, something variable: {}", x);
}

pr("I am the variable data");

输出

一些总是重复的内容,以及一些可变的数据:我就是这个可变的数据。


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