A guide to Rust for Kotlin developers
This guide should help developers experienced with Kotlin quickly and easily learn the basics of Rust by comparing the major differences but also how the languages, by the nature of them both being modern languages, have quite a few similar features.
This book is a summary of the official Rust books, as well as forum threads, GitHub issues, and StackOverflow posts I've read and testing I've done. Please also read the following books for a much more complete understanding of Rust:
The official docs (often called the rust book or just the book in discussions) | Link |
A collection of examples for common patterns and tasks | Link |
Guide for Cargo (see also the Cargo chapter) | Link |
A lot of things you already know are going to be re-explained in this guide for two reasons:
- as a refresher and just to make your understanding is correct
- to help explain and draw out differences between the languages; Rust is designed so that the compiler can guarantee some level of correctness in regards to variable access and destruction, and as such it will not compile code it can't predict.
Some of the code samples have Ferris on them:
Ferris | Meaning |
---|---|
This code does not compile! | |
This code panics! | |
This code block contains unsafe code. | |
This code does not produce the desired behavior. |
(taken from the official book)
Basics
To start with Rust you'll the compiler, go to rustup.rs and follow the instructions.
Then I recommend installing and using Intellij CLion as you're already familiar with Intellij IDEs, otherwise you can use VSCode.
Command line basics
Rust is controlled by it's package manager cargo
To run your program execute cargo run
To test it's cargo test
Syntax differences
In Rust...
- lines have to end with a semicolon, except if it's an implicit return value (like in Kotlin lambdas).
- methods parameters can not be variadic, named or have default values.
- method overloading is not supported.
- the naming scheme is
snake_case
for methods, variables and files; andCamelCase
for Traits, Structs, Impls and Enums. - annotations are written like
#[Example]
instead of@Example
Macros
Rust uses macros a lot as they provide ways to shorten code, and provide variadic and optional parameters, and also overloaded methods. Macros are invoked by the macro name then an exclamation mark, i.e. example!
.
Macros can be used like functions example!()
but anything can follow the exclamation mark as macros are very powerful, for example:
println!("Hello World");
let list = vec![1, 2, 3];
let foo = dsl! {
init_with_default
config = bar
}
//At compile time this will invoke the example macro with Foo as the parameter
#[Example]
struct Foo {
}
The annotation macros will generate code for Structs like in Kotlin.
Macros can generate Rust code at compile time or run and return a result at runtime like a method.
Most programs don't need their own macros but will often use ones from std or crates.
Primitives
In Kotlin there are few commonly used primitive types:
Name | Bits | Type |
---|---|---|
Byte | 8 | Integer |
Int | 32 | Integer |
Long | 64 | Integer |
Float | 32 | Floating Point |
Double | 64 | Floating Point |
Char | 16 | UTF-16 Character |
Boolean | N/A | Boolean |
In Kotlin the Integer types are signed but there unsigned versions, i.e UByte.
Rust uses a prefix followed by the bit size to create a number type (e.g. i32 meaning a 32 bit signed integer), these are the prefixes:
Character | Type |
---|---|
i | Signed Integer |
u | Unsigned Integer |
f | Floating Point |
Floating points are 32 and 64 bit only but integers can be 8, 16, 32, 64, and 128 bits; there are also two architecture dependent sizes: isize
and usize
these are whatever the pointer size is for the CPU (for most new computers and phones that is 64 bits).
As an example the equivalent to a Kotlin Int
is i32
and a Double
is f64
.
usize
is important as it's the primary number type: it's used in array, list and string formatting indexing and lengths/sizes of objects.
Suffixes for literals exist in Rust like in Kotlin but rather being for some types, suffixes exist for all primitives and they are the name of type, e.g. 3_i32
or 10.3f32
, the suffixes are only necessary if the compiler can not infer the type or if you want to specify it; also floating point numbers can be written without the 0, e.g. 1.
Booleans are the same in both languages except it's bool
instead of Boolean
.
Characters are different though, char
in Rust is 4 bytes and can represent any Unicode scalar value. If you need to do text manipulation and need to handle any character outside of ASCII you will probably need to use these crates:
Name | Description |
---|---|
unicode-segmentation | Used to iterate over grapheme clusters |
unicode-normalization | Converts char + modifier into single character, this is vital if you need to compare Unicode strings |
See also the Strings chapter for more information about how unicode is handled in Rust.
Error Handling
Rust has a generic error interface: std::error::Error
, it's optionally may have a source error and/or backtrace. Instead of exceptions and try..catch the follow is used:
Kotlin
fun main() {
try {
openSocket()
println("Worked")
} catch (e: IOException) {
println("Error: " + e.message)
}
}
Rust
use std::error::Error as StdError; type Error = Box<dyn StdError>; fn main() { match open_socket() { Ok(()) => println!("Worked"), Err(e) => println!("Error: {}", e) } } fn open_socket() -> Result<(), Error> { Ok(()) }
You can call unwrap()
on a Result
to get the success value or crash the app. To avoid repetitive code you can use ?
which will immediately return the error:
fn unwrap_example() {
let foo = open().unwrap();
}
//the question mark operator is only valid in methods that return Result
fn better_example() -> Result<(), Error> {
let bar = open()?;
Ok(())
}
Some crates errors aren't compatible with each other and so have to be converted to something before the error can be returned:
fn main() -> Result<(), Error> {
let file = open_file()?; //Error type is IoError
let socket = open_socket()?; //Error type is NetError
Ok(())
}
This example wouldn't compile because the size in memory of a return type must be known at compile time and Error
is a trait (an interface
in Kotlin). So it must be wrapped like so Box<dyn Error>
, Box
moves the value to the heap and is essentially a pointer, dyn
just means the type is dynamically dispatched (i.e. a trait). (see https://doc.rust-lang.org/std/keyword.dyn.html)
- Create a wrapper type and implement it for all error types that you have to handle
- Use eyre
I highly recommend anyhow for all apps, it automatically handles all error types and reduces boilerplate:
use eyre::Result; //import eyre Result and Error instead of std as needed
fn main() -> Result<()> { //Result only has single parameter
Ok(())
}
Types and variables
Mutability
Because of the limitations imposed by the JVM in Kotlin variables are mutable or immutable sometimes based on type and sometimes by declaration. In Rust (and some other languages such as Swift) mutability is controlled via declaration:
fn main() { let foo: i32 = 1; //immutable let mut bar: i32 = 2; //mutable }
Immutable variables can't be referenced as mutable, so the following will not compile:
fn main() {
let foo: i32 = 1;
//This is invalid as the variable is immutable and so all
//references must be immutable as well
change_ref(&mut foo);
//This is fine as a copy of the value is passed into the
//method and only the copy is mutable
change_val(foo);
}
fn change_ref(value: &mut i32) {
*value += 1;
}
fn change_val(value: mut i32) {
value += 1;
}
Note that unlike in Kotlin lists (and any other objects and their properties) in Rust can't be changed if the variable isn't marked as mutable, so the following will not compile:
fn main() {
let list = vec![1, 2, 3];
list.push(4);
}
See also Interior mutability
Strings
In both Kotlin and Java, essentially, there is just one String type: String. Whether the text is hardcoded, from a file or user input the same class is used. Rust has two String types: String and str. A hardcoded string will be of type &'static str
and a string read from anywhere else maybe a String
or &str
depending on what the method returns. They can be converted between themselves, in most circumstances.
String
is a pointer to a string in heap with a capacity, this means it can grow and can be mutable. A &str
is a char array and so has a fixed length.
If you attempt to slice a string so it would cause a Unicode character to be broken Rust will panic. Use the .chars() method to access each character independently. So to get the length of a string in bytes you use foo.len() and to get the number of characters you use foo.chars().count().
Note that std Rust has limited supported for unicode and you may need to use crates to add missing features, this is because unicode changes regularly and the unicode table is quite large and has to be compiled into your program.
Name | Description |
---|---|
unicode-segmentation | Used to iterate over grapheme clusters |
unicode-normalization | Converts char + modifier into single character, this is vital if you need to compare Unicode strings |
Common types
Lists
Kotlin | Rust | |
---|---|---|
Type | List<T> , MutableList<T> , ArrayList<T> | Vec<T> |
Constructor | ArrayList(size) , ArrayList(collection) | Vec::new() , Vec::with_capacity(size) |
Shorthand | listOf(items...) , mutableListOf(items...) , arrayListOf(items...) | vec![size; default] , vec![items...] |
Maps
Kotlin | Rust | |
---|---|---|
Type | Map<K, V> , MutableMap<K, V> , HashMap<K, V> | HashMap<K, V> |
Constructor | HashMap(size) , HashMap(map) | HashMap::new() |
Shorthand | mapOf(items...) , mutableMapOf(items...) , hashMapOf(items...) | N/A [^1] |
Tuples
Kotlin | Rust | |
---|---|---|
Type | Pair<T1, T2> , Triple<T1, T2, T3> | (T1, T2, ...) |
Constructor | Pair(value1, value2) , Triple(value1, value2, value3) | N/A |
Shorthand | value1 to value2 | (value1, value2, ...) |
Arrays
Kotlin | Rust | |
---|---|---|
Type | Array<T> | [T] |
Constructor | Array(size, builder_method) | N/A |
Shorthand | arrayOf(items...) | [items...] |
[^1] Crates such as maplit do provide macros for this.
Constants
In Rust there are two types of constants const
and static
. const
are immutable values hardcoded into the program. statics are optionally mutable values that are globally available. Using mutable statics is unsafe unless wrapped in thread safe structs such as Arc
and Mutex
. (See Concurrency)
#![allow(unused)] fn main() { const VERSION: u32 = 1; static PROGRAM_NAME: &'static str = "Example"; }
Currently const and static values must be known at compile time. To get around this you can use the lazy_static crate, it works like by lazy {}
in Kotlin:
use lazy_static::lazy_static;
lazy_static! {
static ref A_MAP = HashMap::new();
}
const values are inserted at compile time where ever they were used, and so if you make a a mutable constant every use will be it's own instance.
A method can be constant if it's all of it's internal are constant as well, for example:
#![allow(unused)] fn main() { const FOO: usize = 1; const BAR: usize = 2; const RESULT: usize = add(FOO, BAR); const fn add(lhs: usize, rhs: usize) -> usize { lhs + rhs } }
References
All variables can be passed as a reference by prefixing with a &
, for example:
fn main() { let foo = 10; print(foo); print_ref(&foo); } fn print(value: i32) { //Passed by value so no need to deference example(value); } fn print_ref(value: &i32) { //Dereferenced with * example(*value); } fn example(value: i32) { //do something with value }
Dereferencing a variable moves the value, so the value must implement Copy
. See Deriving and implementing
For clarity:
Symbols | Meaning |
---|---|
<No symbols> | Value, immutable |
mut | Value, mutable |
& | Reference, immutable |
&mut | Reference, mutable |
* | Dereferenced |
Borrowing and Ownership
In Kotlin a variable exists, and is available, while in it’s scope. A global static variable is always available but a variable created in a method (unless returned) only exists during that instance of the methods execution. Rust is basically the same and generally you’ll be able to write code without having to think about the borrowing system, but sometimes you will have to deal with it.
This will not compile as bar
has taken ownership of the data in foo
and so foo
can no longer be used:
let foo = String::from("Hello");
let bar = foo;
println!("{}", foo);
This will compile however as numbers have Copy implemented for them and so num2 automatically makes a copy of num1s data:
#![allow(unused)] fn main() { let num1 = 54; let num2 = num1; println!("{}", num1); }
but this can be replicated for the string example by doing:
#![allow(unused)] fn main() { let foo = String::from("Hello"); let bar = foo.clone(); println!("{}", foo); }
This will only work when the type implemented Clone. Not all types support Clone as it may be impossible to copy it’s data, for example with network streams. Ownership and borrowing apply to all methods:
fn main() { let a = String::from("Hello"); let b = return_param(a); let c = length(b); println!("{}", c); } fn return_param(param: String) -> String { return param; } fn length(param: String) -> usize { return param.len(); }
When main is executed both a
and b
are lost, length takes ownership of the string and it is dropped at the end of length, to keep b
in memory either of the following changes could be made:
fn main() { let str = String::from("test"); let _ref_len = length_ref(&str); let (nstr, len) = length(str); println!("({},{})", nstr, len); } fn length_ref(param: &String) -> usize { return param.len(); //not dereferenced: //because param is a reference len() will return it's result as a reference //and because usize implements Copy it will be automatically dereferenced } //or fn length(param: String) -> (String, usize) { let len = param.len(); return (param, len); }
References are just pointers and so don’t take ownership but instead the value is borrowed, there are some rules around this for example only one mutable reference can exist at once. Because of this a potential helpful way to think about this is shared vs unique, you can as many read only references as you want shared around but when writeable only a single unique reference can exist (to avoid race conditions, etc).
Rust supports generics like Kotlin and they are expressed like this: Vec<Item>
, occasionally you might see Vec<&'a Item>
the 'a
is a lifetime notation and these are used to guide the compiler as to how long references will be alive. The lifetime name doesn’t matter but the standard names are 'a
, 'b
and 'c
, except for 'static
which means the variable must always be available, i.e. a hardcoded value.
If a parameter has the lifetime a
and a result also has the lifetime a
like this:
#![allow(unused)] fn main() { struct Foo { contents: String } impl Foo { //like with generics the lifetime has to specified in advance with <> fn get_first<'a>(&'a self) -> &'a str { &self.contents[0..1] } } }
then this is saying the instance of Foo
must live as long as the reference returned by get_first
.
However, in the example above the lifetimes aren't because the method has one parameter that is a reference and one result that is a reference and they share the same lifetime, so Rust will automatically assume the lifetime.
This would not compile:
struct Foo { contents: String }
impl Foo {
fn get_first<'a>(&'a self) -> &'a str {&self.contents[0..1]}
}
fn main() {
let result = get_char();
}
fn get_char() -> &str {
let foo = Foo { contents: String::from("example") };
let chr = foo.get_first();
return chr;
}
as there's no lifetime on the result of get_char
and there can't be as foo
is dropped at the end of get_char
and so chr
would be pointing to an invalid section of memory.
Classes, or the lack there of
In Kotlin has Classes, Abstract Classes, Interfaces, and extension methods. Rust has Traits, Structs, and Impls.
Kotlin
- An interface can have methods but can not have variables with values, and may extend another Interface. It can be supplemented with extension methods or sub classed by other Classes, Abstract Classes or Interfaces.
- A class can have variables and methods, and may extend a Class, an Abstract Class and/or Interface(s). It can be supplemented with extension methods or sub classed by other Classes or Abstract Classes.
- An abstract class can have variables and methods, and may extend a Class, an Abstract Class and/or Interface(s). It can be supplemented with extension methods or sub classed by other Classes or Abstract Classes.
Rust
- A trait is like an interface, it defines a list of methods that must be implemented. It can extend other traits, although this is rare.
- A struct is the closest thing to a class but although it has a list of variables it does not have any methods. This can not extend anything.
- An impl is a collection of methods either matching a Trait (like a Class implementing an Interface) or free form from a struct (like a Class), but Impl are not allowed to have variables and if implementing a Trait can not have methods that are not defined in the Trait. An Impl may be defined repeatedly.
- trait and impl can used like extension Methods (although the syntax is closer to Swift than Kotlin).
There is nothing like an abstract class in Rust.
Some Kotlin examples:
interface ParentA {
fun foo() //no method body, just an api
}
interface ParentB : ParentA { //includes methods from parent
fun bar() //no method body, just an api
}
class ClassFoo : ParentA { //includes methods from parent
var x = 0; //allowed to have variables with values
fun foo() {} //methods must be implemented
}
abstract class AbstractClassA: ParentA, ParentB {}
class ClassBar : AbstractClassA() {
fun foo() {} //methods must be implemented
fun bar() {} //from all parents
fun foobar() {} //can add new methods
}
fun ParentFoo.example1() {} //Adds method called example1 to all
//implementations and children of ParentA
Some Rust examples:
#![allow(unused)] fn main() { struct StructFoo { //variables only x: i32 } impl StructFoo { //methods only, but can access fn foo() {} //variables from Struct } //private methods allowed trait TraitA { //API only fn bar(); } trait TraitC : TraitA { fn boo(); //anything implementing TraitC must implement TraitA separately } impl TraitA for StructFoo { //methods only, can not fn bar() {} //have methods not in the trait } }
Deriving and implementing
Let’s say you make a type: Person. It has a first name, last name, date of birth, and occupation. It has functions to get the whole name and their age.
use chrono::Date;
use chrono::offset::{*};
struct Person<'a> {
first_name: &'a str,
last_name: &'a str,
date_of_birth: Date<Utc>,
occupation: &'a str
}
//Constructors
impl <'a> Person<'a> {
//no self param means this is a static method
fn new(first_name: &'a str,
last_name: &'a str,
year: u32,
month: u32,
day: u32,
occupation: &'a str) -> Person<'a> {
return Person {
first_name,
last_name,
date_of_birth: Utc.ymd(year as i32, month, day),
occupation
};
}
}
//Methods
impl <'a> Person<'a> {
//self param means this is an instance method
//as it's a reference this method does not consume
//the object
fn whole_name(&self) -> String {
return format!("{} {}", self.first_name, self.last_name);
}
fn age_in_years(&self) -> i32 {
let weeks = Utc::today().signed_duration_since(self.date_of_birth).num_weeks();
return (weeks / 52) as i32;
}
//the self here isn't a reference so the object
//is consumed by this method and won't exist
//after this is method is called
fn into_tuple(self) -> (String, i32) {
return (self.whole_name(), self.age_in_years());
}
}
fn main() {
//Double colon is for static methods
let person = Person::new("John", "Smith", 1988, 07, 10, "Author");
//Period is for instance methods
println!("{} is a {} who is {} years old.",
person.whole_name(),
person.occupation,
person.age_in_years());
}
The 'a
lifetime tells Rust that the &str
s will be available as long as the parent Person
is.
The sample uses the chrono
crate, it is a simple to use and common date and time library. If we want to print the object we must implement the Display like this:
#![allow(unused)] fn main() { use std::fmt; struct Person { first_name: String, last_name: String, occupation: String } impl fmt::Display for Person { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { return write!(f, "({} {}, {})", self.first_name, self.last_name, self.occupation); } } }
You can now write println!("{}", person)
, there are many traits that can be implemented for any struct that’s part of your project.
To avoid boilerplate Rust can automatically derive some traits for structs like so:
#![allow(unused)] fn main() { #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Default)] struct Foo {} }
Trait | Use |
---|---|
Debug | Automatically generates the equivalent of data classes toString(), use with {:?} instead of {} |
Clone | Implements the clone() method on the struct |
Copy | Allows structs to be cloned automatically instead of transferring ownership when assigned to new variable |
PartialEq | Implements equality checking and enables use of the == and != operators on the struct |
PartialOrd | Implements comparison and enables use of the > and < operators on the struct for types where the comparison may be impossible (e.g. floating numbers) |
Eq | Marker trait (like Sync) meaning that all fields can be always and correctly compared, not valid for all types (e.g. floating numbers) |
Ord | Same as PartialOrd but for types where comparison is always possible |
Hash | Automatically generates the equivalent of data classes hashCode(), required to use the struct as key in HashMaps |
Default | Implements a default value for all fields, see Default |
All of these require all the fields in the struct to implement the same traits. Numbers, strings, etc implement all the built in derivable types. As with PartialEq
and Eq
Rust often has two versions of a trait, one that is allowed to fail (and so will generally return Result
or Option
) and another that is not allowed to fail. In this case Eq
will panic if something goes wrong, likewise there is From
and TryFrom
for converting structs, From
will panic if it fails and TryFrom
will return Err
if it fails.
Default
If you implement a struct where all the fields have all implemented Default then you don’t have to write out every field when making a new instance of the struct:
#[derive(Default)] struct Foo { a: i32, b: i32, c: i32, d: String } fn main() { let foo = Foo::default(); //You can also supply some of the fields and leave the rest to Default: let foo2 = Foo { b: 45, d: "Foobar".to_string(), ..Foo::default() }; //This is also the syntax for copying: let foo3 = Foo { a: 10, ..foo }; }
Methods
this/self
A method in a impl for a struct may have a param for the struct, it must always be the first parameter and does not have a name:
Parameter | Meaning | Usage |
---|---|---|
<None> | A static method (accessed via ::) | For constructors/builders or grouping methods |
self | The object itself (this means unless the method returns Self it will dropped after this method) | Converting the object into another object |
&self | A immutable reference to itself | Getting a value or perform a action that doesn't effect this object |
&mut self | A mutable reference to itself | Setting a value or perform an action that changes internal values |
An example of naming conventions:
struct Foo {
...
}
impl Foo {
//Constructor: new()
fn new() -> Foo {}
}
impl Foo {
//Getter: x()
fn value(&self) -> i32]
//Setter: set_x()
fn set_value(&mut self, value: i32) {}
//Is/Has: is_x()
fn is_empty(&self) -> bool {}
//Clone and convert: as_x()
fn as_bar(&self) -> Bar {}
//Convert: into_x()
fn into_another_object(self) -> Bar {}
}
See also https://rust-lang.github.io/api-guidelines/naming.html
Functional Programming
Rust supports lambdas, the parameters are written comma separated in pipes and the body only requires curly braces if it goes over multiple lines:
#![allow(unused)] fn main() { let x = vec![1,2,3]; let y: Vec<usize> = x.iter() .map(|it| it + 1) .collect(); println!("{:?}", y); }
Unfortunately with Rust (like Dart) map()
, etc return a Map object that has to be converted back into a list using collect()
Rust also supports higher order functions:
fn foo(f: impl Fn(i32) -> i32)
fn foo<F>(f: F) where F: Fn(i32) -> i32
fn bar(f: impl MutFn(String) -> usize)
Fn can not change external state
FnMut can change external state
FnOnce can change external state, but is only allowed to be called once
Box allows you to store values on the heap, this is sometimes necessary as the stack can only hold values with a known size (at compile time), as the Box is just a pointer it has a known size unlike, for example, lambdas.
Modules
When making a project in Rust you are required to have one file (for programs it’s main.rs
, and lib.rs
for libraries), it’s also the only file recognised by the compiler. To add a new file to your project you need to add the line (for a file named new_file.rs
) to main.rs
or lib.rs
:
mod new_file;
Using the following code base:
//main.rs
mod foo; //all Rust files must be referenced here for the compiler to find them
mod bar;
use crate::bar::foobar;
fn main() {
foobar();
}
//foo.rs
pub fn public_method() {}
fn private_method() {}
//bar.rs
use crate::foo::public_method;
pub fn foobar() {}
The foo module has two methods public_method and private_method. private_method is only accessible inside the foo module. The bar module imports the public_method method from the foo module.
crate
means this project, if using a third party library (for example serde
) you would write serde::foo::bar;
.
Directories
When organising code it is common to group files in a directory. This requires a mod.rs file per directory, at minimum it must reference the other files in the directory to expose them to the compiler:
project
├ main.rs
├ foo.rs
└ bar
├ mod.rs
└ inner.rs
//main.rs
mod foo;
mod bar;
//bar/mod.rs
mod inner;
This would expose all files to the compiler.
Crates
Adding crates
Third party libraries are called Crates (and are available from https://crates.io). To add a crate, for example Serde, add this line to Cargo.toml after the [dependencies] line:
serde = "1.0.0"
You’ll still need to import the individual parts of the crate you want to use, for example:
use serde::json::Serialize;
::Foo
means import just Foo
::{Foo, Bar}
means import Foo and Bar
::*
means import everything in the module.
You can rename when importing:
use example::Foo as Bar;
Most of the massive crates have a prelude module that you should import, i.e.
use chrono::prelude::*
Features
Some crates have optional features, often these include macros or provide interop with other crates:
serde = "1.0.0"
can also be written as serde = { version = "1.0.0" }
. To include a feature this gets expanded to serde = { version = "1.0", features = ["derive"] }
.
Some crates will have features that you almost always want:
Crate | Feature(s) | Description |
---|---|---|
serde | derive | Adds macros to automatically serialize structs |
chrono | serde | Allows DateTime , etc to be serialized by serde |
reqwest | json , gzip | Adds support for sending/received structs and adds automatic support for gzip |
Not in standard
Some functionality built in to Java/Kotlin isn’t part of the Rust std lib and you’ll need to use these crates to add it:
Functionality | Crate | Notes |
---|---|---|
Random numbers | rand | Maintained by Rust team |
Serialization | serde | Does XML, JSON, protobuf, etc |
Lazy static variables | lazy_static | |
Regex | regex | Maintained by Rust team |
Base64 | base64 | |
UUID | uuid | |
Enums | strum | Enum features variant names, properties, count, list, ordinal |
Common crates
These crates are the closest equivalent to the commonly used Kotlin libraries:
Kotlin | Rust | Notes |
---|---|---|
GSON/Moshi | serde | Much more powerful and flexible than GSON |
JodaTime | chrono | Essentially the same |
JDBC | diesel | Works with multiple databases |
Result, Option and Exceptions
Result and exceptions
Result<V, E> is used for when a method may fail, it can contain the result or an error. It is created via Ok()
and Err()
fn get(idx: u32) -> Result<bool, usize> { if idx > 10 { Ok(true) } else if idx > 20 { Ok(false) } else { Err(404) } } fn main() { let result = get(10); match result { Ok(item) => println!("{}", item), Err(e) => println!("{}", e) } }
You can also do the equivalent of var!!
with both Option and Result by using var.unwrap()
and var.expect("some message")
. Both methods will crash the app if it’s Err/None, expect() will also write the message to the console.
To avoid having to write unwrap() every time if you’re in a method that returns a Result you can just write var?
, if var
is an Err the method will return the Err immediately.
To crash a Rust program you should use panic!("message")
. This will print the message and stacktrace to the command line.
Option and nulls
Option<T>
s are the same as Optional<T>
s and quite like nullable values and are created via Some()
and None
fn divide(numerator: f64, denominator: f64) -> Option<f64> { if denominator == 0.0 { None //notice no return and no semicolon } else { Some(numerator / denominator) } //the last value in a method is automatically returned } //assuming no return call fn main() { let result = divide(2.0, 3.0); match result { Some(x) => println!("Result: {}", x), None => println!("Cannot divide by 0"), } }
Creating macros
Macro template
macro_rules! <name> {
(<args>) => {
<body>
};
}
<name>
can be any valid rust ident<args>
see below<body>
see below
Arguments
Arguments can be empty, or combinations of variables and variadic.
Each arg must be the format $<name>:<type>
. (i.e. $example:literal
)
<name>
can be any valid rust ident<type>
must be one of
type | description |
---|---|
literal | Rust literal, such as a number |
expr | A single expression |
ty | Rust type |
ident | Rust ident |
and others |
For example, this macro makes methods that adds two numbers:
#![allow(unused)] fn main() { macro_rules! add_numbers { ($method_name:ident, $num_type:ty) => { fn $method_name(lhs: $num_type, rhs: $num_type) -> $num_type { lhs + rhs } } } }
When called with add_number!(add_i32, i32);
this code is generated:
#![allow(unused)] fn main() { fn add_i32(lhs: i32, rhs: i32) -> i32 { lhs + rhs } }
Variadic
Arguments can be written as $( <arg> )*
for 0 or more and $( <arg> )+
for 1 or more values. (i.e. $( $name:literal )+
), this would allow example!(1 2 3)
.
To support commas you can write $( <arg> ),*
, this would allow example!(1,2,3)
.
Optional
Anything can be optional using this syntax $( <thing> )?
, this include commas, such as
- optional trailing comma:
macro_rules! example($var1:literal, $var2:literal $(,)?)
- optional commas in variadic:
macro_rules! list($( $items:literal )$(,)?*)
Body
The body must be an expression or single line, to support multiple lines surround the code in {}, this is often written as
macro_rules! <name> {
(<args>) => {{
<body>
}};
}
Variadic
To use these arguments you have to surround them in $( <name> )*
in the body.
Anything can be written in the parentheses and it will be repeated once per item in <name>
For example if you want to print each item on a different line:
#![allow(unused)] fn main() { macro_rules! print_nums { ($( $numbers:literal ),+) => {{ $( println!("{}", $numbers); )* }}; } }
If called with print_nums(1,2,3);
This generates
#![allow(unused)] fn main() { println!("{}", 1); println!("{}", 2); println!("{}", 3); }
Optional
Optional arguments should also be surrounded like $( <name> )*
, anything can be written inside like before.
When dealing with optional args you may need to get an alternative value
#![allow(unused)] fn main() { macro_rules! add_nums { ($num1: expr $(, $num2: expr)?) => { $num1 + $( $num2 )* }; } }
This works fine if called with add_nums!(1,2)
but with add_nums!(1)
the code generated would be 1 +
which fails to compile.
To get around this use something to offer a substitute such as
#![allow(unused)] fn main() { macro_rules! some_or_none { () => { None }; ($entity:expr) => { Some($entity) } } //or macro_rules! or_else { ($value:literal, $other: literal) => { $value}; (, $other: literal) => { $other }; } }
These are used like this
#![allow(unused)] fn main() { macro_rules! add_nums { ($( $num1: expr )? $(, $num2: expr)?) => { or_else!($( $num1 )*, 0) + or_else!($( $num2 )*, 0) }; } }
Overloading
Macros can support different argument sets:
#![allow(unused)] fn main() { macro_rules! add_nums { ($num1:literal, $num2:literal) => { $num1 + $num2 }; ($num1:literal, $num2:literal, $num3:literal) => { $num1 + $num2 + $num3 }; } }
This can be called with add_nums!(1,2);
and add_nums!(1,2,3);
Hygiene
Macros have 'hygiene' which means to access something from your crate you'll have to spell out the full path:
$crate::path::some_method(..)
$crate
refers to your crate.
Visibility
Macros have to be annotated with #[macro_export]
for it to be usable by other modules/crates.
Advanced
Macros will accept extra text which can be required to invoke the macro:
#![allow(unused)] fn main() { macro_rules! add_nums { ($num1: literal + $num2: expr) => { $num1 + $num2 }; (($num1: expr) + $num2: expr) => { $num1 + $num2 }; } }
Would have be called like this add_nums!(1 + 2)
or add_nums!((some_var) + 2)
With custom text in the arguments then either commas are needed to separate them
macro_rules! example($thing1:expr, $thing2:expr)
or all but the last expr must be declared and called surrounded by parentheses macro_rules! example(($thing1:expr) $thing2:expr)
and example!((thing1) thing2);
See more Macros by example
Concurrency
To pass values safely between threads you need to use Mutexes or Atomic values in most languages, Rust is no different. For example:
use std::thread; use std::sync::atomic::{AtomicI8, Ordering}; use std::sync::Arc; use std::time::Duration; fn main() { //AtomicXX implement Sync meaning they can be used //in multiple threads safely //Arc (Atomically Reference Counted) allows for multiple, independent //references of a single value to exist outside of the borrow checker //for a tiny overhead by counting the number of references that //exist let number = Arc::new(AtomicI8::new(0i8)); //Make a copy of the arc, any number of copies can exist but //the each copy has to be moved into it's thread let thread_number = number.clone(); //move means this lambda is taking ownership of any variable //it uses, this is necessary for lambdas executed in a different //context e.g. in a different thread thread::spawn(move || { let mut i = 0; loop { //ordering controls how the atomic value is set/read //You should probably always use SeqCst thread_number.store(i, Ordering::SeqCst); i += 1; thread::sleep(Duration::from_millis(500)); if i > 10 { break; } } println!("Done"); }); loop { println!("{}", number.load(Ordering::SeqCst)); if number.load(Ordering::SeqCst) >= 10 { break; } } }
This program will continually print out the value stored in number
until the thread reaches 10
Testing
The standard in Rust is to have the tests in a module inside the module being tested, the test module needs to be annotated as do all the tests:
#![allow(unused)] fn main() { //foo.rs fn add(value1: i32, value2: i32) -> i32 { value1 + value2 } #[cfg(test)] mod tests { use super::*; #[test] fn test_all() { assert_eq!(2, add(1, 1)); } } }
Cargo
To run and build programs from the command line you should always use cargo (outside of an IDE):
#Build debug version
cargo build
#Run debug version
cargo run
#Run tests
cargo test
#Build release version
cargo build --release
Other command line options:
#Format all code
cargo fmt
#Linter
cargo clippy
#These have to be installed first by
rustup update
rustup component add rustfmt
rustup component add clippy
Tools
To install a tool use cargo install <tool>
, for example cargo install cargo-edit
Dependency management
Description
Allows you to add, remove or update dependencies from the command line
Usage
cargo add <crate>
, e.g. cargo add chrono
cargo rm <crate>
cargo upgrade
Link
Dependency graph
Description
Generates a dependency graph for your project
Usage
cargo deps
Link
Security Audit
Description
Audit Cargo.lock files for crates with security vulnerabilities reported to the RustSec Advisory Database.
Usage
cargo audit
Install commands
cargo install cargo-audit
Link
Macro Expansion
Description
Prints out the result of macro expansion and #[derive]
expansion applied to the current crate.
Usage
cargo expand [module]
Install commands
cargo install cargo-expand
Link
Outdated dependencies
Description
Prints out report of out of date dependencies.
Usage
cargo outdated
Install commands
cargo install cargo-outdated
Link
Enums
Unfortunately enums in Rust work they do in Swift and so no default values can be provided and instead you have to add methods which use matches to provide values:
enum MobileOs { Android, Ios, Windows } impl MobileOs { fn status(&self) -> &str { match self { MobileOs::Android => return "alive", MobileOs::Ios => return "alive", MobileOs::Windows => return "dead", } } } fn main() { println!("{}", MobileOs::Android.status()); }
Thankfully they can also work like sealed classes:
enum Example { Foo { named_value: i32 }, Tuple(u8, u8), Empty } fn main() { let foo = Example::Foo { named_value: 45 }; let tuple = Example::Tuple(1, 2); let empty = Example::Empty; }
When coming from other modern languages you be expecting the ability to get a variant count, list or names and add static values to each variant but unfortunately Rust enums do not support any of these features, but all of these can be added with the strum crate.
Strum
The strum crate adds many features to enums on a case by case basis.
It provides:
- To and from string for enum variants
- Variant iterator
- Enum count
- Property values
- etc
Each feature needs to be enabled by adding a derive macro.
Tips and tricks
Minor tips
Reading a line from the command prompt using std::io::stdin().read_line()
will include the return character, remove it using trim()
if let
Like in Swift an Option can be unwrapped with an if, if there is a value match:
#![allow(unused)] fn main() { fn some_method(optional_string: Option<String>) { if let Some(string_value) = optional_string { println!("Does exist: {}", string_value); } } }
This also works for Result but use Ok and Err instead of Some and None. If you need to handle both states you should use match as an if let throws away the other value:
#![allow(unused)] fn main() { type Error = Box<dyn std::error::Error>; fn some_method(optional_string: Option<String>) { match optional_string { Some(string_value) => println!("Does exist: {}", string_value), None => println!("No content") } } fn another_method(result: Result<String, Error>) { match result { Ok(string_value) => println!("Success: {}", string_value), Err(err) => println!("Failure: {}", err) } } }
Reference counting
Sometimes you need bypass the borrow checker, for example, you want to use a reference as a pointer to an item in a collection or you’re passing values between threads. To do this you use the Arc (Atomically Reference Counted) class, it adds a small overhead in the form of a count and some extra code to monitor and update the count. Arc will keep the value alive as long as any Arc value is still alive, when the last Arc value is dropped the value will be as well. To make multiple references to an value protected by Arc clone it:
use std::sync::Arc; fn main() { let some_heap_thing = Thing::new(); let arc_thing = Arc::new(some_heap_thing); thread1(arc_thing.clone()); thread2(arc_thing.clone()); } fn thread1(thing: Arc<Thing>) { thing.methods_accessed_in_normal_way(); } fn thread2(thing: Arc<Thing>) {} struct Thing {} impl Thing { fn new() -> Thing {Thing {} } fn methods_accessed_in_normal_way(&self) {} }
The value in the Arc can not be mutable unless it’s contained in another class, as the value will need to be protected against concurrent updates, the wrapper types are Mutex and RwLock. The differences are that RwLock will allow any number of concurrent readers but only one writer and Mutex only allows one reader or writer at a time.
Converting strings
Often when writing a function that takes a piece of text you’ll want to support both String
and &str
to be more convenient to the caller. This is best achieved by using &str
or Into<String>
.
String
can be converted to &str
like this:
let text = String::new();
print(&text);
(Actually it's converted to &String
as adding a &
just makes it a reference but &String
implements AsRef<str>
allowing it to be automatically converted)
The Into trait tells the compiler to allow any parameter that be coerced as that type to be passed in. &str
already has the Into<String>
trait but it can also be implemented for any struct. Into<X>
for Y is automatically implemented for any type that implements From<Y>
for X which is actually how it’s implemented for Strings and is the recommended approach.
fn print(value: &str) {
println!("{}", value;
}
fn print<S: Into<String>>(value: S) {
println!("{}", value.into());
}
Interior mutability
Sometimes you need to have a mutable value but can only pass it around as a value or reference, to achieve you can use the Cell
structs.
Cell
is a wrapper around a value that can be changed at any point.
RefCell
is the same as Cell
but allows the value to be exposed as a reference.
RwLock
is the same as RefCell
but can be shared across threads.
Mutex
is the same as RefCell
but can make references that can be shared across threads.
All of these are safe, they use reference counting and/or memory swapping to update values.
Example:
use std::cell::RefCell; fn main() { let not_mutable = Person { name: RefCell::new(String::from("Emma Britton")) }; not_mutable.change_name(String::from("New Name")); println!("{}", not_mutable.name.borrow()); } struct Person { name: RefCell<String> } impl Person { fn change_name(&self, new_name: String) { self.name.replace(new_name); } }
Indexed iteration
Kotlin provides forEach, map, filter, etc for iterators but these only give you the element if you also need the index as Kotlin provides forEachIndexed, mapIndexed, filterIndexed, etc
list.forEach { element ->
println(element)
}
list.forEachIndexed { i, element ->
println("${i}: ${element}")
}
Rust also has this feature but it works differently, instead of different methods the iterator type is changed via enumerate:
#![allow(unused)] fn main() { let list = vec!["a", "b", "c"]; list.iter() .for_each(|element| println!("{}", element)); list.iter() .enumerate() .for_each(|(i, element)| println!("{}: {}", i, element)); }
This has the downside that all later operators must also handle the index as well.
Formatting strings
To format a string, the easiest (and correct) way is to use format!(string, parameters...)
. String formatting in Rust uses {} as placeholders for parameters.
Formatting is supported by several methods:
format!(fmt, values...)
- returns a formatted stringprint!(fmt, values...)
andprintln!(fmt, values...)
- writes a formatted string to stdoutwrite!(Formatter, fmt, values...)
- writes the formatted string into the first parameter
You can pass values into these macros without making them a reference first as they will be turned into references, and so the macros don't take ownership.
Basics
With
format!("{}", some_value)
If some_value
implements Display then it will printed otherwise you'll get a compile error saying the type doesn't implement Display. All primitives implement Display, but other std types like arrays and collections do not.
How to implement Display for a custom type:
struct Point {
x: i32,
y: i32
}
impl Display for Point {
fn fmt(&self, f: &mut Formatter) -> Result<(),fmt::Error> {
write!(f, "Point({},{})", self.x, self.y)
}
}
then when calling format!("{}", Point {x: 2, y: 3})
results in Point(2,3)
.
Another option exists though, for example if you don't really care about the formatting: Debug
, which is implemented like this
#![allow(unused)] fn main() { #[derive(Debug)] struct Point { x: i32, y: i32 } }
To print the debug version of a value use {:?}
(or {:#?}
to pretty print), calling this format!("{:?}", Point { x: 3, y: 5})
results in Point { x: 3, y: 5 }
All primitives and std types like arrays and collections, and most structs from third party crates, implement Debug
.
Param selection
There are several ways to order the params
Default
They are used in the order supplied
#![allow(unused)] fn main() { println!("{}, {}, {}", 1, 2, 3); }
produces 1, 2, 3
Positional/Indexed
#![allow(unused)] fn main() { println!("{1}, {2}, {0}", 1, 2, 3); }
produces 2, 3, 1
Named
Non named parameters must come before any named parameters
println!("{0}, {foo}, {bar}", 3, bar = 1, foo = 2);
produces 3, 2, 1
Referencing variables
#![allow(unused)] fn main() { let foo = 1; println!("{foo} = {}", 2); }
produces 1 = 2
Named params can formatted with debug by adding :?
, format!("{foo:?}")
Padding
"{:>5}"
Left pad with up to 5 spaces
"{:<7}"
Right pad with up to 7 spaces
"{:^22}"
Centre with up to 11 spaces on both sides
Padding with any character:
"{:_>5}"
Left pad with up to 5 underscores
"{:#<7}"
Right pad with up to 7 hashes
"{:c^22}"
Centre with up to 11 ’c’s on both sides
Numbers
"{:.3}"
Will print 3 fractional digits (adding 0s on the end if necessary) but only if it’s a floating point number
"{:+3}"
Print sign
"{:03}"
Print at least 3 digits (padding with 0s on the start if necessary), if negative the minus symbol will replace a 0
Example: format!("{:>5} {named}", "Foo", named=123)
To have variable parts (such as variable length padding) use this syntax:
("{1:.0$}", 1, 1.22)
This will print 1.2, the syntax is {value_index:.precision_index$}
("{1:=<0$}", 10, "test")
This will print test======, the syntax is {value_index:padding_char<length_index$}
Note that invalid parameter details are ignored silently and treated as {}. If debugging via logging consider using dgb!():
let x;
x = dbg!(1 * 4);
prints
[src/main.rs:3] 1 * 4 = 4
For crates
Date Formatting
Chrono
Using DateTimeFormat.forPattern("<pattern>").print(DateTime.now())
for Kotlin and Utc::now().format("<pattern>")
for Rust.
Kotlin/JodaTime | Rust/Chrono | Example | |
---|---|---|---|
Date | yyyy-MM-dd | %Y-%M-%D | 2000-01-01 |
Text Date | dd MMM yyyy | %e %b %Y | 15 Jun 2004 |
Time | HH:mm:ss | %H:%M:%S | 14:12:56 |
to ISO 8601 | DateTime.now().toString() | Utc::now().to_rfc3339_opts( SecondsFormat::Millis, true) | 1996-12-19T16:39:57.000Z |
from ISO 8601 | DateTime.parse | Utc::parse_from_rfc_3339 |
JSON Parsing
Serde
Add this to cargo.toml
:
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Kotlin
data class Example(
val name: String,
@SerializedName("num")
val someNumber: Int
)
fun convert(json: String, example: Example) {
val outputExample: Example = Gson().fromJson<Example>(json, Example::class.java)
val outputJson: String = Gson().toJson(example)
}
Rust
#[derive(Serialize,Deserialize)]
struct Example {
name: String,
#[serde(rename = "num")]
some_number: i32
}
fn convert(json: String, example: Example) {
let output_example: Example = serde_json::from_str(&json).unwrap();
let output_json: String = serde_json::to_string(&example).unwrap();
}
Dependency Injection
Silhouette
Rust
use silhouette::facade::Container;
// will always use the same pool
Container::singleton(&|_| DBPool::new())?;
// will resolve a new connection each time
Container::bind(&|container| -> DBConnection {
let shared_pool = container.resolve::<DBPool>().unwrap();
shared_pool.get_conn()
})?;
// somewhere else in your app...
let connection: DBConnection = Container::resolve()?;
Common bugs/issues
Cloning a reference returns a reference despite the signature being a value
This can because the struct doesn’t implement/derive Clone
cannot move out of borrowed content when using unwrap()
This is because unwrap()
consumes the reference (it’s self parameter is just self
), to fix this use variable.as_ref().unwrap()
.
No method named ..
found for Rc<RefCell>
Most likely you have imported use std::borrow::BorrowMut
, remove it.
The error is caused because there are two methods named borrow_mut
and importing the trait causes the wrong one to be called. See GitHub issue
closures can only be coerced to fn
types if they do not capture any variables
You need to change the param to a generic, for example:
fn main() {
let value = "example";
print(|text| format!("{text}{value}"));
}
fn print(method: fn(&str) -> String) {
println!("{}", method("hi"));
}
into
fn main() { let value = "example"; print(|text| format!("{text}{value}")); } fn print<F: Fn(&str) -> String>(method: F) { println!("{}", method("hi")); }
Architecture
Kotlin is an object orientated language which means that generally data and methods are grouped together in classes and it’s very rare to have methods outside of classes even though Kotlin fully supports this. Also classes can extend each other so a lot of frameworks (such as Android or AWT) rely heavily on inheritance to provide functionality to classes.
Rust is a data orientated language, and programs will more resemble C programs where a program may be made of a few methods and a few structs without any impls. And as impl inheritance is impossible in Rust and trait inheritance doesn’t really add much use, composition is used a lot.
That said grouping logic and data is still fine and done with Rust:
class Point(
val x: Float,
val y: Float
){
fun distanceTo(other: Point): Float {...}
fun angleTo(other: Point): Float {...}
}
can become
struct Point {
x: f32,
y: f32
}
impl Point {
fn new(x: f32, y: f32) -> Point {
return Point {x, y};
}
}
impl Point {
fn distance_to(other: Point) -> f32 {...}
fn angle_to(other: Point) -> f32 {...}
}
Resources
Tutorials
Linked List An in depth guide on how to implement a linked list in Rust
Tic-Tac-Toe This project is heavily commented and is an example of idiomatic and well written Rust.
Rust Community
References
Guide for changes in 2018 edition of Rust (This should only be needed for converting old code)