On Strings in Rust | BloggingsTable of Contents1. str vs. Stringstr<br>String
2. [Char] vs. String/str and dot operator semanticsDeref coercion<br>Dot operator semantics
Other string types
Strings in Rust are not simple, and perhaps not the easiest starting point when learning Rust. There is more to Rust strings than initially meets the eye, and when learning to understand them, you encounter a large variety of Rust-specific concepts that pervade the language as a whole. The concepts touched in this post are: slices (and thus dynamically sized types, ?Sized, wide/fat pointers), iterators, deref coercion, dot operator semantics, operator overloading, and Unicode (not specific to Rust, but still useful to know).<br>Apart from a spellchecker no LLMs were used in the writing of this blog post, all mistakes are mine alone.<br>In this blog post, I want to fully demystify the following (admittedly contrived) but instructive block of code.<br>// 1. str vs. String<br>let hello: String = String::from("Hello");<br>let world: &'static str = "Wörld";<br>assert_eq!(world.len(), 6);
let mut char_constructed_string: String = String::new();<br>char_constructed_string.push('a');<br>let string_as_str: &str = char_constructed_string.as_str();
let hello_world: String = hello + " " + world;<br>assert_eq!(hello_world, "Hello Wörld");
// 2. [char] vs. String/str and dot operator semantics<br>let world_chars: [char; 5] = ['W', 'ö', 'r', 'l', 'd'];<br>assert_eq!(world.len(), world_chars.len()); ↯<br>assert_eq!(size_of_val(world), size_of_val(&world_chars)); ↯
let mut owned_world: String = String::from(world);<br>assert_eq!(owned_world.chars().count(), world_chars.len());<br>owned_world.make_ascii_uppercase();<br>assert_eq!(owned_world, "WÖRLD"); ↯<br>assert_eq!(owned_world, "WöRLD");
1. str vs. String#<br>let hello: String = String::from("Hello");<br>let world: &'static str = "Wörld";<br>assert_eq!(world.len(), 6);
let mut char_constructed_string: String = String::new();<br>char_constructed_string.push('a');<br>let string_as_str: &str = char_constructed_string.as_str();
let hello_world: String = hello + " " + world;<br>assert_eq!(hello_world, "Hello Wörld");
str should be thought of as a wrapper around [u8] (i.e. pub struct str([u8]), the precise definition is compiler defined, since str is a primitive type).<br>String is literally defined as pub struct String { vec: Vec } here in the alloc crate (which is re-exported by the standard library).<br>These definitions already tell us that the characters of a string are stored as bytes. However, safe Rust guarantees an additional invariant on those bytes for both String and str: their sequence is UTF-8 encoded Unicode. In UTF-8, every code point (think character) is encoded using 1 to 4 bytes. For example: all ASCII code points, the umlaut ‘ö,’ and the segmented digit six ‘🯶’ are encoded using 1, 2, and 4 bytes, respectively 1. As a result, the len() of both String and str is the number of bytes required to encode their contained code points: assert_eq!(world.len(), 6). This implementation detail seeps through the entirety of the String/str API, as we will see later.<br>So, how do str and String differ?<br>str#<br>str is a slice type and as such is also called string slice. Slices [T] are an abstraction over a contiguous sequence of elements of type T. For example, a slice [u8] can be used to represent all the following arrays: [1,2,3], [137], []. This abstraction is incredibly powerful, as it allows you to write code that works on any contiguous sequence of elements, instead of only specific array sizes, whilst not requiring heap allocations in your interface. There is one “problem” however: what is the size_of::() such a slice?<br>Slices are dynamically sized types (DSTs), which are types whose size cannot be known at compile time. To still be able to work with them in a compiled language, they are to be explicitly placed behind indirection in the form of a pointer, whose size can be known at compile time (which is array decay in C/C++).<br>With slices, however, we also want to know how many elements they contain, such as to avoid out-of-bounds access. Thus, we use a special type of pointer that can store 8 auxiliary bytes of metadata and shove the length in there. These special pointer types are called wide (or fat) pointers and are syntactically equivalent to regular pointers (pointers without metadata). As opposed to regular pointers, which are size_of::() 8 (sue me) bytes wide, these pointers are then 16 bytes wide. You can verify this easily: assert_eq!(size_of::>(), 16).<br>Most commonly, slices are encountered behind shared or exclusive references, &[T] or &mut [T], which, of course, are then also 16 bytes wide.<br>Now to tie back from slices to str, which we know is basically just [u8]: assert_eq!(size_of::(), 16) and assert_eq!(size_of::>(), 16) - nice.<br>As we can see in the example, the string literal "Wörld" has type &str, which is a shared reference into the .rdata section of your binary, where the compiler put the UTF-8...