Rust is a systems programming language that is designed to catch at compile time many types of type confusion, memory allocation, and concurrency race condition bugs that are difficult to find solely through runtime testing and debugging. Once mastered, it is a productivity tool - allowing you to spend more time on improving the design or functionality of your systems, and less time on debugging.
Another important aspect of Rust is that it incurs little overhead compared to native C programming. Like Java and Go, Rust is type safe - the compiler or runtime will catch and prevent misuse of memory. However, unlike these other languages, there is no background garbage collector. Rather, the programmer adds information to the program to allow the compiler to know exactly when data can be reclaimed. In addition, data structures are formatted in memory in a C-like style. Of course, Rust also comes with a suite of generic data structures like maps. The combination of no garbage collection and visible memory formatting means that it is reasonable to use Rust for systems code where predictable, high performance is essential.
Rust achieves this through an idea called “ownership safety”. The idea is that every byte of allocated memory (on the heap or the stack) must have a compiler-checkable lifetime -- when it is live and when it can be reclaimed. This includes anytime we take a reference or pointer to an allocated item, anytime we store a reference of one data structure in another data structure, or anytime we pass a reference to a procedure. The Rust compiler understands both mutable and immutable data structures, as well as read-only versus read-write references. This does require some extra care when programming. In practice however, if you want your C code to be free of memory and concurrency errors, that extra care is essential. All the Rust compiler is doing is providing an automated check that you are using a coding discipline that will eliminate whole classes of bugs.
There are two flavors of Rust: safe and unsafe. Safe Rust enforces type and ownership safety. Unsafe Rust allows the programmer to evade some of Rust’s restrictions. We’re purists, so we’ll be using Safe Rust.
Rust is in many ways similar to C and Java, but Rust is not an object-oriented language. We will assume you are already familiar with those languages. With the exception of ownership safety, most things in Rust have direct analogues to C and Java, and so we’ll start by giving you a mapping of concepts between the languages. However, this is meant as only a very brief introduction to allow you to get started. We are not going to try to cover every topic, and where appropriate, we’ll just include pointers to an explanation. If you would like to explore more, we recommend Rust’s official tutorial. As a warning, it is long, in part because it does not assume any specific prior programming experience (e.g., so it needs to explain an enum to those who haven’t seen one before). However, it is the single best resource for rigorous Rust learning. Other resources are also available, like “Rust by Example” (recommend,) “Rust for Java/C++ programmers,” or “30min Rust.”
The majority of the time, the Rust compiler will be your best teacher. That is, try things out, and the compiler will tell you when you need to add clarity. With Rust's super powerful compiler and some patience with the verboseness of Rust compile-time error messages, you will solve most of the problems and learn a lot from the compiler!
Note that all “FYI” parts are beyond the scope. They are only fun to know :)
Follow Chapter 1 in Rust’s official guide: Getting Started.
Let’s go through concepts with a demo.
Java:
import java.util.HashSet;
import java.util.Set;
class Student {
public String name;
private int idNumber;
private Set<String> classTaken;
public Student(String name, int idNumber) {
this.name = name;
this.idNumber = idNumber;
this.classTaken = new HashSet<>();
}
public void sayHello() {
System.out.println("My name is " + name + " and my student id number is " + idNumber);
System.out.println("I have taken " + String.join(", ", classTaken));
}
public void takeClass(String className) {
classTaken.add(className);
}
public static void main(String[] args) {
Student student = new Student("haha", 1234);
student.takeClass("CSE 143");
student.takeClass("CSE 453");
student.sayHello();
}
}Rust:
// note “use” instead of “import”
use itertools::Itertools;
use std::collections::HashSet;
// Rust has C-like structures rather than Java-like classes, and structs can be public or private
// type information comes after the variable name, commas as separators between struct fields
pub struct Student {
pub name: String, // public field
id_number: i32, // private field by default
class_taken: HashSet<String>,
}
// this defines a set of methods on the object
impl Student {
fn new(name: String, id: i32) -> Student { // the constructor with two arguments
Student { //allocates a Student with fields and returns the result
name, // this is a shortcut for `name: name`
id_number: id,
class_taken: HashSet::new(),
}
}
fn say_hello(&self) {
println!(
"My name is {} and my student id number is {}",
self.name, self.id_number
);
println!("I have taken {}", self.class_taken.iter().join(", "));
}
fn take_class(&mut self, class_name: String) {
self.class_taken.insert(class_name);
}
}
// like C, Rust expects a main function in every program
fn main() {
// or explicitly declare the type: `let mut student: Student = ...`
let mut student = Student::new(String::from("haha"), 1234);
student.take_class(String::from("CSE 143"));
student.take_class(String::from("CSE 453"));
student.say_hello();
}output:
My name is haha and my student id number is 1234
I have taken CSE 453, CSE 143
The small demo covers many key concepts in Rust, let’s go over them one by one.
-
Structs vs. Objects
Rust is not an object-oriented language but, as with C, you can use it in an object-oriented manner. Rust does not have objects. Instead, Rust has structs similar to those in C. However, in Rust, you can define methods for structs.
-
usevs.importThese are similar, except that Rust uses
::to separate namespaces. -
Variable Declaration
Both languages are statically typed – every value has a type that is checked at compile time. Java places types before variable names while Rust puts them afterwards. However, the most significant difference is that Rust does not require you to declare the type if the compiler can infer the type instead (e.g., from how the variable is used). You may find examples of mandatory and omittable type declarations in the demo. Types are always needed for struct and function definitions.
-
Mutable vs. Immutable
As you may notice, there is
mutas part of some declarations. In Rust, variables are immutable by default. If you want to be able to modify a struct, like adding a new element to a list, you must declare it asmut. (FYI, the above “immutable” is indeed “exteriorly immutable”. A type has interior mutability if its internal state can be changed through a shared reference to it. We’ll talk about it later.) Mutability is important for ownership safety - an immutable object can be silently copied by the compiler when needed for a function call, but (obviously) there’s an important difference between a mutable object and its copy. -
Public/Private
Anything that can be either public or private (e.g. structs, enums, functions, modules, etc.) is private by default. E.g.,
struct Hahais private butpub struct Hahais public. -
Return Statement
Java mandates the keyword
returnbut Rust does not. Rather, Rust takes the final expression of the function as its return value. As an expression (x), and not a statement (return x;), you do not append a semicolon. You can use an explicit return to return from the middle (or end) of a function; that makes it a statement, and so it takes a semicolon. -
Function Declaration
Rust’s function declaration looks very similar to that of Python.
- Parameter declarations requires types.
- Return type is after
-> self/mut self/&self/&mut selfFor functions defined on structs (similar to object methods, though “object method” is not a Rust terminology),selfmust be the first argument in the definition, similar to Python. The&is similar a pointer in C. Rust uses*to dereference too, though there are cases you don’t need to explicitly dereference (FYI, like when passing a pointer to a function call. I.e., Deref Coercions.) If you’re not familiar with Python, all these fourself’s are the instance itself.
-
println!– the most frequently used macro Rust’s println is similar to Python’s.format(). If you’re not familiar with python, each{}is like%s,%d, or so in C, except Rust knows the type, so{}is for any type. Also, Rust’s macro is very similar to C’s macro, and macros have a exclamation mark as a suffix. -
String vs. String Literal
String in Rust is a struct, yet string literals are not. String literals have type
&strand, like in C, are stored in the static region of the program executable, while String’s are dynamically allocated on the heap. To convert a&strto a String, it’s recommended to useString::from("a literal”). (FYI, there are other ways like.to_string()or.to_owned.) Note that Strings and string literals are stored as unicode, so it much differs from C: strings can't be indexed and you need to use an explicit iterator (e.g..chars(),.bytes()) to walk through a String. -
Object Constructor vs. Struct Initialization
Rust does not officially have constructors, even though you may create a method
new()as if it is a constructor. The struct initialization is like that in C.
The following is mostly a digest of Rust by Example with some of our words. It serves as a brief introduction (or a refresher when you want to check back) to relatively unique topics in Rust that cannot be found in Java/C. To learn more, follow the links under each topic.
The memory management system in Rust is the one thing that most differentiates Rust from Java or C. Memory is governed by a new concept (FYI, well, not so new if you know RAII) — ownership. Ownership is also leveraged for concurrency safety which we discuss below. Unlike Java’s automated memory management, there is no garbage collector - the programmer is in full control as to when allocated data can be reclaimed. Unlike C, however, the compiler does the work - the programmer never calls malloc or free. Thus, the ownership system is like malloc/free with a memory safeguard done by the compiler. This is key to Rust being “blazingly fast.”
There are two ways to learn this topic:
- If you want a comprehensive understanding of ownership and lifetime (recommended), please check out the official book Ownership and Lifetime.
- If you are familiar with RAII or want to get hands-on faster, check out Rust by Example: Scoping Rules.
Rust traits are similar to interfaces in Java. Say we have a trait
pub trait Summary {
fn summarize(&self) -> String;
}We may impl it for structs, like we can implements an interface for a class in Java.
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}The compiler is capable of providing basic implementations for some traits via the #[derive] attribute. These traits can still be manually implemented if more complex behavior is required. For example,
#[derive(Debug)]
struct Inches(i32);Debug is a trait to format a value using the {:?} formatter.
The Iterator trait is used to implement iterators over collections such as arrays, vectors, hash maps, and so on.
The trait only requires a single method called next which returns the next element if it exists. Iterator is implemented for arrays, ranges, and most collections in the standard library. For custom data types, it must be defined manually in an impl block.
As a point of convenience for common situations, the for construct turns some collections into iterators using the .into_iter() method.
struct Fibonacci {
curr: u32,
next: u32,
}
// Implement `Iterator` for `Fibonacci`.
// The `Iterator` trait only requires a method to be defined for the `next` element.
impl Iterator for Fibonacci {
// We can refer to this type using Self::Item
type Item = u32;
// Here, we define the sequence using `.curr` and `.next`.
// The return type is `Option<T>`:
// * When the `Iterator` is finished, `None` is returned.
// * Otherwise, the next value is wrapped in `Some` and returned.
// We use Self::Item in the return type, so we can change
// the type without having to update the function signatures.
fn next(&mut self) -> Option<Self::Item> {
let new_next = self.curr + self.next;
self.curr = self.next;
self.next = new_next;
// Since there's no endpoint to a Fibonacci sequence, the `Iterator`
// will never return `None`, and `Some` is always returned.
Some(self.curr)
}
}
// Returns a Fibonacci sequence generator
fn fibonacci() -> Fibonacci {
Fibonacci { curr: 0, next: 1 }
}
fn main() {
// `0..3` is an `Iterator` that generates: 0, 1, and 2.
let mut sequence = 0..3;
println!("Four consecutive `next` calls on 0..3");
println!("> {:?}", sequence.next());
println!("> {:?}", sequence.next());
println!("> {:?}", sequence.next());
println!("> {:?}", sequence.next());
// `for` works through an `Iterator` until it returns `None`.
// Each `Some` value is unwrapped and bound to a variable (here, `i`).
println!("Iterate through 0..3 using `for`");
for i in 0..3 {
println!("> {}", i);
}
// The `take(n)` method reduces an `Iterator` to its first `n` terms.
println!("The first four terms of the Fibonacci sequence are: ");
for i in fibonacci().take(4) {
println!("> {}", i);
}
// The `skip(n)` method shortens an `Iterator` by dropping its first `n` terms.
println!("The next four terms of the Fibonacci sequence are: ");
for i in fibonacci().skip(4).take(4) {
println!("> {}", i);
}
let array = [1u32, 3, 3, 7];
// The `iter` method produces an `Iterator` over an array/slice.
println!("Iterate the following array {:?}", &array);
for i in array.iter() {
println!("> {}", i);
}
}When dealing with resources, the default behavior is to move them during assignments or function calls. However, sometimes we need to make a copy of the resource as well.
The Clone trait helps us do exactly this. Most commonly, we use the .clone() method defined by the Clone trait.
// A unit struct without resources
#[derive(Debug, Clone, Copy)]
struct Unit;
// A tuple struct with resources that implements the `Clone` trait
#[derive(Clone, Debug)]
struct Pair(Box<i32>, Box<i32>);To learn more, check out Rust By Example: Trait.
Enums in Rust are like enums in Java
enum Work {
Civilian,
Soldier,
}and we use them like in Java, though a match must cover all possible cases in Rust.
let work = Work::Civilian;
match work {
Work::Civilian => println!("Civilians work!"),
Work::Soldier => println!("Soldiers fight!"),
}Notice that enums may store values.
// Create an `enum` to classify a web event. Note how both
// names and type information together specify the variant:
// `PageLoad != PageUnload` and `KeyPress(char) != Paste(String)`.
// Each is different and independent.
enum WebEvent {
// An `enum` may either be `unit-like`,
PageLoad,
PageUnload,
// like tuple structs,
KeyPress(char),
Paste(String),
// or c-like structures.
Click { x: i64, y: i64 },
}
// A function which takes a `WebEvent` enum as an argument and
// returns nothing.
fn inspect(event: WebEvent) {
match event {
WebEvent::PageLoad => println!("page loaded"),
WebEvent::PageUnload => println!("page unloaded"),
// Destructure `c` from inside the `enum`.
WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
WebEvent::Paste(s) => println!("pasted \"{}\".", s),
// Destructure `Click` into `x` and `y`.
WebEvent::Click { x, y } => {
println!("clicked at x={}, y={}.", x, y);
},
}
}
fn main() {
let pressed = WebEvent::KeyPress('x');
// `to_owned()` creates an owned `String` from a string slice.
let pasted = WebEvent::Paste("my text".to_owned());
let click = WebEvent::Click { x: 20, y: 80 };
let load = WebEvent::PageLoad;
let unload = WebEvent::PageUnload;
inspect(pressed);
inspect(pasted);
inspect(click);
inspect(load);
inspect(unload);
}For more information, see Rust by Examples: Enums.
A crate is a compilation unit in Rust. Whenever rustc some_file.rs is called, some_file.rs is treated as the crate file. Like in C, the crate file is linked/inserted to where mods inside the file is used.
For examples, check out the short chapter Rust by Example: Crates.
Calling panic() prints an error message and terminates the program. This is the simplest way of handling an error.
fn drink(beverage: &str) {
// You shouldn't drink too much sugary beverages.
if beverage == "lemonade" { panic!("AAAaaaaa!!!!"); }
println!("Some refreshing {} is all I need.", beverage);
}
fn main() {
drink("water");
drink("lemonade");
}Rust doesn’t allow unhandled null values. To enforce handling null values, an enum called Option<T> in the std library is used when absence is a possibility. It manifests itself as one of two "options":
Some(T): An element of typeTwas found.None: No element was found.
unwrap, when applied to an Option, returns the real value in Some if it’s a Some and panics if it’s a None.
// The adult has seen it all, and can handle any drink well.
// All drinks are handled explicitly using `match`.
fn give_adult(drink: Option<&str>) {
// Specify a course of action for each case.
match drink {
Some("lemonade") => println!("Yuck! Too sugary."),
Some(inner) => println!("{}? How nice.", inner),
None => println!("No drink? Oh well."),
}
}
// Others will `panic` before drinking sugary drinks.
// All drinks are handled implicitly using `unwrap`.
fn drink(drink: Option<&str>) {
// `unwrap` returns a `panic` when it receives a `None`.
let inside = drink.unwrap();
if inside == "lemonade" { panic!("AAAaaaaa!!!!"); }
println!("I love {}s!!!!!", inside);
}Result<T, E> is similar to Option<T> except that an error of type E is in place of None. We can also apply unwrap to Result.
fn multiply(first_number_str: &str, second_number_str: &str) -> i32 {
// Let's try using `unwrap()` to get the number out. Will it bite us?
let first_number = first_number_str.parse::<i32>().unwrap();
let second_number = second_number_str.parse::<i32>().unwrap();
first_number * second_number
}
fn main() {
let twenty = multiply("10", "2");
println!("double is {}", twenty);
let tt = multiply("t", "2");
println!("double is {}", tt);
}? is almost exactly equivalent to an unwrap which returns instead of panicking on Errs. It’s a convenient way to fail-fast and reduce code verboseness.
fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
let first_number = first_number_str.parse::<i32>()?;
let second_number = second_number_str.parse::<i32>()?;
Ok(first_number * second_number)
}For more information, like iterating over Results, check out Rust by Example: Error Handling.
Most unit tests go into a tests mod with the #[cfg(test)] attribute. Test functions are marked with the #[test] attribute. Unit tests usually live in the same file as the mod to be tested.
Tests fail when something in the test function panics. There are some helper macros:
assert!(expression)- panics if expression evaluates tofalse.assert_eq!(left, right)andassert_ne!(left, right)- testing left and right expressions for equality and inequality respectively.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// This is a really bad adding function, its purpose is to fail in this
// example.
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
a - b
}
#[cfg(test)]
mod tests {
// Note this useful idiom: importing names from outer (for mod tests) scope.
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_bad_add() {
// This assert would fire and test will fail.
// Please note, that private functions can be tested too!
assert_eq!(bad_add(1, 2), 3);
}
}To run all test, type cargo test in your terminal. To run a particular test, for example test_add, use cargo test test_add.
Integration tests usually live in a separate file like tests/integration_test.ts.
Cargo looks for integration tests in the tests directory next to src.
File src/lib.rs:
// Define this in a crate called `adder`.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}File with test: tests/integration_test.rs:
#[test]
fn test_add() {
assert_eq!(adder::add(3, 2), 5);
}Integration tests are also run by cargo test.
Closures are functions that can capture the enclosing environment. For example, a closure that captures the x variable:
|val| val + xThe syntax and capabilities of closures make them very convenient for on the fly usage. Calling a closure is exactly like calling a function. Some characteristics of closures include:
- using
||instead of()around input variables. - optional body delimination (
{}) for a single expression (mandatory otherwise). - the ability to capture the outer environment variables.
fn main() {
// Increment via closures and functions.
fn function(i: i32) -> i32 { i + 1 }
// Closures are anonymous, here we are binding them to references
// Annotation is identical to function annotation but is optional
// as are the `{}` wrapping the body. These nameless functions
// are assigned to appropriately named variables.
let closure_annotated = |i: i32| -> i32 { i + 1 };
let closure_inferred = |i | i + 1 ;
let i = 1;
// Call the function and closures.
println!("function: {}", function(i));
println!("closure_annotated: {}", closure_annotated(i));
println!("closure_inferred: {}", closure_inferred(i));
// A closure taking no arguments which returns an `i32`.
// The return type is inferred.
let one = || 1;
println!("closure returning one: {}", one());
}All values in Rust are stack allocated by default. Values can be boxed (allocated on the heap) by creating a Box<T>. A box is a smart pointer to a heap allocated value of type T. When a box goes out of scope, its destructor is called, the inner object is destroyed, and the memory on the heap is freed.
Boxed values can be dereferenced using the * operator; this removes one layer of indirection.
#[derive(Debug, Clone, Copy)]
struct Point {
x: f64,
y: f64,
}
let boxed_point: Box<Point> = Box::new(Point { x: 0.0, y: 0.0 })
let unboxed_point: Point = *boxed_point;Like reference counted smart pointers in C++, when multiple ownership is needed, Rc(Reference Counting) can be used. Rc keeps track of the number of the references which means the number of owners of the value wrapped inside an Rc.
Reference count of an Rc increases by 1 whenever an Rc is cloned, and decreases by 1 whenever one cloned Rc is dropped out of the scope. When an Rc's reference count becomes zero, which means there are no owners remaining, both the Rc and the value are all dropped.
Cloning an Rc never performs a deep copy. Cloning creates just another pointer to the wrapped value, and increments the count.
use std::rc::Rc;
fn main() {
let rc_examples = "Rc examples".to_string();
{
println!("--- rc_a is created ---");
let rc_a: Rc<String> = Rc::new(rc_examples);
println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
{
println!("--- rc_a is cloned to rc_b ---");
let rc_b: Rc<String> = Rc::clone(&rc_a);
println!("Reference Count of rc_b: {}", Rc::strong_count(&rc_b));
println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
// Two `Rc`s are equal if their inner values are equal
println!("rc_a and rc_b are equal: {}", rc_a.eq(&rc_b));
// We can use methods of a value directly
println!("Length of the value inside rc_a: {}", rc_a.len());
println!("Value of rc_b: {}", rc_b);
println!("--- rc_b is dropped out of scope ---");
}
println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));
println!("--- rc_a is dropped out of scope ---");
}
// Error! `rc_examples` already moved into `rc_a`
// And when `rc_a` is dropped, `rc_examples` is dropped together
// println!("rc_examples: {}", rc_examples);
// TODO ^ Try uncommenting this line
}Output
Reference Count of rc_a: 1
--- rc_a is cloned to rc_b ---
Reference Count of rc_b: 2
Reference Count of rc_a: 2
rc_a and rc_b are equal: true
Length of the value inside rc_a: 11
Value of rc_b: Rc examples
--- rc_b is dropped out of scope ---
Reference Count of rc_a: 1
--- rc_a is dropped out of scope ---
When shared ownership between threads is needed, Arc(Atomic Reference Counted) can be used. This struct, via the Clone implementation can create a reference pointer for the location of a value in the memory heap while increasing the reference counter. As it shares ownership between threads, when the last reference pointer to a value is out of scope, the variable is dropped.
fn main() {
use std::sync::Arc;
use std::thread;
// This variable declaration is where its value is specified.
let apple = Arc::new("the same apple");
for _ in 0..10 {
// Here there is no value specification as it is a pointer to a reference
// in the memory heap.
let apple = Arc::clone(&apple);
thread::spawn(move || { // spawn a thread and move (transfer ownsership) the closure to the new thread
// As Arc was used, threads can be spawned using the value allocated
// in the Arc variable pointer's location.
println!("{:?}", apple);
});
}
}RefCell<T> is another way to “wrap” a value. Rc<T>is similar to RefCell<T>, but Rc<T> is checked at compile time while RefCell<T> is checked at runtime. That means the former raises compile errors while the latter panics during runtime if errors occur.
The common ways to get value stored in RefCell<T> are via .borrow() or .borrow_mut() where the former one provides an immutable reference to the value inside while the latter provides a mutable reference.
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}We highly recommend going through the Rust Book: RefCell as RefCell<T> is important yet relatively hard to grasp with a short intro.
Rc<T>enables multiple owners of the same data;Box<T>andRefCell<T>have single owners.Box<T>allows immutable or mutable borrows checked at compile time;Rc<T>allows only immutable borrows checked at compile time;RefCell<T>allows immutable or mutable borrows checked at runtime.- Because
RefCell<T>allows mutable borrows checked at runtime, you can mutate the value inside theRefCell<T>even when theRefCell<T>is immutable.
Generics in Rust are like those in Java. There are many places where generics may play a role. For a comprehensive list, please check Rust By Example: Generics. Here lists some basic use cases:
-
Function
fn foo<T>(arg: T) { ... }
so
argis of typeT -
implstruct S; // Concrete type `S` struct GenericVal<T>(T); // Generic type `GenericVal` // impl of GenericVal where we explicitly specify type parameters: impl GenericVal<f32> {} // Specify `f32` impl GenericVal<S> {} // Specify `S` as defined above // `<T>` Must precede the type to remain generic impl<T> GenericVal<T> {}
-
Trait
Of course
traits can also be generic. Here we define one which reimplements theDroptraitas a generic method todropitself and an input.// Non-copyable types. struct Empty; struct Null; // A trait generic over `T`. trait DoubleDrop<T> { // Define a method on the caller type which takes an // additional single parameter `T` and does nothing with it. fn double_drop(self, _: T); } // Implement `DoubleDrop<T>` for any generic parameter `T` and // caller `U`. impl<T, U> DoubleDrop<T> for U { // This method takes ownership of both passed arguments, // deallocating both. fn double_drop(self, _: T) {} } fn main() { let empty = Empty; let null = Null; // Deallocate `empty` and `null`. empty.double_drop(null); //empty; //null; // ^ TODO: Try uncommenting these lines. }
cargo is the official Rust package management tool. You don’t need to be a master of cargo for this course. Following lab specs should suffice, but if you are curious, Rust by Example: Cargo gives a concise introduction, so please read it. There are also great third-party libraries that may help implementations. Here is a list of some popular ones.
- anyhow (apps)
- thiserror (libs)
- serde
- clap
- structopt
- http-types
Concurrency is a major topic for 453 and we don’t want you to miss any detail. Please check out Rust Book: Concurrency.
Why I cannot impl certain traits?
It’s subject to the restriction that you can't implement a foreign trait on a foreign type.
- self trait on foreign type (legal)
- foreign trait on self type (legal)
- foreign trait on foreign type (ILLEGAL)
Some examples and descriptions are from Rust by Example and Rust Book.