
Node to Rust — Day 6: Strings, Part 1
December 6, 2021
Introduction
The first hurdle with Rust and strings comes from misaligned expectations. A string literal ("Hi!"
) isn’t an instance of a String
in Rust. You don’t need to fully understand the code below yet, just know that it outputs the types of the values sent to print_type_of
.
fn main() {
print_type_of(&"Hi!");
print_type_of(&String::new());
}
fn print_type_of<T>(_: &T) {
println!("Type is: {}", std::any::type_name::<T>())
}
$ cargo run
Type is: &str
Type is: alloc::string::String
Fun fact: JavaScript string literals aren’t JavaScript String
s either.
"Hi!" === "Hi!";
// > true
"Hi!" === new String("Hi!");
// > false
Wait, there’s more.
typeof "Hi!";
// > "string"
typeof new String("Hi!");
// > "object"
typeof String("Hi!");
// > "string"
"Hi!" === String("Hi!");
// > true
String("Hi!") === new String("Hi!");
// > false
That last part is just to point out that if you can learn to love JavaScript, you can learn to love Rust.
JavaScript hand waves away the difference between string
primitives and String
instances. It automatically does what you want, when you want it, without incurring the overhead of creating an Object
for every string
. When you call a method on a primitive string
, JavaScript interpreters magically translate it to a method on the String
prototype.
Rust has similar magic, it just doesn’t always do it for you.
Quick links
- Day 1: From nvm to rustup
- Day 2: From npm to cargo
- Day 3: Setting up VS Code
- Day 4: Hello World (and your first two WTFs)
- Day 5: Borrowing & Ownership
- → Day 6: Strings, part 1
- Day 7: Syntax and Language, part 1
- Day 8: Language Part 2: From objects and classes to HashMaps and structs
- Day 9: Language Part 3: Class Methods for Rust Structs (+ enums!)
- Day 10: From Mixins to Traits
- Day 11: The Module System
- Day 12: Strings, Part 2
- Day 13: Results & Options
- Day 14: Managing Errors
- Day 15: Closures
- Day 16: Lifetimes, references, and
'static
- Day 17: Arrays, Loops, and Iterators
- Day 18: Async
- Day 19: Starting a large project
- Day 20: CLI Arguments & Logging
- Day 21: Building and Running WebAssembly
- Day 22: Using JSON
- Day 23: Cheating The Borrow Checker
- Day 24: Crates & Tools
The code in this series can be found at wasmflow/node-to-rust
This guide is not a comprehensive Rust tutorial. This guide tries to balance technical accuracy with readability and errs on the side of “gets the point across” vs being 100% correct. If you have a correction or think something needs deeper clarification, send a note on Twitter at @jsoverson, @candle_corp, or join our Discord channel.
Additional links
There is a lot written about Strings. Don’t miss the official docs and other great posts out there.
- Strings in the Rust docs
- Why Are There Two Types of Strings In Rust?
- How do I convert a &str to a String in Rust?
- Rust String vs str slices
- Rust: str vs String
- String vs &str in Rust
Rust strings in a nutshell
&str
String literals are borrowed string slices. That is to say: they are pointers to a substring in other string data. The Rust compiler puts all of our literal strings in a bucket somewhere and replaces the values with pointers. This lets Rust optimize away duplicate strings and is why you have a pointer to a string slice, vs a pointer to a single String
.
You can verify the optimizations are real, if you don’t believe me. Copy-paste the print line below a gazillion times (or less) and see that it only has a minor impact on the executable size.
fn main() {
print("TESTING:12345678901234567890123456789012345678901234567890");
}
fn print(msg: &str) {
println!("{}", msg);
}
You can also run the (not-rust-specific) strings
command to output all the string data in a binary.
$ strings target/release/200-prints | grep TESTING
TESTING:12345678901234567890123456789012345678901234567890
If you run that command on the 200-unique-prints
binary in the node-to-rust repo, you’ll get much more output.
String
String
s are the strings that you know and love. You can change them, cut them up, shrink them, expand them, all sorts of great stuff. All that brings along additional cost though. Maybe you don’t care, maybe you do. It’s in your hands now.
How do you make a &str
a String
?
In short: use the .to_owned()
method on a &str
(a “borrowed” string slice) to turn it into an “owned” String
, e.g.
let my_real_string = "string literal!".to_owned();
For what its worth, this method calls the code below under the hood.
String::from_utf8_unchecked(self.as_bytes().to_owned())
self
is Rust’sthis
.
This is why we had to go over ownership before we got into strings. String literals start off borrowed. If you need an owned String
, you have to convert it (copy it, essentially).
You’re telling me I need to write .to_owned()
everywhere?
Yes. And no. Sort of. For now, accept “yes” until we get into Traits and generics.
What about .to_string()
, .into()
, String::from()
, or format!()
?
All these options also turn a &str
into a String
. If this is your first foray into Rust from node.js, don’t worry about this section. This is for developers who have read all the other opinions out there and are wondering why other methods aren’t the “one true way.”
A Rust
trait
is sharable behavior. We haven’t gotten to them yet, but think of a trait like amixin
if you’ve ever used the mixin pattern in JavaScript.
Why not .to_string()
?
fn main() {
let real_string: String = "string literal".to_string();
needs_a_string("string literal".to_string());
}
fn needs_a_string(argument: String) {}
something.to_string()
converts something into a string. It’s commonly implemeted as part of the Display
trait. You’ll see a lot of posts that recommend .to_string()
and a lot that don’t.
The nuances in the recommendation stem from how much you want the compiler to help you. As your applications grow — especially when you start to deal with generics — you’ll inevitably refactor some types into other types. A value that was initially a &str
might end up being refactored into something else. If the new value still implements Display
, then it has a .to_string()
method. The compiler won’t complain.t
In contrast, .to_owned()
turns something borrowed into something owned, often by cloning. Turning a borrowed not-string
into an owned not-string
gives the compiler the context necessary to raise an error. If you’re OK with the difference, it’s easy to change a .to_owned()
into a .to_string()
. If you weren’t expecting it, then you highlighted an issue before it became a problem.
If you use .to_string()
, the world won’t explode. If you are telling someone they shouldn’t use .to_string()
, you have to be able to explain why. Just like you would if you used the word octopodes.
Clippy has a lint that will alert you if you use
.to_string()
on a&str
: clippy::str_to_string
Why not something.into()
?
For example:
fn main() {
let real_string: String = "string literal".into();
needs_a_string("string literal".into());
}
fn needs_a_string(argument: String) {}
something.into()
will (attempt) to turn something into a destination type by calling [dest_type]::from()
, e.g. String::from(something)
. If the destination type is a String
and your something is a &str
then you’ll get the behavior you’re looking for. The concerns are similar to those above. Are you really trying to turn something into something else, or are you trying to turn a &str
into a String
? If it’s the former, then .into()
works fine, if it’s the latter then there are better ways to do it.
Why not String::from()
?
fn main() {
let real_string: String = String::from("string literal");
needs_a_string(String::from("string literal"));
}
fn needs_a_string(argument: String) {}
String::from(something)
is more specific than .into()
. You are explicitly stating your destination type, but it has the same issues as .to_string()
. All it expresses is that you want a string but you don’t care from where.
Why not format!()
?
fn main() {
let real_string: String = format!("string literal");
needs_a_string(format!("string literal"));
}
fn needs_a_string(argument: String) {}
format!()
is for formatting. This is the only one you should definitely not use for simply creating a String
.
Clippy also has a lint for this one: clippy::useless_format
Implementation details
The path to this “one, true answer” is mapped out here. At the end of the road, everything points to .to_owned()
.
→ .to_owned()
Implemented here
Calls String::from_utf8_unchecked(self.as_bytes().to_owned())
→ String::from()
Implemented here
Calls .to_owned()
→ .to_string()
Implemented here
Calls String::from()
→ .into()
Implemented here
Calls String::from()
→ format!()
Implemented here[here]
Calls Display::fmt
for str
here
Wrap-up
Turning &str
into String
is the first half of the string issue. The second is which to use in function arguments when you want to create an easy-to-use API that takes either string literals (&str
) or String
instances.
As always, you can reach me personally on twitter at @jsoverson, the Candle team at @candle_corp, and our Discord channel.
Written By
