A half-hour to learn Rust
A half-hour to learn Rust
(⌘ K) Log in
fasterthanlime
Contents
Jan 27, 2020 27 min #rust
Variable bindings
In order to increase fluency in a programming language, one has to read a lot of it. Tuples
Destructuring tuples
But how can you read a lot of it if you don’t know what it means? Statements
Functions
In this article, instead of focusing on one or two concepts, I’ll try to go through as many Rust snippets
Blocks
as I can, and explain what the keywords and symbols they contain mean.
Blocks are expressions
Implicit return
Ready? Go!
Everything is an expression
Field access and method calling
Variable bindings Modules, use syntax
Types are namespaces too
The let keyword The libstd prelude
Structs
let introduces a variable binding:
Struct update syntax
Destructuring structs
let x; // declare "x"
Patterns and destructuring
x = 42; // assign 42 to "x"
Destructuring with if let
let x = 42;
Type annotation
You can specify the variable’s type explicitly with : , that’s a type annotation:
Uninitialized variables
If you declare a name and initialize it later, the compiler will prevent you from using it before it’s
initialized.
let x;
foobar(x); // error: borrow of possibly-uninitialized variable: `x`
x = 42;
let x;
x = 42;
foobar(x); // the type of `x` will be inferred from here
The underscore _ is a special name - or rather, a “lack of name”. It basically means to throw away
something:
Names that start with an underscore are regular names, it’s just that the compiler won’t warn about
them being unused:
Shadowing bindings
Separate bindings with the same name can be introduced - you can shadow a variable binding:
let x = 13;
let x = x + 3;
// using `x` after that line only refers to the second `x`,
//
// although the first `x` still exists (it'll be dropped
// when going out of scope), you can no longer refer to it.
Tuples
Rust has tuples, which you can think of as “fixed-length collections of values of different types”.
Destructuring tuples
Tuples can be destructured when doing an assignment, which means they’re broken down into their
individual fields:
Of course, when destructuring a tuple, _ can be used to throw away part of it:
Statements
The semi-colon marks the end of a statement:
let x = 3;
let y = 5;
let z = y + x;
let x = vec![1, 2, 3, 4, 5, 6, 7, 8]
.iter()
.map(|x| x + 3)
.fold(0, |x, y| x + y);
Functions
fn declares a function.
fn greet() {
println!("Hi there!");
}
And here’s a function that returns a 32-bit signed integer. The arrow indicates its return type:
Blocks
A pair of brackets declares a block, which has its own scope:
// this:
let x = 42;
// is equivalent to this:
let x = { 42 };
let x = {
let y = 1; // first statement
let z = 2; // second statement
y + z // this is the *tail* - what the whole block will evaluate to
};
Implicit return
And that’s why “omitting the semicolon at the end of a function” is the same as returning, ie. these are
equivalent:
Everything is an expression
In this example, std is a crate (~ a library), cmp is a module (~ a source file), and min is a function:
use directives can be used to “bring in scope” names from other namespace:
use std::cmp::min;
Within use directives, curly brackets have another meaning: they’re “globs”. If we want to import both
min and max , we can do any of these:
// this works:
use std::cmp::min;
use std::cmp::max;
// this brings `min` and `max` in scope, and many other things
use std::cmp::*;
Types are namespaces too, and methods can be called as regular functions:
str is a primitive type, but many non-primitive types are also in scope by default.
// this is exactly the same code, but with the *full* path to `Vec`
let v = std::vec::Vec::new();
This works because Rust inserts this at the beginning of every module:
use std::prelude::v1::*;
(Which in turns re-exports a lot of symbols, like Vec , String , Option and Result ).
Structs
Structs are declared with the struct keyword:
struct Vec2 {
x: f64, // 64-bit floating point, aka "double precision"
y: f64,
}
There is a shortcut for initializing the rest of the fields from another struct:
let v3 = Vec2 {
x: 14.0,
..v2
};
This is called “struct update syntax”, can only happen in last position, and cannot be followed by a
comma.
Note that the rest of the fields can mean all the fields:
Destructuring structs
So is this:
And this:
let Vec2 { x, .. } = v;
// this throws away `v.y`
struct Number {
odd: bool,
value: i32,
}
fn main() {
let one = Number { odd: true, value: 1 };
let two = Number { odd: false, value: 2 };
print_number(one);
print_number(two);
}
fn print_number(n: Number) {
if let Number { odd: true, value } = n {
println!("Odd number: {}", value);
} else if let Number { odd: false, value } = n {
println!("Even number: {}", value);
}
}
// this prints:
// Odd number: 1
// Even number: 2
fn print_number(n: Number) {
match n {
Number { odd: true, value } => println!("Odd number: {}", value),
Number { odd: false, value } => println!("Even number: {}", value),
}
}
Exhaustive matches
fn print_number(n: Number) {
match n {
Number { value: 1, .. } => println!("One"),
Number { value: 2, .. } => println!("Two"),
Number { value, .. } => println!("{}", value),
// if that last arm didn't exist, we would get a compile-time error
}
}
fn print_number(n: Number) {
match n.value {
1 => println!("One"),
2 => println!("Two"),
_ => println!("{}", n.value),
}
}
Methods
You can declare methods on your own types:
struct Number {
odd: bool,
value: i32,
}
impl Number {
fn is_strictly_positive(self) -> bool {
self.value > 0
}
}
fn main() {
let minus_two = Number {
odd: false,
value: -2,
};
println!("positive? {}", minus_two.is_strictly_positive());
// this prints "positive? false"
}
Immutability
Variable bindings are immutable by default, which means their interior can’t be mutated:
fn main() {
let n = Number {
odd: true,
value: 17,
};
n.odd = false; // error: cannot assign to `n.odd`,
// as `n` is not declared to be mutable
}
fn main() {
let n = Number {
odd: true,
value: 17,
};
n = Number {
odd: false,
value: 22,
}; // error: cannot assign twice to immutable variable `n`
}
fn main() {
let mut n = Number {
odd: true,
value: 17,
}
n.value = 19; // all good
}
Traits
Traits are something multiple types can have in common:
trait Signed {
fn is_strictly_negative(self) -> bool;
}
Orphan rules
fn main() {
let n = Number { odd: false, value: -44 };
println!("{}", n.is_strictly_negative()); // prints "true"
}
fn main() {
let n: i32 = -44;
println!("{}", n.is_strictly_negative()); // prints "true"
}
fn main() {
let n = Number { odd: true, value: 987 };
let m = -n; // this is only possible because we implemented `Neg`
println!("{}", m.value); // prints "-987"
}
An impl block is always for a type, so, inside that block, Self means that type:
Marker traits
Some traits are markers - they don’t say that a type implements some methods, they say that certain
things can be done with a type.
For example, i32 implements trait Copy (in short, i32 is Copy ), so this works:
fn main() {
let a: i32 = 15;
let b = a; // `a` is copied
let c = a; // `a` is copied again
}
fn print_i32(x: i32) {
println!("x = {}", x);
}
fn main() {
let a: i32 = 15;
print_i32(a); // `a` is copied
print_i32(a); // `a` is copied again
}
But the Number struct is not Copy , so this doesn’t work:
fn main() {
let n = Number { odd: true, value: 51 };
let m = n; // `n` is moved into `m`
let o = n; // error: use of moved value: `n`
}
fn print_number(n: Number) {
println!("{} number {}", if n.odd { "odd" } else { "even" }, n.value);
}
fn main() {
let n = Number { odd: true, value: 51 };
print_number(n); // `n` is moved
print_number(n); // error: use of moved value: `n`
}
fn print_number(n: &Number) {
println!("{} number {}", if n.odd { "odd" } else { "even" }, n.value);
}
fn main() {
let n = Number { odd: true, value: 51 };
print_number(&n); // `n` is borrowed for the time of the call
print_number(&n); // `n` is borrowed again
}
It also works if a function takes a mutable reference - but only if our variable binding is also mut .
fn print_number(n: &Number) {
println!("{} number {}", if n.odd { "odd" } else { "even" }, n.value);
}
fn main() {
// this time, `n` is mutable
let mut n = Number { odd: true, value: 51 };
print_number(&n);
invert(&mut n); // `n is borrowed mutably - everything is explicit
print_number(&n);
}
fn main() {
let n = Number { odd: true, value: 51 };
let mut m = n.clone();
m.value += 100;
print_number(&n);
print_number(&m);
}
let m = n.clone();
let m = std::clone::Clone::clone(&n);
fn main() {
let n = Number { odd: true, value: 51 };
let m = n.clone();
let o = n.clone();
}
fn main() {
let n = Number { odd: true, value: 51 };
let m = n; // `m` is a copy of `n`
let o = n; // same. `n` is neither moved nor borrowed.
}
Deriving traits
Some traits are so common, they can be implemented automatically by using the derive attribute:
#[derive(Clone, Copy)]
struct Number {
odd: bool,
value: i32,
}
// this expands to `impl Clone for Number` and `impl Copy for Number` blocks.
Generics
Generic functions
fn foobar<T>(arg: T) {
// do something with `arg`
}
They can have multiple type parameters, which can then be used in the function’s declaration and its
body, instead of concrete types:
Type parameters usually have constraints, so you can actually do something with them.
fn print<T: Display>(value: T) {
println!("value = {}", value);
}
fn print<T: Debug>(value: T) {
println!("value = {:?}", value);
}
fn print<T>(value: T)
where
T: Display,
{
println!("value = {}", value);
}
Constraints can be more complicated: they can require a type parameter to implement multiple traits:
use std::fmt::Debug;
fn compare<T>(left: T, right: T)
where
T: Debug + PartialEq,
{
println!("{:?} {} {:?}", left, if left == right { "==" } else { "!=" }, right);
}
fn main() {
compare("tea", "coffee");
// prints: "tea" != "coffee"
}
Monomorphization
Generic functions can be thought of as namespaces, containing an infinity of functions with different
concrete types.
Same as with crates, and modules, and types, generic functions can be “explored” (navigated?) using
::
fn main() {
use std::any::type_name;
println!("{}", type_name::<i32>()); // prints "i32"
println!("{}", type_name::<(f64, char)>()); // prints "(f64, char)"
}
This is lovingly called turbofish syntax, because ::<> looks like a fish.
Generic structs
struct Pair<T> {
a: T,
b: T,
}
fn print_type_name<T>(_val: &T) {
println!("{}", std::any::type_name::<T>());
}
fn main() {
let p1 = Pair { a: 3, b: 9 };
let p2 = Pair { a: true, b: false };
print_type_name(&p1); // prints "Pair<i32>"
print_type_name(&p2); // prints "Pair<bool>"
}
Example: Vec<T>
fn main() {
let mut v1 = Vec::new();
v1.push(1);
let mut v2 = Vec::new();
v2.push(false);
print_type_name(&v1); // prints "Vec<i32>"
print_type_name(&v2); // prints "Vec<bool>"
}
Speaking of Vec , it comes with a macro that gives more or less “vec literals”:
fn main() {
let v1 = vec![1, 2, 3];
let v2 = vec![true, false, true];
print_type_name(&v1); // prints "Vec<i32>"
print_type_name(&v2); // prints "Vec<bool>"
}
Macros
All of name!() , name![] or name!{} invoke a macro. Macros just expand to regular code.
fn main() {
println!("{}", "Hello there!");
}
fn main() {
use std::io::{self, Write};
io::stdout().lock().write_all(b"Hello there!\n").unwrap();
}
fn main() {
panic!("This panics");
}
// output: thread 'main' panicked at 'This panics', src/main.rs:3:5
fn main() {
let o1: Option<i32> = Some(128);
o1.unwrap(); // this is fine
enum Option<T> {
None,
Some(T),
}
impl<T> Option<T> {
fn unwrap(self) -> T {
// enums variants can be used in patterns:
match self {
Self::Some(t) => t,
Self::None => panic!(".unwrap() called on a None option"),
}
}
}
fn main() {
let o1: Option<i32> = Some(128);
o1.unwrap(); // this is fine
Lifetimes
Variables bindings have a “lifetime”:
fn main() {
// `x` doesn't exist yet
{
let x = 42; // `x` starts existing
println!("x = {}", x);
// `x` stops existing
}
// `x` no longer exists
}
fn main() {
// `x` doesn't exist yet
{
let x = 42; // `x` starts existing
let x_ref = &x; // `x_ref` starts existing - it borrows `x`
println!("x_ref = {}", x_ref);
// `x_ref` stops existing
// `x` stops existing
}
// `x` no longer exists
}
The lifetime of a reference cannot exceed the lifetime of the variable binding it borrows:
fn main() {
let x_ref = {
let x = 42;
&x
};
println!("x_ref = {}", x_ref);
// error: `x` does not live long enough
}
Borrowing rules (one or more immutable borrows XOR one mutable borrow)
fn main() {
let x = 42;
let x_ref1 = &x;
let x_ref2 = &x;
let x_ref3 = &x;
println!("{} {} {}", x_ref1, x_ref2, x_ref3);
}
fn main() {
let mut x = 42;
let x_ref = &x;
x = 13;
println!("x_ref = {}", x_ref);
// error: cannot assign to `x` because it is borrowed
}
fn main() {
let mut x = 42;
let x_ref1 = &x;
let x_ref2 = &mut x;
// error: cannot borrow `x` as mutable because it is also borrowed as immutable
println!("x_ref1 = {}", x_ref1);
}
fn print(x: &i32) {
// `x` is borrowed (from the outside) for the
// entire time this function is called.
}
Functions with reference arguments can be called with borrows that have different lifetimes, so:
// named lifetimes:
fn print<'a>(x: &'a i32) {}
This allows returning references whose lifetime depend on the lifetime of the arguments:
struct Number {
value: i32,
}
fn main() {
let n = Number { value: 47 };
let v = number_value(&n);
// `v` borrows `n` (immutably), thus: `v` cannot outlive `n`.
// While `v` exists, `n` cannot be mutably borrowed, mutated, moved, etc.
}
Lifetime elision
When there is a single input lifetime, it doesn’t need to be named, and everything has the same
lifetime, so the two functions below are equivalent:
Structs can also be generic over lifetimes, which allows them to hold references:
struct NumRef<'a> {
x: &'a i32,
}
fn main() {
let x: i32 = 99;
let x_ref = NumRef { x: &x };
// `x_ref` cannot outlive `x`, etc.
}
struct NumRef<'a> {
x: &'a i32,
}
fn main() {
let x: i32 = 99;
let x_ref = as_num_ref(&x);
// `x_ref` cannot outlive `x`, etc.
}
struct NumRef<'a> {
x: &'a i32,
}
fn main() {
let x: i32 = 99;
let x_ref = as_num_ref(&x);
// `x_ref` cannot outlive `x`, etc.
}
impl<'a> NumRef<'a> {
fn as_i32_ref(&'a self) -> &'a i32 {
self.x
}
}
fn main() {
let x: i32 = 99;
let x_num_ref = NumRef { x: &x };
let x_i32_ref = x_num_ref.as_i32_ref();
// neither ref can outlive `x`
}
impl<'a> NumRef<'a> {
fn as_i32_ref(&self) -> &i32 {
self.x
}
}
You can elide even harder, if you never need the name:
impl NumRef<'_> {
fn as_i32_ref(&self) -> &i32 {
self.x
}
}
struct Person {
name: &'static str,
}
fn main() {
let p = Person {
name: "fasterthanlime",
};
}
struct Person {
name: &'static str,
}
fn main() {
let name = format!("fasterthan{}", "lime");
let p = Person { name: &name };
// error: `name` does not live long enough
}
In that last example, the local name is not a &'static str , it’s a String . It’s been allocated dynamically,
and it will be freed. Its lifetime is less than the whole program (even though it happens to be in main ).
struct Person<'a> {
name: &'a str,
}
fn main() {
let name = format!("fasterthan{}", "lime");
let p = Person { name: &name };
// `p` cannot outlive `name`
}
or
struct Person {
name: String,
}
fn main() {
let name = format!("fasterthan{}", "lime");
let p = Person { name: name };
// `name` was moved into `p`, their lifetimes are no longer tied.
}
Tools like clippy will suggest making those changes, and even apply the fix programmatically if you let
it.
Slices
fn main() {
let v = vec![1, 2, 3, 4, 5];
let v2 = &v[2..4];
println!("v2 = {:?}", v2);
}
// output:
// v2 = [3, 4]
Operator overloading
The above is not magical. The indexing operator ( foo[index] ) is overloaded with the Index and IndexMut
traits.
The .. syntax is just range literals. Ranges are just a few structs defined in the standard library.
They can be open-ended, and their rightmost bound can be inclusive, if it’s preceded by = .
fn main() {
// 0 or greater
println!("{:?}", (0..).contains(&100)); // true
// strictly less than 20
println!("{:?}", (..20).contains(&20)); // false
// 20 or less than 20
println!("{:?}", (..=20).contains(&20)); // true
// only 3, 4, 5
println!("{:?}", (3..6).contains(&4)); // true
}
fn main() {
let x = &[1, 2, 3, 4, 5];
let y = tail(x);
println!("y = {:?}", y);
}
This is legal:
fn main() {
let y = {
let x = &[1, 2, 3, 4, 5];
tail(x)
};
println!("y = {:?}", y);
}
fn main() {
let y = {
let v = vec![1, 2, 3, 4, 5];
tail(&v)
// error: `v` does not live long enough
};
println!("y = {:?}", y);
}
fn main() {
let name = "Read me. Or don't.txt";
if let Some(ext) = file_ext(name) {
println!("file extension: {}", ext);
} else {
println!("no file extension");
}
}
fn main() {
let ext = {
let name = String::from("Read me. Or don't.txt");
file_ext(&name).unwrap_or("")
// error: `name` does not live long enough
};
println!("extension: {:?}", ext);
}
fn main() {
let s = std::str::from_utf8(&[240, 159, 141, 137]);
println!("{:?}", s);
// prints: Ok(" ")
fn main() {
let s = std::str::from_utf8(&[240, 159, 141, 137]).unwrap();
println!("{:?}", s);
// prints: " "
fn main() {
let s = std::str::from_utf8(&[195, 40]).expect("valid utf-8");
// prints: thread 'main' panicked at 'valid utf-8: Utf8Error
// { valid_up_to: 0, error_len: Some(1) }', src/libcore/result.rs:1165:5
}
fn main() {
match std::str::from_utf8(&[240, 159, 141, 137]) {
Ok(s) => println!("{}", s),
Err(e) => panic!(e),
}
// prints
}
fn main() {
if let Ok(s) = std::str::from_utf8(&[240, 159, 141, 137]) {
println!("{}", s);
}
// prints
}
Dereferencing
The * operator can be used to dereference, but you don’t need to do that to access fields or call
methods:
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 1.0, y: 3.0 };
let p_ref = &p;
println!("({}, {})", p_ref.x, p_ref.y);
}
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 1.0, y: 3.0 };
let p_ref = &p;
negate(*p_ref);
// error: cannot move out of `*p_ref` which is behind a shared reference
}
fn main() {
let p = Point { x: 1.0, y: 3.0 };
let p_ref = &p;
negate(*p_ref); // ...and now this works
}
Their parameters are a comma-separated list of names within a pair of pipes ( | ). They don’t need curly
braces, unless you want to have multiple statements.
fn for_each_planet<F>(f: F)
where F: Fn(&'static str)
{
f("Earth");
f("Mars");
f("Jupiter");
}
fn main() {
for_each_planet(|planet| println!("Hello, {}", planet));
}
// prints:
// Hello, Earth
// Hello, Mars
// Hello, Jupiter
fn for_each_planet<F>(f: F)
where F: Fn(&'static str)
{
f("Earth");
f("Mars");
f("Jupiter");
}
fn main() {
let greeting = String::from("Good to see you");
for_each_planet(|planet| println!("{}, {}", greeting, planet));
// our closure borrows `greeting`, so it cannot outlive it
}
fn for_each_planet<F>(f: F)
where F: Fn(&'static str) + 'static // `F` must now have "'static" lifetime
{
f("Earth");
f("Mars");
f("Jupiter");
}
fn main() {
let greeting = String::from("Good to see you");
for_each_planet(|planet| println!("{}, {}", greeting, planet));
// error: closure may outlive the current function, but it borrows
// `greeting`, which is owned by the current function
}
fn main() {
let greeting = String::from("You're doing great");
for_each_planet(move |planet| println!("{}, {}", greeting, planet));
// `greeting` is no longer borrowed, it is *moved* into
// the closure.
}
An FnMut needs to be mutably borrowed to be called, so it can only be called once at a time.
This is legal:
fn foobar<F>(f: F)
where F: Fn(i32) -> i32
{
println!("{}", f(f(2)));
}
fn main() {
foobar(|x| x * 2);
}
// output: 8
This isn’t:
fn foobar<F>(mut f: F)
where F: FnMut(i32) -> i32
{
println!("{}", f(f(2)));
// error: cannot borrow `f` as mutable more than once at a time
}
fn main() {
foobar(|x| x * 2);
}
fn foobar<F>(mut f: F)
where F: FnMut(i32) -> i32
{
let tmp = f(2);
println!("{}", f(tmp));
}
fn main() {
foobar(|x| x * 2);
}
// output: 8
fn foobar<F>(mut f: F)
where F: FnMut(i32) -> i32
{
let tmp = f(2);
println!("{}", f(tmp));
}
fn main() {
let mut acc = 2;
foobar(|x| {
acc += 1;
x * acc
});
}
// output: 24
fn foobar<F>(f: F)
where F: Fn(i32) -> i32
{
println!("{}", f(f(2)));
}
fn main() {
let mut acc = 2;
foobar(|x| {
acc += 1;
// error: cannot assign to `acc`, as it is a
// captured variable in a `Fn` closure.
// the compiler suggests "changing foobar
// to accept closures that implement `FnMut`"
x * acc
});
}
FnOnce closures can only be called once. They exist because some closure move out variables that have
been moved when captured:
fn foobar<F>(f: F)
where F: FnOnce() -> String
{
println!("{}", f());
}
fn main() {
let s = String::from("alright");
foobar(move || s);
// `s` was moved into our closure, and our
// closures moves it to the caller by returning
// it. Remember that `String` is not `Copy`.
}
fn foobar<F>(f: F)
where F: FnOnce() -> String
{
println!("{}", f());
println!("{}", f());
// error: use of moved value: `f`
}
And, if you need convincing that our closure does move s , this is illegal too:
fn main() {
let s = String::from("alright");
foobar(move || s);
foobar(move || s);
// use of moved value: `s`
}
fn main() {
let s = String::from("alright");
foobar(|| s.clone());
foobar(|| s.clone());
}
fn main() {
foobar(32, 64, |x, y| x > y);
}
fn main() {
foobar(32, 64, |_, _| panic!("Comparing is futile!"));
}
fn main() {
countdown(3, |i| println!("tick {}...", i));
}
// output:
// tick 3...
// tick 2...
// tick 1...
fn main() {
countdown(3, |_| ());
}
Loops, iterators
Anything that is iterable can be used in a for in loop.
We’ve just seen a range being used, but it also works with a Vec :
fn main() {
for i in vec![52, 49, 21] {
println!("I like the number {}", i);
}
}
Or a slice:
fn main() {
for i in &[52, 49, 21] {
println!("I like the number {}", i);
}
}
// output:
// I like the number 52
// I like the number 49
// I like the number 21
Or an actual iterator:
fn main() {
// note: `&str` also has a `.bytes()` iterator.
// Rust's `char` type is a "Unicode scalar value"
for c in "rust".chars() {
println!("Give me a {}", c);
}
}
// output:
// Give me a r
// Give me a u
// Give me a s
// Give me a t
Even if the iterator items are filtered and mapped and flattened:
fn main() {
for c in "SuRPRISE INbOUND"
.chars()
.filter(|c| c.is_lowercase())
.flat_map(|c| c.to_uppercase())
{
print!("{}", c);
}
println!();
}
// output: UB
Returning closures
You can return a closure from a function:
fn main() {
// you can use `.into()` to perform conversions
// between various types, here `&'static str` and `String`
let test = make_tester("hunter2".into());
println!("{}", test("******"));
println!("{}", test("hunter2"));
}
You can even move a reference to some of a function’s arguments, into a closure it returns:
fn main() {
let test = make_tester("hunter2");
println!("{}", test("*******"));
println!("{}", test("hunter2"));
}
// output:
// false
// true
Conclusion
Conclusion
And with that, we hit the 30-minute estimated reading time mark, and you should be able to read most
of the Rust code you find online.
Writing Rust is a very different experience from reading Rust. On one hand, you’re not reading the
solution to a problem, you’re actually solving it. On the other hand, the Rust compiler helps out a lot.
The Rust compiler has high-quality diagnostics (which include suggestions) for all the mistakes featured
in this article.
And when there’s a hint missing, the compiler team is not afraid to add it.
Read Rust
This Week In Rust
I also blog about Rust and post a lot about Rust on Mastodon and Twitter a lot, so if you liked this
article, you know what to do!
Have fun!
Comment on /r/fasterthanlime
Thanks to my sponsors:
My work is sponsored by people like you. Donate now so it can keep going:
There’s a question that always comes up when people pick up the Rust programming language:
why are there two string types? Why is there String , and &str ?
My Declarative Memory Management article answers the question partially, but there is a lot
more to say about it, so let’s run a few experiments and see if we can conjure up a thorough
defense of Rust’s approach over, say, C’s.
Read more