24 Jan 24
Rust Strings
Let’s learn about Rust strings. As in plural. There are essentially two types. Um, what? You can’t be serious? Well, kinda.
Is this going to be one of those examples that demonstrates why Rust is hard to learn? No, not really. But, as simple as Strings are in other languages, they are a great gateway into the Rust ownership model, stack vs heap, etc… Before we get ahead of ourselves, let’s see some simple examples compared to Ruby.
In Ruby, we can use Strings as values for variables and constants. We’re going to use our Ruby project from the Getting Started post.
# lib/my_project_ruby.rb module MyProjectRuby class Error < StandardError; end # Your code goes here... puts "hello world" end
You can see we already use a String in the puts
statement. The code “hello world” defines a String value. Let’s add some more examples.
# lib/my_project_ruby.rb module MyProjectRuby class Error < StandardError; end NAME = "foo" puts NAME.class # => String substring = "this is a string".split(" ").last puts substring.class # => String puts "hello world".class end
If you run this file via ruby lib/my_project_ruby.rb
from the project root, you will see:
String String String
If you are familiar with Ruby all of this should be familiar. We defined a Constant with a String value. Next we defined a variable called substring
and used a method on a String to define another String value. Lastly, we used the existing puts
statement and rather than printing out the String value, we printed out the class of the value which of course is String. Defining Strings in Ruby is pretty straightforward. You can use double or single quotes.
Let’s look at the equivalent in Rust. Again, we’ll reuse our Rust project from the Getting Started post.
// src/main.rs const NAME: &str = "foo"; fn main() { println!("value of constant is {}", NAME); println!("Hello, world!"); }
Here we go! This is where we get into there being two String types in Rust. See how the constant is defined with a type of &str
. This is the String literal in Rust. It is most commonly referred to as a string slice. And in its borrowed form. You may already be thinking that Rust Strings are a lot more complex than in a language like Ruby. Tis true. Let’s unpack this and get a good understanding.
Strings can be defined in two ways in Rust.
// this is a string slice type (aka &str) (aka not growable) let hello_world = "Hello, World"; // this is a String type (aka a growable string) let hello_world = String::from("Hello, world!");
str
is defined here: https://doc.rust-lang.org/std/str/index.html.
String
is defined here: https://doc.rust-lang.org/std/string/struct.String.html
Ok, why are there two String types? This is where we have to get into how Rust manages memory and performs so well. Rust manages memory using a Stack and a Heap. If you want a brief but concise overview, I suggest the Rust book. In the Ownership section there is a great break out section that describes the Stack vs the Heap.
Notice in the comments, I wrote aka not growable
and aka a growable string
. Growable in this sense means, we know the exact value at the time we define the value, or we don’t know the entire value at the time, or suspect it will change. In the first statement where we define hello_world
using a &str value, we know the entire value and it is not going to change. Based on this information, Rust can place this variable and its value on the Stack. It’s concrete and therefore we can be precise about the amount of memory we need. This makes it fast to retrieve. The Rust book explains: “In the case of a string literal, we know the contents at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient.”
Let’s see what happens when we try to grow it: // src/main.rs const NAME: &str = “foo”;
fn main() { println!("value of constant is {}", NAME); let hello_world = "Hello World"; hello_world.push_str("! May I borrow you?"); println!("{}", hello_world); }
When we run this with cargo run
, we receiving the following error:
error[E0599]: no method named push_str
found for reference &str
in the current scope
–> src/main.rs:8:17
|
8 | helloworld.pushstr(“! May I borrow you?”);
| ^^^^^^^^ method not found in &str
push_str
is a method used to concatenate a String. Notice it doesn’t exist on &str. That is our first clue that we should think of &str values as not able to be changed.
In the second let
we use String::from
to define the value. Image we are collecting user input from a form. We don’t know the value the user would provide, so we take that input and use it to define a String. Because we don’t know the explicit and complete value, we use a String type and Rust stores this value on the Heap. This gives us a slot in memory that will be accommodating for various lengths of strings (with defined bounds). If we’re accepting user input, we may not know the number of characters they are going to enter, so we need variable space. Say something between 0 and 64 characters for example. Rust’s String struct contains an address, length and capacity. Capacity being the maxed allowed size.
Let’s see what happens when we try to grow that value: // src/main.rs const NAME: &str = “foo”;
fn main() { println!("value of constant is {}", NAME); let mut hello_world = String::from("Hello World"); hello_world.push_str("!"); // add the exclamation println!("{}", hello_world); }
This compiles and the result is: Hello World!
Notice we used mut
to make the variable mutable, but the point isn’t about mutatability. We are talking about known values. Maybe we should think of them as hard coded vs dynamic values. In Ruby, NAME = "foo".freeze
would be a &str because the value would be known, essentially hardcoded, should there be a compile step. Obviously, the same classes and objects don’t exist in both Ruby and Rust, so It’s not apples to apples. We’re simply using them to illustrate the point.
I was going to cover borrowing now since I mentioned it before, but it’s a topic all its own. For simple terms, let’s say borrowing means you reference values (and memory slots) rather than copying the value for a specific scope. We’ll cover it in a lot more detail in a future post.
So let’s stick to strings. Let’s complete this post with a look at how to do common things regardless of language. First a few common examples in Rust.
// src/main.rs fn main() { # basic assignment let hello_world = "Hello World"; // print a string println!("{}", hello_world); // concatenate a string let mut name = String::from("Steve"); name.push_str(" Jobs"); println!("name: {}", &name); // determine the length of a string (answer: 3) let len = "foo".len(); println!("len: {}", &len); // determine the index of a substring let index: Vec<_> = "foobar".match_indices("bar").collect(); println!("index: {}", &index[0].0); // trim newlines and spaces let trim = " \nfoo \n ".trim(); println!("trimmed: {}", trim); // debug/display trait let debug_str = "this is a &str"; dbg!(debug_str); let debug_string = String::from("this is a String"); dbg!(debug_string); }
And now the equivalent in Ruby:
module MyProjectRuby class Error < StandardError; end # basic assignment hello_world = "hello world!" # print a string puts hello_world # concatenate a string name = "Steve"; name += " Jobs"; puts name # determine the length of a string (answer: 3) len = "foo".size puts "len: #{len}" # determine the index of a substring index = "foobar".index("bar") puts "index: #{index}" # trim newlines and spaces trim = " \nfoo \n ".strip(); puts "trimmed: #{trim}" # debug/display trait debug_str = "this is a string"; puts debug_str.inspect end
That gives a basic overview of Strings in Rust compared to Ruby. In the next post, we’ll get into the various other common objects related to numbers.