When you’ve spent years writing C#, you get really comfortable with string
being immutable, Span<T>
being your performance trick, and StringBuilder
being your go-to hammer when a for
loop starts building text.
And then you start learning Rust.
Suddenly, String
isn’t immutable. &str
looks suspiciously like a Span<char>
in disguise. And you realize… wait, do I even need a StringBuilder
anymore?
Today on Day 12, I dove into slices and strings in Rust, and let me tell you, it’s a whole new world, but a surprisingly elegant one.
Strings in C#: Immutable, Safe, and Everywhere
You probably know this dance by heart:
string name = "Alice"; string upper = name.ToUpper(); Console.WriteLine(name); // still "Alice"
Strings in .NET are immutable reference types backed by UTF-16. If you want to modify one, you either:
- Reassign the variable, or
- Reach for a
StringBuilder
if performance matters
You also get tools like Span<char>
and Memory<T>
for performance-critical code when slicing or manipulating buffers.
Strings in Rust: A Bit More to Unpack
Rust splits strings into two distinct types:
String
– a growable, heap-allocated UTF-8 string&str
– a string slice, referencing a part of aString
(or a string literal)
fn main() { let name = String::from("Alice"); let greeting = format!("Hello, {}!", name); println!("{}", greeting); }
You can modify String
directly:
let mut msg = String::from("Hello"); msg.push_str(", world"); println!("{}", msg); // Hello, world
Yep, no StringBuilder
needed. Just mutate the String
.
&str
: The Slice-y Sidekick
The &str
type is like a read-only view into a String
. Think of it like a Span<char>
in C#, but with guaranteed safety and lifetimes checked at compile time.
fn greet(name: &str) { println!("Hi, {}!", name); } fn main() { let user = String::from("Alice"); greet(&user); // passing a &str }
You can even slice strings with range syntax:
let text = String::from("Rustacean"); let slice = &text[0..4]; // "Rust" println!("{}", slice);
But here’s the catch: Rust strings are UTF-8 encoded. So slicing is byte-based, not char-based. Slicing in the middle of a multibyte character? Compiler panic.
Comparing with C#: Span<T> Vibes
Rust’s &str
gives you the safety and flexibility of a C# ReadOnlySpan<char>
, but with much tighter compiler enforcement.
And if you want something like a Span<u8>
in Rust, just use a &[u8]
slice:
let bytes = b"hello"; // byte string literal
There’s no need to pin memory, worry about unsafe access, or juggle multiple string types. Rust’s system is consistent, even if it’s more verbose at times.
UTF-8 vs UTF-16: Why It Matters
C# strings are UTF-16. That means most common characters (including emojis) take 2 bytes, but some use surrogate pairs. Slicing blindly is usually okay, but not always safe.
Rust strings are UTF-8, which is smaller for ASCII and more web-native. But it also means:
let smile = "😊"; println!("{}", &smile[0..1]); // ERROR!
Why? Because you’re trying to cut a byte in the middle of a 4-byte emoji. Rust protects you from doing that by accident. Thanks, borrow checker (again).
Do You Ever Need StringBuilder in Rust?
Not really.
With String
, you can:
- Append with
push_str()
orpush()
- Format with
format!()
(likestring.Format()
or C# interpolated strings) - Replace substrings, trim, split… everything you expect
If you need high-performance streaming or chunked writes, you’d probably reach for std::fmt::Write
or buffered I/O, but for most devs, String
just does the job.
Final Thoughts: It’s Simpler Than It Looks
At first, having String
and &str
felt like one type too many. But now? I get it.
Rust separates string ownership from string views. And once you embrace slices, you realize they’re everywhere: strings, arrays, buffers. It’s one concept to learn that applies to multiple types.
Tomorrow, we examine shadowing, specifically re-declaring variables with the same name intentionally. It may sound unusual, but it’s actually quite elegant. Stay tuned.