What is the “standard” way to concatenate strings?

2019-06-15 13:14发布

问题:

While I understand basically what str and std::string::String are and how they relate to each other, I find it a bit cumbersome to compose strings out of various parts without spending too much time and thought on it. So as usual I suspect I did not see the proper way to do it yet, which makes it intuitive and a breeze.

let mut s = std::string::String::with_capacity(200);
let precTimeToJSON = | pt : prectime::PrecTime, isLast : bool | {
    s.push_str(
        "{ \"sec\": " 
       + &(pt.sec.to_string()) 
       + " \"usec\": " 
       + &(pt.usec.to_string()) 
       + if isLast {"}"} else {"},"})
    };    

The code above is honored by the compiler with error messages like:

src\main.rs:25:20: 25:33 error: binary operation + cannot be applied to type &'static str [E0369]

And even after half an hours worth of fiddling and randomly adding &, I could not make this compilable. So, here my questions:

  • What do I have to write to achieve the obvious?
  • What is the "standard" way to do this in Rust?

回答1:

The Rust compiler is right (of course): there's no + operator for string literals.

I believe the format!() macro is the idiomatic way to do what you're trying to do. It uses the std::fmt syntax, which essentially consists of a formatting string and the arguments to format (a la C's printf). For your example, it would look something like this:

let mut s: String = String::new();
let precTimeToJSON = | pt : prectime::PrecTime, isLast : bool | {
    s = format!("{{ \"sec\": {} \"usec\": {} }}{}",
        pt.sec,
        pt.usec,
        if isLast { "" } else { "," }
    )
};

Because it's a macro, you can intermix types in the argument list freely, so long as the type implements the std::fmt::Display trait (which is true for all built-in types). Also, you must escape literal { and } as {{ and }}, respectively. Last, note that the format string must be a string literal, because the macro parses it and the expanded code looks nothing like the original format! expression.

Here's a playground link to the above example.

Two more points for you. First, if you're reading and writing JSON, have a look at a library such as rustc-serialize. It's much less painful!

Second, if you just want to concatenate &'static str strings (that is, string literals), you can do that with zero run-time cost with the concat!() macro. It won't help you in your case above, but it might with other similar ones.



回答2:

Itertools::format can help you write this as a single expression if you really want to.

let times: Vec<PrecTime>; // iterable of PrecTime
let s = format!("{}", times.iter().format(",", |pt, f|
    f(&format_args!(r#"{{ "sec": {}, "usec": {} }}"#, pt.sec, pt.usec))
));

format() uses a separator, so just specify "," there (or "" if you need no separator). It's a bit involved so that the formatting can be completely lazy and composable. You receive a callback f that you call back with a &Display value (anything that can be Display formatted).

Here we demonstrate this great trick of using &format_args!() to construct a displayable value. This is something that comes in handy if you use the debug builder API as well.

Finally, use a raw string so that we don't need to escape the inner " in the format: r#"{{ "sec": {} "usec": {} }}"#. Raw strings are delimited by r#" and "# (free choice of number of #).

Itertools::format() uses no intermediate allocations, it is all directly passed on to the underlying formatter object.



回答3:

You can also do this madness:

fn main() {
    let mut s = std::string::String::with_capacity(200);

    // Have to put this in a block so precTimeToJSON is dropped, see https://doc.rust-lang.org/book/closures.html
    {
        // I have no idea why this has to be mut...
        let mut precTimeToJSON = |sec: u64, usec: u64, isLast: bool| {
            s.push_str(&( // Coerce String to str. See https://doc.rust-lang.org/book/deref-coercions.html
                "{ \"sec\": ".to_string()      // String 
                + &sec.to_string()             // + &str    (& coerces a String to a &str).
                + " \"usec\": "                // + &str
                + &usec.to_string()            // + &str
                + if isLast {"}"} else {"},"}  // + &str
            ));
        };
        precTimeToJSON(30, 20, false);
    }
    println!("{}", &s);
}

Basically the operator String + &str -> String is defined, so you can do String + &str + &str + &str + &str. That gives you a String which you have to coerce back to a &str using &. I think this way is probably quite inefficient though as it will (possibly) allocate loads of Strings.



标签: rust