Rust Utility Traits
Utility traits in Rust are fundamental traits that provide common functionality across the type system. They enable powerful abstractions and make generic programming more expressive. Let's explore each one with detailed explanations and examples.
1. Drop Trait
The Drop
trait allows you to customize what happens when a value goes out of scope 12.
use std::ops::Drop;
struct FileHandler {
filename: String,
}
impl Drop for FileHandler {
fn drop(&mut self) {
println!("Cleaning up file: {}", self.filename);
// Perform cleanup operations here
}
}
fn main() {
{
let handler = FileHandler {
filename: "data.txt".to_string(),
};
println!("File handler created");
} // Drop is called automatically here
println!("Handler dropped");
}
Key Points:
- Drop is called automatically when a value goes out of scope 2
- You cannot call
drop()
manually - usestd::mem::drop()
instead - Types implementing
Drop
cannot implementCopy
2
2. Sized Trait
Sized
is a marker trait for types whose size is known at compile time 2.
// Most types are Sized by default
struct Point {
x: i32,
y: i32,
}
// Generic functions implicitly require Sized
fn process_value<T>(value: T) {
// T: Sized is implicit
println!("Processing value of size: {}", std::mem::size_of::<T>());
}
// To work with unsized types, use ?Sized
fn process_unsized<T: ?Sized>(value: &T) {
println!("Processing reference to potentially unsized type");
}
fn main() {
let point = Point { x: 10, y: 20 };
process_value(point);
let slice: &[i32] = &[1, 2, 3, 4];
process_unsized(slice); // slice is !Sized
}
3. Clone Trait
Clone
creates deep copies of values 2.
#[derive(Clone)]
struct Person {
name: String,
age: u32,
}
// Manual implementation
struct Book {
title: String,
pages: Vec<String>,
}
impl Clone for Book {
fn clone(&self) -> Self {
Book {
title: self.title.clone(),
pages: self.pages.clone(),
}
}
fn clone_from(&mut self, source: &Self) {
self.title.clone_from(&source.title);
self.pages.clone_from(&source.pages);
}
}
fn main() {
let person1 = Person {
name: "Alice".to_string(),
age: 30,
};
let person2 = person1.clone(); // Deep copy
println!("Person 1: {}, Person 2: {}", person1.name, person2.name);
let book1 = Book {
title: "Rust Guide".to_string(),
pages: vec!["Page 1".to_string(), "Page 2".to_string()],
};
let book2 = book1.clone();
println!("Book 1 pages: {}, Book 2 pages: {}",
book1.pages.len(), book2.pages.len());
}
4. Copy Trait
Copy
enables bitwise copying for simple types 2.
#[derive(Clone, Copy)]
struct Point2D {
x: f64,
y: f64,
}
#[derive(Clone, Copy)]
enum Direction {
North,
South,
East,
West,
}
fn use_point(p: Point2D) {
println!("Point: ({}, {})", p.x, p.y);
}
fn main() {
let point = Point2D { x: 1.0, y: 2.0 };
use_point(point); // point is copied, not moved
use_point(point); // Can use point again
let dir = Direction::North;
let same_dir = dir; // Copy, not move
println!("Direction copied successfully");
}
5. Deref and DerefMut Traits
These traits enable custom dereferencing behavior 2.
use std::ops::{Deref, DerefMut};
struct MyBox<T> {
value: T,
}
impl<T> MyBox<T> {
fn new(value: T) -> Self {
MyBox { value }
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
// Smart pointer example
struct Selector<T> {
elements: Vec<T>,
current: usize,
}
impl<T> Selector<T> {
fn new(elements: Vec<T>) -> Self {
Selector { elements, current: 0 }
}
}
impl<T> Deref for Selector<T> {
type Target = T;
fn deref(&self) -> &T {
&self.elements[self.current]
}
}
impl<T> DerefMut for Selector<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.elements[self.current]
}
}
fn main() {
let mut my_box = MyBox::new(String::from("Hello"));
println!("Length: {}", my_box.len()); // Deref coercion
let mut selector = Selector::new(vec!['a', 'b', 'c']);
println!("Current: {}", *selector);
*selector = 'x'; // DerefMut allows mutation
println!("Modified: {}", *selector);
}
6. Default Trait
Default
provides sensible default values for types 2.
#[derive(Default)]
struct Configuration {
host: String,
port: u16,
timeout: u32,
}
struct DatabaseConfig {
url: String,
max_connections: u32,
timeout_seconds: u64,
}
impl Default for DatabaseConfig {
fn default() -> Self {
DatabaseConfig {
url: "localhost:5432".to_string(),
max_connections: 10,
timeout_seconds: 30,
}
}
}
fn main() {
let config1 = Configuration::default();
println!("Default port: {}", config1.port);
let config2 = Configuration {
host: "example.com".to_string(),
port: 8080,
..Default::default()
};
println!("Custom host: {}, default timeout: {}", config2.host, config2.timeout);
let db_config = DatabaseConfig::default();
println!("Default DB URL: {}", db_config.url);
}
7. AsRef and AsMut Traits
These traits provide cheap reference-to-reference conversions.
use std::path::Path;
fn process_path<P: AsRef<Path>>(path: P) {
let path = path.as_ref();
println!("Processing path: {}", path.display());
}
struct Container<T> {
value: T,
}
impl<T> AsRef<T> for Container<T> {
fn as_ref(&self) -> &T {
&self.value
}
}
impl<T> AsMut<T> for Container<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.value
}
}
fn main() {
// AsRef allows multiple types to be passed
process_path("/usr/local/bin");
process_path(String::from("/home/user"));
process_path(Path::new("/tmp"));
let mut container = Container { value: 42 };
let value_ref: &i32 = container.as_ref();
let value_mut: &mut i32 = container.as_mut();
*value_mut = 100;
println!("Container value: {}", value_ref);
}
8. Borrow and BorrowMut Traits
Similar to AsRef/AsMut but with additional guarantees about equivalence.
use std::borrow::{Borrow, BorrowMut};
use std::collections::HashMap;
struct MyString {
data: String,
}
impl Borrow<str> for MyString {
fn borrow(&self) -> &str {
&self.data
}
}
impl BorrowMut<str> for MyString {
fn borrow_mut(&mut self) -> &mut str {
&mut self.data
}
}
fn find_in_map<Q>(map: &HashMap<String, i32>, key: &Q) -> Option<&i32>
where
String: Borrow<Q>,
Q: std::hash::Hash + Eq + ?Sized,
{
map.get(key)
}
fn main() {
let mut map = HashMap::new();
map.insert("hello".to_string(), 42);
map.insert("world".to_string(), 100);
// Can search with &str instead of String
println!("Value: {:?}", find_in_map(&map, "hello"));
let mut my_string = MyString {
data: "Hello".to_string(),
};
let borrowed: &str = my_string.borrow();
println!("Borrowed: {}", borrowed);
}
9. From and Into Traits
These traits provide infallible type conversions.
struct Person {
name: String,
age: u32,
}
impl From<&str> for Person {
fn from(name: &str) -> Self {
Person {
name: name.to_string(),
age: 0,
}
}
}
impl From<(String, u32)> for Person {
fn from((name, age): (String, u32)) -> Self {
Person { name, age }
}
}
// Into is automatically implemented when From is implemented
fn create_person<T: Into<Person>>(input: T) -> Person {
input.into()
}
fn main() {
let person1 = Person::from("Alice");
let person2: Person = "Bob".into(); // Into trait
let person3 = create_person(("Charlie".to_string(), 25));
println!("Person 1: {}, age {}", person1.name, person1.age);
println!("Person 2: {}, age {}", person2.name, person2.age);
println!("Person 3: {}, age {}", person3.name, person3.age);
}
10. TryFrom and TryInto Traits
These traits provide fallible type conversions.
use std::convert::{TryFrom, TryInto};
#[derive(Debug)]
struct PositiveNumber(u32);
#[derive(Debug)]
enum ConversionError {
Negative,
TooLarge,
}
impl TryFrom<i32> for PositiveNumber {
type Error = ConversionError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value < 0 {
Err(ConversionError::Negative)
} else if value > 1000 {
Err(ConversionError::TooLarge)
} else {
Ok(PositiveNumber(value as u32))
}
}
}
fn main() {
let valid: Result<PositiveNumber, _> = 42.try_into();
let negative: Result<PositiveNumber, _> = (-5).try_into();
let too_large: Result<PositiveNumber, _> = 2000.try_into();
println!("Valid: {:?}", valid);
println!("Negative: {:?}", negative);
println!("Too large: {:?}", too_large);
match PositiveNumber::try_from(100) {
Ok(num) => println!("Created positive number: {:?}", num),
Err(e) => println!("Conversion failed: {:?}", e),
}
}
11. ToOwned Trait
ToOwned
creates owned data from borrowed data, generalizing Clone
.
use std::borrow::{Cow, ToOwned};
fn process_string_data<'a>(input: &'a str) -> Cow<'a, str> {
if input.contains("URGENT") {
// Need to modify, so create owned data
Cow::Owned(format!("[PRIORITY] {}", input))
} else {
// Can use borrowed data
Cow::Borrowed(input)
}
}
fn main() {
let message1 = "Regular message";
let message2 = "URGENT: System failure";
let result1 = process_string_data(message1);
let result2 = process_string_data(message2);
println!("Result 1: {}", result1);
println!("Result 2: {}", result2);
// ToOwned in action
let slice: &[i32] = &[1, 2, 3];
let owned: Vec<i32> = slice.to_owned();
println!("Owned vector: {:?}", owned);
}
12. Borrow and ToOwned at Work: The Humble Cow
Cow
(Clone on Write) is a smart pointer that can hold either borrowed or owned data.
use std::borrow::Cow;
fn process_text<'a>(input: &'a str, make_uppercase: bool) -> Cow<'a, str> {
if make_uppercase {
// Need to modify, return owned data
Cow::Owned(input.to_uppercase())
} else {
// No modification needed, return borrowed data
Cow::Borrowed(input)
}
}
fn normalize_path<'a>(path: &'a str) -> Cow<'a, str> {
if path.contains("//") {
// Need to fix double slashes, return owned
Cow::Owned(path.replace("//", "/"))
} else {
// Path is already normalized, return borrowed
Cow::Borrowed(path)
}
}
struct TextProcessor {
prefix: String,
}
impl TextProcessor {
fn process<'a>(&self, text: &'a str, add_prefix: bool) -> Cow<'a, str> {
if add_prefix {
Cow::Owned(format!("{}: {}", self.prefix, text))
} else {
Cow::Borrowed(text)
}
}
}
fn main() {
// Example 1: Conditional processing
let text = "hello world";
let result1 = process_text(text, false); // Borrowed
let result2 = process_text(text, true); // Owned
println!("Borrowed: {}", result1);
println!("Owned: {}", result2);
// Example 2: Path normalization
let good_path = "/usr/local/bin";
let bad_path = "/usr//local//bin";
let normalized1 = normalize_path(good_path); // Borrowed
let normalized2 = normalize_path(bad_path); // Owned
println!("Good path: {}", normalized1);
println!("Fixed path: {}", normalized2);
// Example 3: Using Cow in structs
let processor = TextProcessor {
prefix: "LOG".to_string(),
};
let message = "System started";
let without_prefix = processor.process(message, false);
let with_prefix = processor.process(message, true);
println!("Without prefix: {}", without_prefix);
println!("With prefix: {}", with_prefix);
// Converting Cow to owned when needed
let cow_borrowed = Cow::Borrowed("borrowed");
let cow_owned = Cow::Owned("owned".to_string());
let owned1: String = cow_borrowed.into_owned();
let owned2: String = cow_owned.into_owned();
println!("Converted to owned: {}, {}", owned1, owned2);
}
Summary
These utility traits form the backbone of Rust's type system:
- Drop: Custom cleanup logic
- Sized: Compile-time size information
- Clone: Deep copying
- Copy: Bitwise copying
- Deref/DerefMut: Custom dereferencing
- Default: Sensible default values
- AsRef/AsMut: Cheap reference conversions
- Borrow/BorrowMut: Reference conversions with equivalence guarantees
- From/Into: Infallible conversions
- TryFrom/TryInto: Fallible conversions
- ToOwned: Creating owned data from borrowed data
- Cow: Efficient clone-on-write semantics
Understanding these traits is crucial for writing idiomatic Rust code and building efficient, generic APIs 12.