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, declaring types, 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 the Ruby project created in the Getting Started chapter.

# 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 # => String
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 straightforward. 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 clear and obvious. 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 chapter.

// 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. Slice? Borrowed? 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.

String values can be defined in two ways in Rust. When defining a variable in Rust, actually called a binding, we use the let keyword. Our name for the variable follows. The value follows the equal sign operator. Declaring the data type is optional. Rust is good at inferring.

Briefly, inline comments in Rust use // rather than # in Ruby.

// this is a string slice type - read-only data
let hello_world = "Hello, World";

// this is a String type - pointer on stack or heap
let hello_world = String::from("Hello, world!");

// optionally we could declare the data types:
let hello_world: &str = "Hello, World";
let hello_world: String = 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. In most occasions, 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. In addition to the Stack and Heap, there is a read-only data section in the program’s executable.

Notice in the inline comments I wrote read-only data and pointer on stack or heap. Read-only data in this sense means, we know the exact value at the time we define the value and when we compile our code. Our compiled executables have a read-only data section to store these values. This makes using the value extremely fast. 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 modify a string literal:

// src/main.rs

fn main() {
    let hello_world: &str = "Hello World";
    hello_world.push_str("! May I borrow you?");

    println!("{}", hello_world);
}

When we run this with cargo run, we receive the following error:

error[E0599]: no method named `push_str` found for reference `&str` in the current scope
 --> src/main.rs:8:17
  |
8 |     hello_world.push_str("! 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 &str values can not be changed. Not because it is a constant or frozen like we do in Ruby, but because the compiler is going to take the value at the time of assignment and add it to the data section of the executable.

In the second let above, we use String::from to define the value. Imagine 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 at compile time, 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. This is not something we have to consider with Ruby because the runtime manages the memory allocation for us (and the garbage collection).

Let’s see what happens when we try to grow that value:

// src/main.rs

fn main() {	
    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 mutability. We are talking about growing the value in size.

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. I’m 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 chapter.

So let’s stick to strings. Let’s complete this section 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 chapter, we’ll get into the various other common objects related to numbers.