Advanced Level
Advanced Types
Chapter 20: Advanced Types ๐งช
Explore sophisticated type system features like type aliases, never type, dynamically sized types, and more.
Type Aliases
Creating Type Synonyms
Type aliases create synonyms for existing types without introducing new types:
type Kilometers = i32;
fn main() {
let x: i32 = 5;
let y: Kilometers = 5;
// These are the same type internally
println!("x + y = {}", x + y);
}Complex Type Aliases
use std::collections::HashMap;
use std::io::Result;
type Thunk = Box<dyn Fn() + Send + 'static>;
type Result<T> = std::result::Result<T, std::io::Error>;
type Tables = HashMap<String, HashMap<String, String>>;
fn main() {
let f: Thunk = Box::new(|| println!("hi"));
let r: Result<i32> = Ok(42);
let t: Tables = HashMap::new();
}The Never Type
Representing Unreachable Code
The never type ! represents values that never return:
fn bar() -> ! {
panic!("This call never returns!");
}
fn some_function() -> Result<i32, String> {
// This function returns a Result, but we can use ! to handle errors
let result = match risky_operation() {
Ok(value) => value,
Err(_) => bar(), // Never returns, so this branch never continues
};
// This code is unreachable after bar(), but compiler knows it's safe
Ok(result)
}
fn risky_operation() -> Result<i32, String> {
Err(String::from("Error occurred"))
}Practical Use Cases
fn main() {
let guess = loop {
let input = get_input();
match input.parse::<u32>() {
Ok(num) => break num,
Err(_) => continue, // continue has type !
}
};
println!("You guessed: {}", guess);
}
fn get_input() -> String {
String::from("42")
}Dynamically Sized Types
Sized and Unsized Types
Most types in Rust have a known size at compile time, but some don't:
// This won't compile - unsized types can't be stored directly
// let s1: str = "Hello there!";
// let s2: str = "How's it going?";
// This works - storing references to unsized types
let s1: &str = "Hello there!";
let s2: &str = "How's it going?";Using Box with DSTs
fn main() {
// This won't compile
// let s: str = "Hello";
// But this works
let s: Box<str> = "Hello".to_owned().into_boxed_str();
println!("{}", s);
}Working with DSTs
Trait Objects
trait Draw {
fn draw(&self);
}
struct Circle {
radius: u32,
}
struct Rectangle {
width: u32,
height: u32,
}
impl Draw for Circle {
fn draw(&self) {
println!("Drawing circle with radius {}", self.radius);
}
}
impl Draw for Rectangle {
fn draw(&self) {
println!("Drawing rectangle {}x{}", self.width, self.height);
}
}
fn render_component(component: &dyn Draw) {
component.draw();
}
fn main() {
let circle = Circle { radius: 5 };
let rectangle = Rectangle { width: 10, height: 20 };
render_component(&circle);
render_component(&rectangle);
}Slices
fn main() {
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[..]; // This is a DST
println!("Slice length: {}", slice.len());
for item in slice {
println!("Item: {}", item);
}
}Type Conversion
Using Into and From Traits
#[derive(Debug)]
struct Number {
value: i32,
}
impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}
fn main() {
let num = Number::from(30);
println!("{:?}", num);
let int = 5;
let num: Number = int.into();
println!("{:?}", num);
}TryFrom and TryInto
use std::convert::TryFrom;
use std::convert::TryInto;
#[derive(Debug, PartialEq)]
struct EvenNumber(i32);
impl TryFrom<i32> for EvenNumber {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value % 2 == 0 {
Ok(EvenNumber(value))
} else {
Err(())
}
}
}
fn main() {
assert_eq!(EvenNumber::try_from(4), Ok(EvenNumber(4)));
assert_eq!(EvenNumber::try_from(5), Err(()));
let result: Result<EvenNumber, ()> = 8i32.try_into();
assert_eq!(result, Ok(EvenNumber(8)));
let result: Result<EvenNumber, ()> = 7i32.try_into();
assert_eq!(result, Err(()));
}Practical Examples
Example 1: Custom Error Type with Never Type
use std::fmt;
#[derive(Debug)]
struct Error;
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "An error occurred")
}
}
impl std::error::Error for Error {}
fn might_panic() -> ! {
panic!("This function never returns!");
}
fn handle_error<T>(result: Result<T, Error>) -> T {
match result {
Ok(value) => value,
Err(_) => might_panic(), // Never returns, so this is safe
}
}
fn main() {
let success: Result<i32, Error> = Ok(42);
let value = handle_error(success);
println!("Value: {}", value);
// This would panic:
// let failure: Result<i32, Error> = Err(Error);
// let value = handle_error(failure);
}Example 2: Advanced Type System for Configuration
use std::collections::HashMap;
use std::convert::TryFrom;
// Type aliases for clarity
type ConfigKey = String;
type ConfigValue = String;
type Configuration = HashMap<ConfigKey, ConfigValue>;
// Never type for error handling
fn config_error(msg: &str) -> ! {
eprintln!("Configuration Error: {}", msg);
std::process::exit(1);
}
// Custom types with conversions
#[derive(Debug)]
struct Port(u16);
impl TryFrom<u16> for Port {
type Error = &'static str;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value > 0 && value < 65536 {
Ok(Port(value))
} else {
Err("Port must be between 1 and 65535")
}
}
}
#[derive(Debug)]
struct DatabaseUrl(String);
impl TryFrom<String> for DatabaseUrl {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.starts_with("postgresql://") || value.starts_with("mysql://") {
Ok(DatabaseUrl(value))
} else {
Err("Database URL must start with postgresql:// or mysql://")
}
}
}
fn get_config_value(config: &Configuration, key: &str) -> &str {
config.get(key).map(|s| s.as_str()).unwrap_or_else(|| {
config_error(&format!("Missing required configuration key: {}", key))
})
}
fn main() {
let mut config = Configuration::new();
config.insert("database_url".to_string(), "postgresql://localhost:5432/mydb".to_string());
config.insert("port".to_string(), "8080".to_string());
let db_url_str = get_config_value(&config, "database_url");
let db_url = DatabaseUrl::try_from(db_url_str.to_string()).unwrap_or_else(|e| {
config_error(e)
});
let port_str = get_config_value(&config, "port");
let port_num: u16 = port_str.parse().unwrap_or_else(|_| {
config_error("Port must be a valid number")
});
let port = Port::try_from(port_num).unwrap_or_else(|e| {
config_error(e)
});
println!("Database URL: {:?}", db_url);
println!("Port: {:?}", port);
}Common Mistakes
โ Forgetting Sized Requirement
// This won't compile
fn generic<T>(t: T) {
// ...
}
// Structs also require sized types by default
struct MyStruct<T> {
value: T, // Error if T is unsized
}โ Properly Handling Unsized Types
// Use references or smart pointers for unsized types
fn generic<T: ?Sized>(t: &T) {
// ...
}
// Use Box for storing unsized types
struct MyStruct<T: ?Sized> {
value: Box<T>,
}โ Incorrect Type Conversion Implementation
struct MyType(i32);
// This implementation is incomplete
impl From<i32> for MyType {
fn from(item: i32) -> Self {
// Missing validation or transformation
MyType(item)
}
}โ Proper Type Conversion with Validation
#[derive(Debug)]
struct PositiveNumber(u32);
impl TryFrom<i32> for PositiveNumber {
type Error = &'static str;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value > 0 {
Ok(PositiveNumber(value as u32))
} else {
Err("Value must be positive")
}
}
}Key Takeaways
- โ Type aliases create synonyms for existing types without introducing new types
- โ
The never type
!represents values that never return - โ
Dynamically sized types (DSTs) like
strand[T]don't have known size at compile time - โ
Use references
&stror smart pointersBox<str>to work with DSTs - โ
Trait objects
&dyn Traitare a common use of DSTs - โ
FromandIntotraits provide type conversion - โ
TryFromandTryIntotraits provide fallible type conversion - โ Follow Rust naming conventions and idioms for advanced types
Ready for Chapter 21? โ Advanced Functions and Closures