OSRust.007 advances from Rust ownership and borrowing to slices, which provide borrowed views of contiguous sequences without transferring ownership of their underlying storage. A Rust string slice, &str, contains a reference to valid UTF-8 data. Its ranges use byte offsets, not user-visible character positions. Recognizing that distinction is fundamental for safe text parsing, filesystem tooling, structured logs, networking, and software that handles international text.
The lesson follows OSRust.006 on ownership, moves and borrowing, OSRust.004 on functions, and OSRust.003 on scalar and compound data types. The earlier borrowing lesson introduced references; the present lesson focuses on reference ranges and encoding-aware text boundaries rather than repeating ownership fundamentals.
Learning Objectives and Prerequisites
- Distinguish owned
Stringfrom borrowed&str, and explain the difference between a slice and its owner. - Use half-open slice ranges such as
..3,3.., and1..4for valid boundaries. - Compute UTF-8 byte length separately from the number of Unicode scalar values returned by
chars(). - Use
str::getto returnOption<&str>instead of panicking when a user-provided range is invalid. - Write functions that accept
&strand slices such as&[i32]without cloning data. - Explain how borrow checking prevents mutation or deallocation while an active borrowed view is still used.
Prerequisites: OSRust.006 borrowing and references, if and for syntax from OSRust.005, plus basic understanding of bytes and hexadecimal notation. Materials: a Rust toolchain installed through rustup, a text editor and terminal. A current stable Rust compiler supporting Edition 2021 is suitable. No external crate or elevated access is needed. The available publishing environment did not contain rustc or cargo, so Rust code in the lesson was checked against official specifications but was not compiled or executed here.
Why Borrowed Slices Matter
In Rust, a String owns a growable UTF-8 string allocation, while &str borrows a UTF-8 sequence. The borrowed view can point into a String or represent a string literal. Passing &str does not transfer ownership or require copying the selected text. The official Rust Book chapter on slices develops precisely that relationship and shows why a borrowed range is more reliable than retaining an unrelated byte index after an owned string changes.

A &str value conceptually contains a pointer to the start of the selected bytes plus a byte length. It does not own the allocation behind those bytes. Rust checks that an ordinary safe reference does not outlive the data it points to. The exact physical layout of every variable or allocation is an implementation concern: the important source-level property is that ownership stays with the source while an immutable slice borrows its contents.
String Slices Use Half-Open Byte Ranges
The expression &message[0..4] creates a borrowed string slice from byte offset 0 up to but excluding offset 4. Valid UTF-8 string slicing requires both ends to be within the allocation and positioned at UTF-8 scalar boundaries. Rust’s usual range notation follows a half-open convention; the range length in bytes equals its end minus its start. Ranges with omitted bounds use the beginning or end of the string.
fn main() {
let message = String::from("rust language");
let first = &message[..4];
let rest = &message[5..];
println!("{first}");
println!("{rest}");
}
Expected output (documentation-checked, not executed here):
rust language
The source is entirely ASCII, so all its byte boundaries align with visible characters. The first slice borrows bytes 0–3; the second starts at offset 5, after the space at byte offset 4. A string literal such as "rust language" also has type &str. Code can therefore accept a borrowed view without requiring that callers allocate a new String.
Unicode Text Is Not an Array of One-Byte Characters
UTF-8 uses a variable number of bytes to represent a Unicode scalar value. For example, A is one byte, é is two bytes, and the crab emoji 🦀 is four bytes. Their concatenation "Aé🦀" contains seven bytes but three Unicode scalar values. Rust str::len() returns bytes, whereas chars().count() counts scalar values. A human-perceived grapheme cluster can contain several scalar values and is not counted by a standard-library grapheme API. The official Rust Book discussion of UTF-8 strings explains the differences.

41 C3 A9 F0 9F A6 80 and has valid scalar boundaries at byte offsets 0, 1, 3, and 7. Bytes independently checked using UTF-8 encoding; Rust execution was not available.Offsets 2, 4, 5 and 6 fall inside multibyte scalar values, not between complete scalars. Therefore &text[1..3] is a valid slice of "Aé🦀" representing "é", while &text[2..3] would panic at runtime rather than form an invalid &str. By contrast, text.get(2..3) returns None safely. The standard-library str documentation states both the boundary requirement and the non-panicking behavior of get.
Byte Count, Scalar Count, and Grapheme Count
Visual appearance cannot determine the number of bytes or Unicode scalar values. A precomposed é is two UTF-8 bytes and one scalar value; an e followed by a combining acute accent is three bytes and two scalar values. Both can look similar when rendered with appropriate font support, but their byte sequences differ. Unicode normalization and grapheme segmentation are separate topics, not automatically performed by ordinary string slicing.

