Introduction
This is a short introduction to Rust, intended for developers that already know another language. In the examples, Rust is compared with TypeScript, JavaScript or Java, sometimes with C++ or Kotlin.
For a deep dive into the syntax and Rust’s concepts, have a look at The Rust Programming Language, but for a quick overview, read on.
Naming
Regarding names, Rust prefers snake case for variables and functions, so a method would be called read_str
instead of readStr
. For structs, traits and enums, camel case (or Pascal case) is used, for example HttpClient
.
Syntax
Rust’s syntax is a mix of existing languages (curly braces, functions and references like in C, type after identifier like in Go or Kotlin, generics and type parameters like in C++ or Java) with some Rust-specific elements (lifetime names, patterns, macros, attributes). For a quick overview of the syntax, see the Rust Language Cheat Sheet or an overview of Rust’s keywords.
Variables
Rust variable declarations are very similar to TypeScript or Kotlin, but look a bit different from Java or C.
const s: string = "";
let n: number = 0.9;
let i = 123; // Type inferred
let s: &str = "";
let mut n: f64 = 0.9;
let mut i = 123; // Type inferred
Most of the time, the type can be ommitted in Rust and the compiler will infer the correct type
Types
In Rust, there are more specific primitive data types.
The void type is called unit and is indicated by ()
, see functions for an example.
int i = 123;
long l = 456L;
float f = 0.5f;
double d = 0.5;
String string = "Hello";
int[] arr = {1, 2, 3};
List<Integer> list = Arrays.asList(1, 2, 3);
let i: i32 = 123;
let l: i64 = 456;
let f: f32 = 0.5;
let d: f64 = 0.5f64;
let string: &str = "Hello";
let arr: [i32; 3] = [1, 2, 3];
let list: Vec<i32> = vec![1, 2, 3];
In Rust, numeric literals can optionally have a type suffix,
for example 1000u32
or 0.5f64
.
Mutability
Variables need to be explicitly declared mutable (let
versus let mut
), like in JavaScript (const
and let
) or Kotlin (val
and var
).
The mutability model in Rust is not like JavaScript, but a bit more like const in C++, as Rust will not let you call modifying methods on a variable which is not declared as mutable.
let arr1: string[] = [];
arr1.push("123"); // OK
arr1 = ["a", "b"]; // OK
const arr2: string[] = [];
arr2.push("123"); // OK, even though arr2 is const
arr2 = []; // error, arr2 is const
let mut arr1 = vec![];
arr1.push("123"); // OK
arr1 = vec!["a", "b"]; // OK
let arr2 = vec![];
arr2.push("123"); // error, arr2 is not mutable
arr2 = vec![]; // error, arr2 is not mutable
In TypeScript, declaring a variable as const
only prevents reassignment, not modification.
In Rust, only variables declared as mut
can be modified
Destructuring
Rust supports destructuring, like JavaScript or Kotlin.
function distance(a, b) {
const { x: x1, y: y1 } = a;
const { x: x2, y: y2 } = b;
return Math.sqrt(
Math.pow(x2 - x1, 2) +
Math.pow(y2 - y1, 2)
);
}
fn distance(a: &Point, b: &Point) -> f32 {
let Point { x: x1, y: y1 } = a;
let Point { x: x2, y: y2 } = b;
((x2 - x1).powf(2.0) + (y2 - y1).powf(2.0)).sqrt()
}
struct Point {
x: f32,
y: f32,
}
More examples can be found in this section from the Rust for C++ programmers guide
Functions
Functions are basically the same as in C, Java, Go or TypeScript: They have a name, zero or more parameters and a return type.
void log(char* message) {
printf("INFO %s\n", message);
}
fn log(message: &str) -> () {
println!("INFO {}", message);
}
The unit type ()
(void in some languages) is the default return type when no type is given for a function.
It could be omitted in this example, like fn log(message: &str) { ... }
In Rust, functions are expressions, which means the last statement is also the return value (like in Ruby). This is a bit like implicit return, but not exactly. The official style is to only use return
for early returns.
public static int add(int a, int b) {
return a + b;
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
Note that there is no semicolon in the Rust function, otherwise it would return void
Rust currently has no named arguments or default arguments (like Python and TypeScript) and does not allow method overloading (like C++, Java or TypeScript).
Inner functions
Rust also supports inner functions.
const RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
function isValidRange(start, end) {
function isValid(date) {
return date && date.match(RE);
}
return isValid(start) && isValid(end);
}
use regex::Regex;
const RE: &str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$";
fn is_valid_range(start: &str, end: &str) -> bool {
fn is_valid(date: &str) -> bool {
!date.is_empty()
&& Regex::new(RE)
.unwrap()
.is_match(date)
}
is_valid(start) && is_valid(end)
}
Note that Regex
is an external crate and not part of Rust’s standard library
Extension methods
Rust (like Kotlin) also supports extension methods, so the previous example could be rewritten using extensions.
typealias Range = Pair<String, String>
fun Range.isValid(): Boolean {
val (start, end) = this
return start.isNotEmpty() && end.isNotEmpty()
}
object Main {
@JvmStatic
fun main(args: Array<String>) {
val range = Range("2020-01-01", "2020-12-31")
if (range.isValid()) {
println("Range is valid!")
}
}
}
type Range<'r> = (&'r str, &'r str);
trait IsValid {
fn is_valid(&self) -> bool;
}
impl<'r> IsValid for Range<'r> {
fn is_valid(&self) -> bool {
let (start, end) = &self;
!start.is_empty() && !end.is_empty()
}
}
fn main() {
let range = ("2020-01-01", "2020-12-31");
if range.is_valid() {
println!("Range is valid!");
}
}
In Rust, extension methods are added by implementing a trait.
When there is only one method, it’s common to name the trait like the method (IsValid
).
The 'r
denotes a lifetime, for more information, see
this section from the Rust book
Closures (lambdas)
Rust supports closures (also called Lambdas, arrow functions or anonymous functions in other languages).
When accessing variables from outside the closure, Rust is more strict than JavaScript or Java, see capturing for more details.
function findEmails(list) {
return list.filter(
s => s && s.includes("@")
);
}
fn find_emails(list: Vec<String>) -> Vec<String> {
list.into_iter()
.filter(|s| s.contains("@"))
.collect()
}
For more filter examples, see this section from the documentation
Expressions
In Rust, almost everything is an expression, like in Kotlin and different from JavaScript or Java. You can directly assign the result of an if
statement to a variable, for example.
function getLogLevel() {
let level = process.env.TRACE
? "trace"
: process.env.DEBUG
? "debug"
: "info";
level =
level === "trace"
? 0
: level === "debug"
? 1
: 2;
console.log("using log level", level);
return level;
}
fn get_log_level() -> u32 {
let level = if std::env::var("TRACE").is_ok() {
"trace"
} else if std::env::var("DEBUG").is_ok() {
"debug"
} else {
"info"
};
let level = match level {
"trace" => 0,
"debug" => 1,
_ => 2,
};
println!("using log level {}", level);
level
}
The Rust code uses match, which is like switch
in Java or JavaScript except that it’s an expression and provides more flexibility. Unless many other languages, Rust allows variable shadowing for local variables (level
in this example)
Structs (classes)
Rust does not have full support for classes like Java or TypeScript, but instead offers structs (similar to structs in C). These are like data containers with methods, but they don’t support all of the object oriented concepts, like inheritance.
public class HttpClient {
private final ClientImpl clientImpl;
public HttpClient() {
clientImpl = new ClientImpl();
}
public String get(String url) {
return clientImpl.newRequest()
.get(url).asString();
}
}
public static void main(String[] args) {
HttpClient httpClient = new HttpClient();
System.out.println(httpClient
.get("https://example.com/"));
}
pub struct HttpClient {
client_impl: ClientImpl,
}
impl HttpClient {
pub fn new() -> HttpClient {
HttpClient {
client_impl: ClientImpl {},
}
}
pub fn get(&self, url: &str) -> String {
self.client_impl.new_request()
.get(url)
.as_string()
}
}
fn main() {
let http_client = HttpClient::new();
println!("{}",
http_client.get("https://example.com/"));
}
In Java, mutability is given on a field level, here clientImpl
is immutable.
In Rust, the mutability modifier is set on the instance variable and not per field, so you cannot have mutable and immutable fields in the same struct (see the Interior Mutability Pattern)
Methods do not have implicit access to this, as in Java or JavaScript, so you need to pass an explicit argument named self
, like in Python. Methods without this parameter are called associated functions (they are called static methods in some languages).
In Rust, there is no constructor, structs are created similarly to objects in JavaScript. For cases where constructor logic is required, there is a convention to create an associated function (static method) named new
, which will return the constructed object instance.
Traits (interfaces)
The most similar thing to interfaces in Rust are traits.
interface Named {
fun name(): String
}
data class User(
val id: Int,
val name: String
) : Named {
override fun name(): String {
return this.name
}
}
pub trait Named {
fn name(&self) -> &String;
}
pub struct User {
pub id: i32,
pub name: String,
}
impl Named for User {
fn name(&self) -> &String {
return &self.name;
}
}
Rust is mostly a static language, so some things that other language will do during runtime, Rust will do during compile time, when possible. Interfaces are usually used for dynamic dispatch and if you want to use traits in a similar way, see this chapter about static and dynamic dispatch and this blog post.
Default methods
Traits also support default methods, like interfaces in Java or Kotlin.
Associated functions
In Rust, traits can also have associated functions, for example from_str
in std::str::FromStr
(section string parsing in the Rust Cookbook)
interface FromList<T> {
fun fromList(list: List<Int>): T?
}
data class MyPoint(val x: Int, val y: Int) {
companion object : FromList<MyPoint> {
override fun fromList(list: List<Int>):
MyPoint? {
return when (list.size) {
2 -> MyPoint(list[0], list[1])
else -> null
}
}
}
}
object Main {
@JvmStatic
fun main(args: Array<String>) {
val point = MyPoint.fromList(listOf(100, 200))
println(point)
}
}
trait FromList<T> {
fn from_list(list: &Vec<i32>) -> Option<T>;
}
struct MyPoint {
x: i32,
y: i32,
}
impl FromList<Self> for MyPoint {
fn from_list(list: &Vec<i32>) -> Option<Self> {
match list.len() {
2 => Some(MyPoint {
x: list[0],
y: list[1],
}),
_ => None,
}
}
}
fn main() {
let point =
MyPoint::from_list(&vec![100, 200]).unwrap();
println!("({}, {})", point.x, point.y);
}
For more information about the Option
type, see the Null values section below.
The keyword Self
(upper case) can be used to reference the current type
Enums
Rust supports enums, like Java or Kotlin, but with more flexibility. The Rust enums offer more than in C++ or TypeScript, as they are not merely a list of constants, but more like unions.
enum UserRole {
RO("read-only"), USER("user"),
ADMIN("administrator");
private final String name;
UserRole(String name) {
this.name = name;
}
String getName() {
return name;
}
boolean isAccessAllowed(String httpMethod) {
switch (httpMethod) {
case "HEAD":
case "GET":
return true;
case "POST":
case "PUT":
return this == USER || this == ADMIN;
case "DELETE":
return this == ADMIN;
default:
return false;
}
}
}
class Main {
public static void main(String[] args) {
UserRole role = UserRole.RO;
if (role.isAccessAllowed("POST")) {
System.out.println("OK: "
+ role.getName());
} else {
System.out.println("Access denied: "
+ role.getName());
}
}
}
#[derive(PartialEq)]
enum UserRole {
RO,
USER,
ADMIN,
}
impl UserRole {
fn name(&self) -> &str {
match *self {
UserRole::RO => "read-only",
UserRole::USER => "user",
UserRole::ADMIN => "administrator",
}
}
fn is_access_allowed(
&self,
http_method: &str,
) -> bool {
match http_method {
"HEAD" | "GET" => true,
"POST" | "PUT" => {
*self == UserRole::USER
|| *self == UserRole::ADMIN
}
"DELETE" => *self == UserRole::ADMIN,
_ => false,
}
}
}
fn main() {
let role = UserRole::RO;
if role.is_access_allowed("POST") {
println!("OK: {}", role.name());
} else {
println!("Access denied: {}", role.name());
}
}
Rust does not support constants in enums,
so we need to use a match in the name
method. The enum matches are checked at compile time,
so when all enum variants are used, there is no need to have a default branch.
For more information about the #[derive(PartialEq)]
line, see the section about Attributes
Associated values
Rust enums also support associated values, which means they are not constant, but instead allow the creation of enum variant instances with specific values.
sealed class GitCommand {
abstract fun execute()
}
object Status : GitCommand() {
override fun execute() =
executeCommand(listOf("status"))
}
class Checkout(
private val branch: String
) : GitCommand() {
override fun execute() =
executeCommand(listOf("checkout", branch))
}
class Add(
private val files: List<String>
) : GitCommand() {
override fun execute() =
executeCommand(listOf("add") + files)
}
class Log(
private val decorate: Boolean,
private val patch: Boolean
) : GitCommand() {
override fun execute() {
val args = mutableListOf("log")
if (decorate) {
args.add("--decorate")
}
if (patch) {
args.add("--patch")
}
executeCommand(args)
}
}
fun executeCommand(args: List<String>) {
val redirect = ProcessBuilder.Redirect.INHERIT
ProcessBuilder(listOf("git") + args)
.redirectInput(redirect)
.redirectOutput(redirect)
.redirectError(redirect)
.start()
.waitFor()
}
object Main {
@JvmStatic
fun main(args: Array<String>) {
val command =
Log(decorate = false, patch = true)
command.execute()
}
}
use std::process::Command;
pub enum GitCommand<'g> {
STATUS,
CHECKOUT(&'g str),
ADD(Vec<&'g str>),
LOG { decorate: bool, patch: bool },
}
impl<'g> GitCommand<'g> {
fn execute(self) {
let args: Vec<&str> = match self {
GitCommand::STATUS => vec!["status"],
GitCommand::CHECKOUT(branch) => {
vec!["checkout", branch]
}
GitCommand::ADD(files) => {
[vec!["add"], files].concat()
},
GitCommand::LOG {
decorate,
patch,
} => {
let mut args = vec!["log"];
if decorate {
args.push("--decorate")
}
if patch {
args.push("--patch")
}
args
}
};
execute_command("git", &args);
}
}
fn execute_command(command: &str, args: &[&str]) {
Command::new(command)
.args(args)
.spawn()
.expect("spawn failed!")
.wait()
.expect("command failed!");
}
fn main() {
let command = GitCommand::LOG {
decorate: false,
patch: true,
};
command.execute();
}
Sealed Classes in Kotlin are similar to Rust’s enums, as they allow the modelling of a closed type hierarchy
See this section about enums for more examples.
Concepts
Ownership
One thing that is very special about Rust is the way it handles memory allocations: It doesn’t have a garbage collector (like JavaScript, Java or Go), but the developer does not need to free memory explicitly, either (like in C). Instead, Rust automatically frees memory when it is no longer in use. For this mechanism to work, the developer needs to explicitly think about ownership of the values the program is using.
class User {
private String name;
public User(String name) {
this.name = name;
}
}
public static void main(String[] args) {
String name = "User";
User user1 = new User(name);
User user2 = new User(name);
}
struct User {
name: String,
}
fn main() {
let name = String::from("User");
let user1 = User { name };
let user2 = User { name }; // compile error
}
In Java, the garbage collector will regularly check the object references and when nothing references the User instances anymore, they will be deleted. Once both instances have been detected as unused, the "User"
string can also be deleted.
In Rust, values can only be owned by one object at a time: the assignment to user1
is OK, because the value will be moved from the name
variable to user1
. The creation of user2
, however, will give a compile error, because the string "User"
is now owned by user1
, not by name
.
In C, the developer has to make sure that allocated memory is freed when it is no longer needed. This can lead to bugs (memory leaks) and was one of the motivation for Rust’s different approach to managing memory. While Rust offers protections against it, it’s not impossible to have memory leaks.
#include <string>
std::string* get_string() {
std::string* string = new std::string("hello");
delete string;
return string;
}
fn get_string() -> String {
let string = String::from("hello");
drop(string);
return string; // compile error!
}
In this example, the C++ code has a use after free error. Note that this is only an example, drop is rarely used in Rust code (values will be dropped automatically when they go out of scope)
Rust’s ownership model also helps when dealing with multi-threaded code. The compiler keeps track of the values the program is using and makes sure that the same value is not accessed from multiple threads without proper locking logic around it.
#include <vector>
#include <thread>
int main() {
std::vector<std::string> list;
auto f = [&list]() {
for (int i = 0; i < 10000; i++) {
list.push_back("item 123");
}
};
std::thread t1(f);
std::thread t2(f);
t1.join();
t2.join();
}
use std::thread;
fn main() {
let mut list = vec![];
let f = move || {
for _ in 0..10000 {
list.push("item 123");
}
};
let t1 = thread::spawn(f);
let t2 = thread::spawn(f); // compile error!
t1.join().unwrap();
t2.join().unwrap();
}
The std::vector
class is not thread-safe and the C++ program will compile
without errors, but when running, it will probably crash with an error
like pointer being freed was not allocated
or similar. In Rust, the closure f
takes
ownership of list
(indicated by the move keyword),
that’s why the compiler gives an error when f
is used more than once.
Strings
Rust has multiple string types, the most important ones are str (usually in the form of &str
) and String.
The &str
type only references borrowed content, so this is the type that is used for static strings and when referencing string slices.
Like other immutable references, &str
values cannot be modified. Use String
if you need a modifiable string.
const FILE_DATE = "2020-01-01";
function printCopyright() {
const year = FILE_DATE.substr(0, 4);
const copyright = `(C) ${year}`;
console.log(copyright);
}
const FILE_DATE: &str = "2020-01-01";
fn print_copyright() {
let year: &str = &FILE_DATE[..4];
let copyright: String = format!("(C) {}", year);
println!("{}", copyright);
}
See this section about strings for more examples
The String
type is used for dynamically created strings, which can be modified and where the length is not fixed at compile time.
Usually, &str
will be used for function parameters and String
will be used as return value.
public static String repeat(String s, int count) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) {
result.append(s);
}
return result.toString();
}
fn repeat(s: &str, count: u32) -> String {
let mut result = String::new();
for _ in 0..count {
result += s;
}
result
}
This is just an example, Rust already has String.repeat
For struct fields, usually String
should be used, as the value will probably be owned by that struct,
especially when dealing with longer living objects.
For string constants, &'static str
can be used for fields instead.
data class User(
private val source: String,
private val name: String,
private val address: String
) {
companion object {
fun fromString(string: String): User {
val lines = string.lines()
return User("kotlin-v1.0", lines[0],
lines[1])
}
}
}
pub struct User {
source: &'static str,
name: String,
address: String,
}
impl User {
pub fn new(s: &str) -> User {
let mut lines = s.lines();
User {
source: "rust-v1.0",
name: lines.next().unwrap().to_owned(),
address: lines.next().unwrap().to_owned(),
}
}
}
Note that default field access is private
in Rust
To convert a variable s1
of type String
to &str
, use &s1
.
To convert a variable s2
of type &str
to String
, use s2.to_owned()
(this allocates new memory and creates a copy of the string).
Sometimes this conversion is also necessary for literals, like "string literal".to_owned()
.
Null values
Rust does not have a special null
value (also called None
or nil
in some languages). Instead, there is the Option enum, which is very similar to the Optional type in Java.
public static Integer getYear(String date) {
if (date.length() >= 4) {
String s = date.substring(0, 4);
try {
return Integer.valueOf(s);
} catch (NumberFormatException e) {
return null;
}
} else {
return null;
}
}
public static void main(String[] args) {
Integer year = getYear("2020-01-01");
if (year != null) {
System.out.println(year);
}
}
fn get_year(date: &str) -> Option<u32> {
if date.len() >= 4 {
let s = date[..4];
match s.parse() {
Ok(year) => Some(year),
Err(_) => None
}
} else {
None
}
}
fn main() {
if let Some(year) = get_year("2020-01-01") {
println!("{}", year);
}
}
The Option
type is just a regular enum from Rust’s standard library,
with the two entries None
and Some
.
When returning Option
, empty (null
) values can be returned as None
and non-empty values need to be wrapped, like Some(year)
To simply check if a value is null
, without needing to get the actual
value, is_none()
can be used.
Integer year = getYear("");
if (year == null) {
System.err.println("Invalid date given!");
}
let year: Option<u32> = get_year("");
if year.is_none() {
println!("Invalid date given!");
}
Sometimes it’s useful to run some code only if a value is null
,
to ensure a variable has a non-null
value.
There are many ways to do it in Rust, but using match
offers
the most concise syntax.
String url = "https://github.com";
String content = cache.get(url);
if (content == null) {
content = loadUrl(url);
}
let url = "https://github.com";
let content = match cache.get(url) {
Some(content) => content,
None => load_url(url),
};
Note that in the Rust code, content
is immutable
(to achieve the same in Java, we would need to introduce another local variable or a new method)
Error handling
Rust does not offer exceptions like C++, Java or JavaScript. Instead, error conditions are indicated via the method’s regular return value, like in C or Go.
package main
import (
"fmt"
"log"
"strconv"
)
func ParsePort(port string) (uint16, error) {
p, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return 0, err
}
if p == 0 {
return 0, fmt.Errorf("invalid: %d", p)
}
return uint16(p), nil
}
func main() {
port, err := ParsePort("123")
if err != nil {
log.Fatalf("failed to parse port: %v", err)
}
log.Printf("port: %d", port)
}
use std::error::Error;
fn parse_port(s: &str) -> Result<u16, Box<Error>> {
let port: u16 = s.parse()?;
if port == 0 {
Err(Box::from(format!("invalid: {}", port)))
} else {
Ok(port)
}
}
fn main() {
match parse_port("123") {
Ok(port) => println!("port: {}", port),
Err(err) => panic!("{}", err),
}
}
In Rust it’s common to have statements which cover all possible cases,
either using if
/else
or match
, so you are less likely to encounter
an early return
in Rust than in Go
In the previous example, we needed to use Box<Error>
, because the returned error
type cannot be determined during compile time: It will either contain an
instance of std::num::ParseIntError
(from the
parse
method, when parsing fails), or a string (when the port is zero).
The ?
in the line let port: u16 = s.parse()?
is the
? operator:
when parse
returned an error, parse_port
will return that error,
otherwise the
unwrapped
result of parse
will be assigned to port
.
Input
Rust is often used for terminal applications, so it also supports reading input. The relevant code is in the std::io module.
const { stdin } = require("process");
function readStr() {
return new Promise((resolve, reject) => {
let result = "";
stdin.setEncoding("utf8");
stdin.on("readable", () => {
let chunk;
while ((chunk = stdin.read())) {
result += chunk;
}
});
stdin.once("error", err => reject(err));
stdin.once("end", () => resolve(result));
});
}
use std::io::{stdin, Read};
fn read_str() -> Option<String> {
let mut buffer = String::new();
match stdin().read_to_string(&mut buffer) {
Ok(_) => Some(buffer),
Err(err) => {
eprintln!(
"Error while reading input: {}",
err
);
None
}
};
}
Note that the JavaScript code returns a Promise, so it’s asynchronous, while the Rust example code is synchronous
Attributes (annotations)
Rust also supports attributes (also called annotations in other languages). They are interpreted during compile time and are comparable to preprocessor macros in C or C++.
#include <cstdint>
[[nodiscard]] int32_t add(int32_t a, int32_t b) {
return a + b;
}
int main() {
add(1, 2); // warning: unused return value
}
#[must_use]
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
add(1, 2); // warning: unused return value
}
A very common attribute is
derive,
which can be used to quickly implement traits. It is often used like
#[derive(Debug, PartialEq)]
, to implement the
Debug
and
PartialEq
traits.
Miscellaneous
Package management
Rust usually uses the Cargo package manager, which is like npm or yarn for JavaScript or Maven for Java.
The package registry is available at crates.io.
Project setup
If you have used ESLint before for JavaScript or TypeScript, rust-clippy is a similar tool for Rust, which detects common mistakes and bugs.
The equivalent for prettier or gofmt is rustfmt, which automatically formats code based on the official Rust style guide.
Useful links
- The excellent Rust book: https://doc.rust-lang.org/book/
- Rust by Example: https://doc.rust-lang.org/rust-by-example/
- The Rust Cookbook: https://rust-lang-nursery.github.io/rust-cookbook
- Rust Language Cheat Sheet: https://cheats.rs/
- Rust REPL: https://crates.io/crates/runner
- Comments about Rust: https://brson.github.io/fireflowers/