Strings
Gruel's String type holds a sequence of bytes, conventionally UTF-8. Strings own their data and clean up automatically when they go out of scope.
String Literals
String literals have type String:
fn main() -> i32 {
let greeting = "hello";
@dbg(greeting); // prints: hello
if greeting == "hello" {
@dbg(1); // prints: 1
}
0
}
Move Semantics
Like structs, strings move by default—passing a string to a function transfers ownership:
fn print_it(s: String) {
@dbg(s);
}
fn main() -> i32 {
let s = "hello";
print_it(s); // s moves here
// print_it(s); // ERROR: use of moved value
0
}
Building Strings
Create an empty string with String::new(), then append with push_str:
fn main() -> i32 {
let mut s = String::new();
s.push_str("hello");
s.push_str(", ");
s.push_str("world!");
@dbg(s); // prints: hello, world!
0
}
Appending Individual Bytes
Use push to append a single byte:
fn main() -> i32 {
let mut s = String::new();
s.push_str("hello");
s.push(33); // 33 is '!'
@dbg(s); // prints: hello!
0
}
Querying a String
Query methods use borrow self, so they don't consume the string:
fn main() -> i32 {
let s = "hello, world!";
@dbg(s.len()); // prints: 13
@dbg(s.is_empty()); // prints: false (0)
let empty = String::new();
@dbg(empty.is_empty()); // prints: true (1)
0
}
Cloning
To make an independent copy of a string, use clone:
fn main() -> i32 {
let a = "hello";
let b = a.clone(); // b is a separate copy
// Both a and b are valid independent strings
@dbg(a); // prints: hello
@dbg(b); // prints: hello
0
}
Clone is explicit because it allocates memory. Use it only when you need two independent strings.
Automatic Cleanup
When a String goes out of scope, its memory is freed automatically. You never call free manually:
fn build_string() -> String {
let mut s = String::new();
s.push_str("built inside a function");
s // returned, not dropped
}
fn main() -> i32 {
let s = build_string();
@dbg(s); // prints: built inside a function
// s is dropped here when main returns, memory freed
0
}
Custom Destructors
If your struct holds a resource that needs cleanup, define a destructor with drop fn:
struct Buffer {
data: String,
size: i32,
}
drop fn Buffer(self) {
// self.data is dropped automatically (String has its own destructor)
// Any additional cleanup logic goes here
@dbg(0); // just to show this runs
}
fn main() -> i32 {
let buf = Buffer { data: "some data", size: 9 };
// When buf goes out of scope, the destructor runs,
// which then drops data (freeing the String's memory)
0
}
The destructor runs automatically at the end of the scope. Drop order is reverse declaration order: the last declared variable is dropped first.
Pre-allocating Capacity
If you know roughly how large a string will be, pre-allocate to avoid repeated reallocations:
fn main() -> i32 {
let mut s = String::with_capacity(64);
s.push_str("first part");
s.push_str(" second part");
s.push_str(" third part");
@dbg(s); // prints: first part second part third part
0
}
Clearing a String
clear empties a string but keeps the allocated memory for reuse:
fn main() -> i32 {
let mut s = String::new();
s.push_str("temporary");
s.clear();
s.push_str("reused");
@dbg(s); // prints: reused
0
}