A 1 byte/1 scalar, é 2/1, 🦀 4/1, and e plus combining acute accent 3/2. Standard Rust counts scalars with chars(); it does not provide user-perceived grapheme segmentation by default.Use get() for Untrusted Byte Offsets
Direct indexing with a range checks boundaries but may panic if the range is invalid. For text offsets derived from a file, network message, or user input, return an Option instead. The get method reports valid ranges with Some(&str) and invalid ones with None. It does not make arbitrary offsets meaningful: the caller still needs to decide whether those positions represent words, protocol fields, or whole Unicode scalar values.
fn safe_prefix(text: &str, end: usize) -> Option<&str> {
text.get(..end)
}
fn main() {
let text = "Aé🦀";
println!("bytes = {}", text.len());
println!("scalars = {}", text.chars().count());
println!("valid = {:?}", safe_prefix(text, 3));
println!("invalid = {:?}", safe_prefix(text, 2));
}
Expected output (not compiler-tested in the present environment):
bytes = 7
scalars = 3
valid = Some("Aé")
invalid = None
The signature Option<&str> uses lifetime elision: the returned borrowed view is tied to the input text, not to new owned storage. A program must not return a slice of a temporary String that has already been destroyed. When an application actually needs to keep text independently, it may use an explicitly allocated owned String. Do not replace safe indexing with get_unchecked to bypass boundary checks without a carefully proven unsafe contract.
Build a Function That Borrows Its First Word
A reusable function may accept &str regardless of whether the caller has a String or a literal. The example uses split_whitespace to find a word according to Rust’s Unicode whitespace behavior and returns a borrowed slice of the original input. It does not allocate a new word string. For an empty input or input containing only whitespace, the chosen API returns an empty slice.
fn first_word(text: &str) -> &str {
text.split_whitespace().next().unwrap_or("")
}
fn main() {
let owned = String::from("café 🦀 tools");
let word = first_word(&owned);
println!("word = {word}");
println!("word bytes = {}", word.len());
println!("word scalars = {}", word.chars().count());
println!("empty = {:?}", first_word(" "));
}
Expected output (source reviewed, not executed):
word = café word bytes = 5 word scalars = 4 empty = ""
The whitespace iterator uses a language-defined property; applications needing punctuation-aware or locale-specific tokenization must implement a more explicit policy. The borrowed word stays tied to the original allocation. A later attempt to mutate the underlying String while the borrowed word remains in use can be rejected by the borrow checker. Under modern non-lexical lifetimes, the borrow can end after its last actual use; mutation later in the same block may be permitted when the reference is no longer used.
Array Slices Work Without Unicode Complications
Slices are not limited to text. The type &[i32] views a contiguous region of integer elements while retaining ownership elsewhere. Its indices are element positions rather than UTF-8 byte positions. The same safe borrowing principle applies to a borrowed array view, an allocated Vec<i32>, or a portion of either. The Rust slice primitive reference documents the type and its methods.
fn sum(values: &[i32]) -> i32 {
values.iter().copied().sum()
}
fn double(values: &mut [i32]) {
for value in values.iter_mut() {
*value *= 2;
}
}
fn main() {
let numbers = [3, 5, 8, 13, 21];
println!("sum = {}", sum(&numbers[1..4]));
let mut other = [2, 4, 6, 8];
double(&mut other[1..3]);
println!("doubled = {:?}", other);
}
Expected output from the shown values (calculated, not executed):
sum = 26 doubled = [2, 8, 12, 8]
The sum includes elements with indices 1, 2 and 3: 5 + 8 + 13 = 26. The mutable slice modifies elements 1 and 2 in other, leaving the owner of the array intact. Real input processing must consider signed integer overflow: sum() with i32 is not safe for every conceivable array. For arbitrary data, use checked arithmetic, an appropriately wider type, or a documented overflow policy.
Practical Lab and Reproducibility
Use a local training directory and the standard Rust toolchain; no external crates, administrator access, network service, or real customer information are required. The demonstrations are copyable and use Rust’s 2021 edition. In this publishing environment, Rust compiler execution was unavailable because rustc and cargo were not installed; compilation and test results are instructions for the learner to verify, not a test record falsely attributed to the publisher.
- Run
rustc --versionandcargo --versionto document your toolchain. If unavailable, install using the official rustup instructions for your operating system. - Create a new package using
cargo new slice_lab, move into its folder, and replacesrc/main.rswith one complete example above at a time. Runcargo run. - Confirm
"Aé🦀".len() == 7,chars().count() == 3,get(1..3) == Some("é"), andget(2..3).is_none(). - Change a requested prefix byte offset from 3 to 7 and record why the result changes from
Some("Aé")toSome("Aé🦀"). - Call
first_wordon"café 🦀 tools", an empty string, and a whitespace-only input. Document all three results. - Explain why an array slice
&numbers[1..4]has three elements even though it may occupy many bytes.
Add Automated Tests to Verify the Claims
To make the boundary behavior independently testable, add the following code to src/lib.rs in a local Cargo package, then run cargo test. These assertions cover normal Unicode slicing, an invalid mid-scalar offset, empty input, and the word-borrowing contract. The tests have not been run by the publisher and should be treated as a reproducible verification plan.
pub fn safe_prefix(text: &str, end: usize) -> Option<&str> {
text.get(..end)
}
pub fn first_word(text: &str) -> &str {
text.split_whitespace().next().unwrap_or("")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utf8_boundaries() {
let text = "Aé🦀";
assert_eq!(text.len(), 7);
assert_eq!(text.chars().count(), 3);
assert_eq!(safe_prefix(text, 1), Some("A"));
assert_eq!(safe_prefix(text, 3), Some("Aé"));
assert_eq!(safe_prefix(text, 2), None);
assert_eq!(safe_prefix(text, 7), Some(text));
}
#[test]
fn words_borrow_input() {
assert_eq!(first_word("café 🦀 tools"), "café");
assert_eq!(first_word(""), "");
assert_eq!(first_word(" "), "");
}
}
A successful local cargo test should report two passing test functions, while the assertions inside them verify several properties. If compilation fails because there is already a function named safe_prefix or first_word in the same module, avoid duplicate definitions by using the library tests separately from the earlier main.rs examples. If the editor inserts invalid Unicode replacement symbols, save the source in UTF-8 and inspect the original literal.
Troubleshooting Common Errors
- Panic from a range: a range like
&text[2..3]splits a multibyte scalar. Usegetto handle invalid positions and work with actual scalar boundaries. - Cannot index str by integer: Rust does not implement direct numeric character indexing like
text[0]. Choosechars(), a byte-range slice, or a protocol-specific parser. - Borrow checker rejects a mutation: an immutable borrowed view is still used after a proposed mutation. Rearrange the final use, return an owned value, or redesign the data flow instead of bypassing memory safety.
- Unexpected byte count:
len()counts UTF-8 bytes. Compare it withchars().count()and do not assume grapheme clusters equal scalar values. - Empty result from word parsing: leading whitespace or empty input requires a defined application policy.
split_whitespace()skips whitespace, andunwrap_or("")provides the chosen empty fallback. - Out-of-bounds array range: array and vector slices have bounds checks too, but use element positions;
getalso provides safe optional access for slices. - Compiler not found:
rustcandcargoare not installed or missing from the path. Use official rustup documentation and record the installed toolchain before claiming any test was executed.
Knowledge Check with Answers
- Who owns bytes referenced by an &str? The owner of the original allocation or static data, not the borrowed slice.
- How many bytes and scalar values are in “Aé🦀”? Seven bytes and three scalar values.
- What is the difference between
text.get(2..3)and&text[2..3]for that string? The former returnsNone; the latter panics because offset 2 lies insideé. - Why can
first_word(&owned)return a &str without a clone? It borrows a contiguous part of the caller’s UTF-8 data. - Can
chars()reliably count how many letters a user sees? Not always. Combining scalar values can form one grapheme cluster. - What does a half-open range
1..4include? Positions 1, 2, and 3; index 4 is excluded. - What sum results from
[3,5,8,13,21][1..4]? 26. - What must be true before keeping a borrowed slice? Its underlying data must remain valid for the borrow, subject to Rust’s rules.
- Was Rust compilation verified by the publisher? No. UTF-8 byte calculations were independently verified, and the Rust source follows official references, but a Rust compiler was unavailable.
References and Further Study
Primary references: The Rust Programming Language — The Slice Type; References and Borrowing; Storing UTF-8 Encoded Text With Strings; Rust standard-library str documentation; Rust primitive slice documentation; and Cargo test documentation. The logo’s trademark and reuse policy is maintained by the Rust Foundation. Earlier lessons include OSRust.001 on installing the toolchain and OSRust.006 on ownership.
Conclusion
A slice lets Rust code work with borrowed regions of memory without copying or taking ownership. Text slices add an essential restriction: valid UTF-8 must remain intact, and byte ranges cannot split a Unicode scalar. Safe optional indexing, borrowed function parameters, careful handling of Unicode and reproducible local tests form a foundation for future lessons on collections, structs and robust parser design.
Editor’s Note: OSRust.007 continues the published OSRust.006 ownership lesson. The featured 1200 × 630 illustration depicts a fictional older South Asian female instructor, rotating the instructor representation from OSC.006’s middle-aged male programming mentor. The Rust Foundation logo is genuine, credited and used without implying affiliation. Two original charts visualize independently checked UTF-8 byte and scalar-value calculations; they are explanatory graphics, not outputs of a Rust runtime. Because the available environment lacked rustc and cargo, no successful Rust compile or test execution is claimed. No unverified YouTube ID or video playback is included; independent public-browser pixel inspection remains separate from WordPress markup checks.

On a mobile wallet, display the QR on another screen or use a supported saved-image scanner. Confirm the payment recipient and 5,000-satoshi amount before sending.
We volunteer daily to ensure the information on this platform is Verifiably True. If you would like to support the research, please scan the QR code.

Leave a Reply