My name is Nathan Stocks.
I work at GitHub for my day job, but my opinions
on Rust are my own and do not represent any official GitHub
stance.
So I'm also an independent game developer and an online course
instructor with O'Reilly teaching
Rust, which is a fantastic systems language that I love.
If you have questions during this presentation,
please save them till the end.
We have lunch right after this session,
and I'm happy to stay here the entire lunch
and answer all sorts of questions, OK.
I bet you're interested in Rust because you
came to Intro to Rust.
I'm glad you showed up because we're going
to have a fantastic time today.
This is a whirlwind tour of some of the most important things
that I teach in depth in my longer courses.
If you find this interesting, I encourage
you to make something with Rust because that
is how you will really learn.
So without further ado, Rust is awesome.
What is Rust?
It's a systems programming language
pursuing the trifecta, safety, which
is guaranteed at compile time, concurrency,
which is a lot easier when things are safe,
and blazingly fast speed due to zero cost abstractions
and other nice things.
High level scripting languages like Ruby or Python
will get you safety but not concurrency or speed.
On the other hand, systems languages like C or C++ will
give you speed and some access to concurrency,
but forget about safety.
So where did Rust come from?
Rust was started in 2006 as a personal project of a Mozilla
employee named Graydom Hoare.
Mozilla started sponsoring Rust officially in 2009,
and version 1.0 was released in 2015,
which makes Rust about four years
old after a nine-year incubation period.
Now, why would Mozilla sponsor a systems programming language?
Well, because they were sick of C++ and wanted a better
language to program Firefox in.
Did you hear about Firefox Quantum in 2017?
It was a big update.
Suddenly, Firefox was over twice as fast and less buggy.
Why?
Because the core of Firefox was rewritten in Rust.
Today, there's about 1.5 million lines of Rust in Firefox.
So that's where Rust came from.
Let's talk about some of the reasons why Rust is awesome.
I mentioned Rust is blazingly fast.
It's also very memory efficient because it
has no garbage collector and a very minimal runtime.
You can scale it up to use dozens
of cores and a powerful server, or scale it down
to run on a tiny microcontroller with a few kilobytes of RAM.
But that's not all.
Rust is safe and reliable.
Rust guarantees, guarantees memory safety and thread safety
at compile time.
There are whole classes of bugs that Rust eliminates,
dangling pointers, use-after-free,
double-free, null pointer references, buffer overflow,
data races.
Rust has great, high-quality documentation
in the form of both books you can read
and online documentation.
There's a built-in way to document
your own code via comments written in Markdown syntax.
And Rust has a built-in way to convert those comments
into a searchable website.
As a result, every Rust project can have high quality
documentation, including yours.
The standard library in Rust is documented this way.
Rust has a built-in way to write and run tests for your code,
including running tests on code embedded in your documentation,
so your documentation actually stays relevant.
Rust currently has tier one support for eight platforms,
including Mac, Linux, and Windows, as you would expect.
It also has tier two or three support
for over 80 additional platforms, including
exotic things like WebAssembly, microcontrollers, iOS
and Android, NVIDIA GPUs, BSDs, Redox, the Rust operating
system, or Linux on ARM, MIPS, PowerPC, or SPARC.
The Rust community is fantastic and is
known for being friendly, safe, and welcoming
with a code of conduct that embraces diversity.
The community does a lot of planning and looking ahead
to things in the future.
The whole process takes place on GitHub, which
makes it very transparent.
One of the interesting planning topics is around additions.
Last year, the 2018 edition of Rust was released.
The addition concept is to release
a big set of syntactically breaking changes
as an opt-in set of features every three years while still
supporting the older additions and the binary compatibility
between the additions, forever.
So in between editions, there's a dot release steadily every
six weeks that focuses on performance improvements
and other non-breaking fixes or enhancements.
The Rust compiler emits useful error messages.
That's really important because even though the Rust
compiler is very friendly, it's always very, very strict.
I encourage you to look at the compiler as your friend,
as your friend who cares about you.
Because dealing with an error at your desk in the day
is so much nicer than dealing with a segfault in production
in the middle of night.
Editor support for Rust is getting really good.
Auto completion, type inspections, auto formatting,
syntax highlighting, and more is available for most
major editors and IDEs.
Rust comes with a fantastic tool called Cargo.
Cargo is a package manager.
Wait.
A package manager for a viable systems programming language?
Yes.
Finally, systems programmers can have nice things too.
You use Cargo to search for, install, and manage packages
that you want to use.
Cargo is also the build system.
No more make files.
Cargo is also the test runner and the documentation
generator and--
sorry, my slides are not updating.
There we go-- et cetera.
It's like all the good things of NPM, and Pip, and Bundler,
and Make all rolled together.
Let's make a project.
So if you run Cargo new hello and hit Enter,
Cargo will happily and colorfully
create a project named hello for you.
So let's take a look at what Cargo created for us.
A hello directory with a config file
named Cargo.toml and a source subdirectory
with a main.rs file.
So config files in Rust use the TOML format,
which stands for Tom's Obvious Minimal Language.
Rust source code files use the dot rs extensions,
as you can see on main.rs here.
Let's open up Cargo.toml and take a look.
Cargo.toml is the config file for your project,
all in one place.
In fact, it is the authoritative source of information
about your project.
Name is the name of your project,
not the name of the directory or your repo.
Rust uses semantic versioning, meaning the three version
numbers all have a meaning.
Cargo can usually, magically, figure out your name and email
address, and it will set you up with the latest
edition by default, but you can change that.
Now let's open up main.rs in the source directory.
It already has a Hello, world program for us.
Oh, what a fantastic tool Cargo is.
What a helpful language.
Let's go back to our terminal.
So Cargo run will both build and run your executable.
Now, let's pause for a second and go over
the roadmap for this talk.
I already introduced myself.
I told you some awesome things about Rust,
and then I showed you how to create, compile, and run
a Rust project.
So now I'm going to unleash a firehose of Rust language
information.
This is the "crash" in the crash course.
And then I'm going to top it off with why Rust's ownership
model is a game changer.
So let's start with variables.
They're pretty important.
Rust uses curly braces and semicolons
and intentionally mimics as much syntax from other languages
as it can, C and Python in particular.
So it should feel familiar right from the start
for a lot of you.
To declare a variable, you use a "let" statement.
Here we declare bunnies and initialize it
to the value of 2.
Rust is a strongly typed language.
So where is the type annotation?
Well, whenever the Rust compiler can figure out
what the type is from the context,
you can just leave it out.
But when you do annotate the type,
it looks like this, a colon after the variable name
and then the type.
Variables are immutable by default in Rust, which
means unless you choose to make them mutable,
you can't ever change their value.
What happens if we try to change it anyway?
Well, it gives us an error, this error, in fact.
So here's our friendly compiler nudging us
in the right direction with quite a bit of context.
Here they said, make the variable on line 2 mutable.
That's the core of what we're learning here.
So let's do that.
Let's go and put "mut" in front of bunnies,
and this code will work just fine because now bunnies
is a mutable variable.
Now, why are variables immutable by default?
I mean, it may seem confining at first,
but let's think about what Rust cares about, safety.
There are a lot of bugs that can't happen if a value never
changes.
Concurrency, well, data that never changes
can be shared between multiple threads or coroutines
without locks.
And speed, well, the compiler can do extra optimizations
on data it knows never changes.
So that's why variables are immutable by default.
Let's talk about scope.
Variables have a scope, which is the place in the code
that you're allowed to use them.
The scope of a variable begins where it is created and extends
to the end of the block.
Along the way, it's accessible from nested blocks.
A block is a collection of statements inside curly braces,
that includes function bodies.
In this example, x is defined in the main function's block.
Then we create a nested block, and inside the nested block,
we create y.
And then we print x and y, which works just fine because x is
accessible from nested blocks.
Then the block ends, and y is immediately
dropped at this point.
There's no garbage collection.
Values are always immediately dropped
when they go out of scope, which means
the last print won't work.
But fear not, we discover this at compile time.
The error message is pretty clear.
Cannot find value y in this scope.
So Rust guarantees memory safety at compile time.
Let's talk about that.
As part of that, variables must be initialized
before you can use them.
This code won't work because enigma
has been declared but not initialized to a value
before we try to use it.
In fact, it won't even compile.
We get the error use of possibly an initialized variable enigma.
What if we might initialize enigma
depending on some condition?
Well, the compiler won't reason about the value of condition
at compile time, even if it's a literal true or false.
So conditional evaluation is handled at runtime.
So the compiler can't guarantee that enigma will be initialized
before it's used.
But this works.
The compiler can tell that enigma is guaranteed
to be initialized in this case.
And so, as long as the compiler can guarantee that something
is safe, it'll let you do it.
What if you tried the same thing in C?
What if you declared a variable and then
used it before initializing it?
Welcome to the glorious realm of undefined behavior.
On my Mac, I tried it, and I got the number 1.
I'm not sure if that's just what was in memory or what,
but your mileage may vary because your compiler can
choose to do anything it wants to in this situation.
So anyway, let's leave C and its craziness alone.
Now it's time for functions.
You have seen the main function quite a bit already.
Let's talk about functions in general.
Functions are defined using the fn keyword.
It's pronounced "fun."
The Rust style guide says to use snake case
for function names, lowercase word separated by underscores.
One awesome thing about Rust is that functions
don't have to appear in the file before code that calls them.
So feel free to leave main at the top of your main.rs
file if you like.
Hurray for more chronologically arranged source code.
Systems programmers can have nice things too.
Function parameters are always defined with name, colon,
and type, and, of course, multiple parameters
are separated by a comma.
You specify the return type after the parameters
by adding an arrow pointing to the return type.
The arrow is a hyphen and a greater-than symbol.
And then the body of the function is inside a block.
You return a value from a function using return
just like you'd expect.
There's also a shorthand for returning values.
If you leave the semicolon off of the last expression
in a block, then that will be returned
as the value of the block.
In other words, this is the same as this.
Whenever you are returning something
at the end of a block, the shorter way
is the preferred idiomatic Rust way.
Calling the function looks the same
as in most other languages.
Finally, a single Rust function does not
support variable numbers of arguments or different types
for the same argument.
But macros, such as print line, do.
A macro call looks just like a function call,
except the name of a macro always
ends with an exclamation mark.
There are four scalar types, integers, floats, Booleans,
and characters.
Let's go over integer types first.
There's a lot of them.
Unsigned integers start with u followed
by the number of bits the integer has,
consistent across all platforms, except for u size.
u size is the size of the platform's pointer type
and can represent every memory address in the process.
It's also the type you'll usually
use to index into an array or a vector, so you'll use that.
Signed integers are the exact same story,
except they use i for their prefix, i for integer,
I presume.
isize also has the same number of bits
as the platform's pointer type.
The maximum isize value is the upper bound
of object in array size.
This ensures the isize can be used
to calculate differences between pointers
and be able to address every single byte
within a value like a struct.
If you don't annotate an integer literal,
it defaults to 32 because 32-bit integers are generally
the fastest, even on 64-bit architectures.
Now, just because the types have the same number of bits on all
architectures doesn't mean all types are
supported on all architectures.
A 16-bit microcontroller might only support these types.
Integer literals can be specified
in a number of ways, which is particularly unsurprising.
All except the last way can have any number
of ignored underscores inside them to help with readability.
Floating point types are straightforward,
f32 has 32 bits of precision, and f64
has 64 bits of precision. f64 is the default because it has more
precision, but it's slow on 32-bit architectures,
so careful with that.
Floating point literals follow the IEEE-754 standard
but basically look like this.
No special suffix is required.
Now for the Boolean type, the actual type
is specified with bool.
That's easy enough.
The two Boolean literals are true and false, all lowercase.
The character type is misnamed.
Even though you specify it with char,
it actually represents a single Unicode scalar
value, which could be anything from a character
of our alphabet, to a character of someone else's alphabet,
to an idea graph, or a diacritic, or an emoji,
or a non-printable Unicode control
character that could represent anything
from a sound to an action.
A character is always 4 bytes, which,
effectively, makes an array of characters
a UCS-4 or UTF-32 string.
Character literals are specified using single quotes,
and most important of all, characters are fairly useless.
Strings are UTF-8, and characters are not,
so strings do not use characters.
The source files are also UTF-8.
So, chances are, when you want to deal
with a single character, it's going
to be a UTF-8 string and not a character.
So we'll talk about strings in a little bit.
But before that, let's talk about compound types.
Compound types gather multiple values of other types
into one type.
The first compound type is the tuple.
Tuples store multiple values of different types,
use parentheses containing comma-separated values.
The type annotation is pretty intuitive,
parentheses surrounding comma-separated types.
There are two ways to access the members of the tuple.
The first way is to use dot syntax.
Since the tuples fields have no names,
you use their indices starting with 0.
The second way to access members of a tuple is with a pattern.
Please be aware that tuples currently
have a maximum arity of 12, above which you can technically
still use the tuple, but only with limited functionality.
Arity means how many items are in the tuple.
So this tuple type has an arity of four.
So since the current maximum arity is 12,
you can have a dozen elements in your tuple
but not a baker's dozen, at least
not if you want to use all the features.
Arrays store multiple values of the same type.
You can specify them literally with square brackets and commas
or with a value and how many you want separated by a semicolon.
Note that the type annotation for an array
always uses the semicolon form, even
when you specify all the literal values in the array.
You index values in an array, as you would expect,
with square brackets.
What you would not expect, however,
is that arrays are limited to a size of 32, above which they
lose most of their functionality.
Arrays live on the stack, by default, and are fixed size,
so you will usually use vectors or slices
of vectors instead of arrays.
We'll talk about vectors in the collections section.
Next up, control flow, which is awesome in Rust.
If expressions are great, you don't need parentheses
around the condition because everything
between the "if" and the opening curly brace is the condition.
The condition must evaluate to a Boolean
because Rust does not like type coercion
and mostly doesn't coerce types.
If you want to chain a condition,
you use else and if separately.
And, of course, you can finish with else.
If is an expression, not a statement.
Statements don't return values, expressions
do, meaning that we can change this code to this.
So message is assigned the return value
of the if expression.
The braces are not optional, by the way.
You always have to have them, which
means you never hit that terrible situation that you
do in C when you mean to add a second line to the branch
of an if statement, but instead, your second line ends up
being outside the branch body, and it happens unconditionally,
despite your careful indentation.
Isn't that just the worst?
Let's talk about the unconditional loop.
It turns out that if the compiler knows
that a loop is unconditional, there's
some pretty cool optimizations that it can do.
Of course, even unconditional loops need to end,
so you use the break statement for that.
But wait.
There's more.
What if you want to break out of a nested loop?
Can do.
First, annotate the loop that you want
to break out of with a label.
This one is named bob.
Labels have a strange syntax, single apostrophe
beginning an identifier.
Then tell break which loop you want to break out of.
When this code hits the break statement in the inner loop,
it'll break all the way out of the outermost bob loop.
Continue is very similar.
By itself, it continues the innermost loop.
If you give it a label, then it continues the named loop.
While loops have all the same behaviors
as unconditional loops, except they
terminate the loop when their condition evaluates to false.
If you think about it, while loops
are pretty much just syntactic sugar
for putting a negated brake condition at the top
of an unconditional loop.
Now there's no do while construct in Rust.
But it's pretty easy to rearrange this code
to make it happen.
Just put that condition down at the bottom of your loop.
Voila, do while.
Similar to modern scripting languages,
Rust's for loop iterates over any iterable value.
As an added bonus, the for loop can take a pattern
to destructure the items it receives and bind
the inside parts to variables local to the body of the
for loop.
Systems programmers can have nice things too.
You'll probably want to use ranges
with for loops at some point.
The syntax for the range is two dots separating your start
and end points.
The start is inclusive.
The end is exclusive.
So this will count from 0 to 49.
And if you use dot dot equals, then the end
will be inclusive as well.
So this will count from 0 to 50.
String types, it's time.
I'm going to warn you up front, here be dragons.
I'll do me best to steer you right.
There are at least six types of strings in Rust
in the standard library alone.
But we mostly care about two of them that overlap each other.
The first is called a string slice,
and you almost always see it as a borrowed string slice.
We'll talk more about borrowing later.
A literal string is always a string slice.
A string slice is often referred to as a string, which
can be really confusing when you learn that the other string
type is a String with a capital S.
The biggest difference between the two
is that the data and a string slice can't be modified,
but the data in a string can be modified.
You'll often create a string by calling the to_string
method on a string slice or by passing a string
slice to string from.
A string slice is internally made up of a pointer
to some bytes and a length.
A String is made up of a pointer to some bytes, a length,
and a capacity that may be higher than what's currently
being used.
In other words, a string slice is a subset
of a string in more ways than one,
which is why they share a bunch of other characteristics.
For example, both string types are valid UTF-8,
by definition, by compiler enforcement,
and by runtime checks.
Also, strings cannot be indexed by character position.
Why not?
Well, because English is not the only language in the world.
In fact, Google told me that there
were over 6,900 living languages,
and emojis on top of that.
And they all seem to make their way into Unicode.
And strings are Unicode, which means things get complicated.
So let's look at Thai word [SPEAKING THAI]..
Let's say that we wanted to get this thing.
Looks like it should be index 3, right?
Well, ultimately, this string is stored as a vector of 18 bytes.
Would we get what we wanted if we indexed in by bytes?
Well, not even close.
I mean, come on.
Unicode scalars in UTF-8 can be represented
by 1, 2, 3, or 4 bytes.
And you have to traverse the bytes in order
to tell where one scalar ends and the next begins.
In this case, every three bytes is the Unicode scalar.
So if there were a way to index into the scalars,
would we get what we want?
Well, closer, but still off.
Diacritics are one of the types of Unicode scalars
that combine with other Unicode scalars
to produce a different grapheme.
And the grapheme is usually what we care about.
So now you understand that graphemes decompose
into variable amounts of scalars, which decompose
into variable amounts of bytes.
And as part of Rust's emphasis on speed,
indexing operations on standard library collections
are always guaranteed to be constant time operations.
So you can't do that with strings
because of the bytes, which are indexable, aren't guaranteed
to be what people want when they index into a string.
And the graphemes, which people do want,
can only be retrieved after slowly
examining a sequence of bytes.
So when presented with the string, you have some options.
You can use the bytes method to access
the vector of UTF-8 bytes, which you can index into if you want
since bytes are fixed size.
This actually works fine for simple English text
as long as you stick to the portion that overlaps Ascii.
You can use the chars method to retrieve an iterator
that you can use to iterate through the Unicode scalars.
And finally, you can use a crate, or a project,
like Unicode segmentation, which provides handy functions
that return iterators that handle
graphemes of various types.
With each of these approaches, you
know that if you can index into something,
it will be a fast constant time operation,
while if you iterate through something,
it's going to process some variable number of bytes
during each iteration of the loop.
Hopefully, you can sidestep most of these issues
by using one of the many helper methods created
to manipulate strings.
But if you do end up manually using one of the iterators,
iterators have a handy method called nth
that you can use in place of indexing.
And now you know why you have to pick an iterator and use
nth instead of being able to index into a string directly.
Let's learn about structs.
In other languages, you have classes.
In Rust, you have structs.
Structs can have data fields, methods,
and associated functions.
The syntax for the struct and its fields
is the keyword struct and then the name
of the struct in capital camel case, RedFox, in this case,
then curly braces, and then the fields and their type
annotations in a list separated by commas.
And as a nice touch, you can end your last field with a comma
as well so the compiler doesn't yell at you when
you add another field and don't think to add
a comma to the field before it.
This feature is repeated in a lot of Rust constructs
that use commas.
Instantiating a struct is straightforward, though
verbose.
You need to specify a value for every single field by name.
Typically, you would implement an associated function
to use as a constructor to create a RedFox with default
values and then call that.
Methods and associated functions are
defined in an implementation block
that is separate from the struct definition.
The implementation block starts with impl and then the name
of the struct whose functions and methods
you're going to implement.
This is an associated function because it
doesn't have a form of self as its first parameter.
In many other languages, you'd call this a class method.
Associated functions are often used
like constructors in other languages,
with new being the conventional name
to use when you want to create a new struct with default values.
Let's create our RedFox like this.
The scope operator in Rust is double colons,
and we'll use it to access parts of namespace
like Rust constructs.
Once you have an instantiated value,
you get and set fields and call methods with dot syntax
as you do in most languages.
Methods are also defined in the implementation block.
Methods always take some form of self as their first argument.
At this point in the tutorial, for most
of the other languages, I would go over class inheritance
for a little bit, or struct inheritance,
since that's what Rust calls its classes.
It's a short discussion, though, because there
is no struct inheritance.
So, wait.
Is Rust an object-oriented language or not?
Well, it turns out that that's a religious war question
because there's no universally accepted definition of what
an object-oriented language is or isn't.
And since the Rust community doesn't do religious wars,
nobody really cares what the answer is.
The real question is, why doesn't Rust
have struct inheritance?
The answer is because they chose a better way
to solve the problem that we wish inheritance solved.
Traits.
Traits are similar to interfaces in other languages,
and they're really cool.
Rust takes the composition over inheritance approach.
Remember our RedFox struct?
Let's make a trait called noisy.
Traits define required behavior.
In other words, functions and methods that a struct
must implement if it wants to have that trait.
The noisy trait specifies that a struct
must have a method named get_noise
that returns a borrowed string slice if the struct wants
to be noisy.
So let's add an implementation of the noisy trait for RedFox.
Let's take a look at how this works.
We implement the noisy trait for the RedFox struct,
and our implementation of the required get_noise method
is meow?
Which is all great because now we
can start writing generic functions that
accept any value that implements the trait.
So this function takes an item of type T,
which is defined to be anything that
implements the noisy trait.
The function can use any behavior on item
that the noisy trait defines.
So now we have a generic function
that can take any type as long as it is noisy.
So that's pretty cool.
As long as one of either the trait or the struct
is defined in your project, then you can implement
any trait for any struct.
Think about that.
That means you can implement your traits, on any types,
from anywhere, including built-in, standard library,
or some third-party package.
And on your structs, you can implement any trait
from anywhere.
Here I've implemented the noisy trait for the built-in type
u8, which is a byte.
So a generic print noise function
now works fine with bytes.
So let's go through an example of where traits really
seem to shine.
Let's say we have a game with a goose, a Pegasus, and a horse.
These have several attributes in common.
These two can fly.
These two can be ridden.
And these two explode.
You can see how it could get really
tricky to use inheritance to define the correct behavior
that each final value needs to have.
But it's pretty straightforward with traits.
The goose implements the fly and explode traits.
The Pegasus implements the fly and ride traits,
and the horse implements the ride and explode traits.
And we're all happy.
I will call out that you can't define fields
as part of traits.
That may be added in the future some time
if someone can figure out the right way to do it.
In the meantime, the workaround is
to define setter and getter methods in your trait
if you want traits to have data.
So let's go over some collections now.
Vector is a generic collection that holds a bunch of one type
and is useful where you'd use lists
or arrays in other languages.
It's the most commonly used collection by far.
HashMap is a generic collection where
you specify a type for the key and a type for the value,
and you access the values by key.
In some languages, you would call it a dictionary.
The whole point of a hash map is to be
able to insert, look up, and remove values
by key in constant time.
There are a bunch of other collections
that I don't have time to talk about today.
You should check them out later if you're
interested in using them.
Let's learn about enums.
Enums in Rust are more like algebraic data types in Haskell
than C-like enums.
You specify an enum with a keyword enum,
the name of the enum with capital camel case,
and the names of the variants in a block.
You can stop there if you want and use it just
like that, in which case, it sort of is like an enum in C.
Just namespace into the enum, and away you go.
However, the real power of a Rust enum
comes from associating data with the variants.
You can always have a named variant with no data.
A variant can have a single type of data, a tuple of data,
or an anonymous struct of data.
Let's be clear.
An enum is sort of like a union in C, only so much better.
If you create an enum, the value can
be any one of these variants.
So, for example, your dispenser item
could be an empty with no data associated with it,
but you can tell that it's an empty.
Or it could be an ammo with 69 as its value.
Or it could be things with a string and a 7 in there.
Or it could be a place with x and y-coordinates.
It can be any one of these things but only one at a time.
Even better, you can implement functions and methods
for an enum.
You can also use enums with generics.
Option is a generic enum in the standard library
that you'll use all the time.
It represents when you either have something or you don't.
You use patterns to examine enums.
If you want to check for a single variant,
you use the if let expression.
if let takes a pattern that will match one of the variants.
If the pattern does match, then the condition is true,
and the variables inside the pattern
are created for the scope of the if let block.
If the pattern doesn't match, then the condition is false.
So this is pretty handy if you care about one variant matching
or not but not as great if you need to handle
all the variants at once.
In that case, you use the match expression,
which is match and then the variable, whose type supports
matching like an enum.
The body of the match, in braces,
is where you specify patterns followed
by double arrows, which are equal signs followed by greater
than symbols, pointing to an expression that also represents
the return value of that arm of the match.
Matches are like switch statements on serious steroids.
Match expressions require you to write a branch
for every possible outcome.
In other words, the patterns in a match expression
must be exhaustive.
A single underscore all by itself
represents a wild card pattern that matches anything.
So you can use that as a default or an anything else branch.
Note that even though you'll often see blocks
as the expression for a branch arm,
any expression will do, including things like function
calls and bare values.
So if you actually use the return value
of an expression that ends in a curly brace,
so if match if let, et cetera, then you
need to put a semicolon after the closing brace.
Finally, it's time to talk about ownership.
There are three rules to ownership,
and this is why I came to Rust.
First, each value has an owner.
There is no value in memory, no data that doesn't
have a variable that owns it.
Second, there is only one owner of a value.
No variables share ownership.
Other variables may borrow the value, which
we'll talk about in just a minute,
but only one variable owns it.
And three, when the owner goes out of scope,
the value gets dropped immediately.
So let's see ownership in action.
Let's create a string s1 and then create another variable s2
and assign s1's value to it.
What happens to this string is not a copy.
At this point, the value for s1 is moved to s2
because only one variable can own the value.
If we try to go ahead and use s1 after this point,
we get a compiler error.
Borrow of moved value s1.
So what's going on here?
Well, we've got stack and heap sections of memory.
Just a super quick refresher.
The stack stores values in order,
which you can do because values are always fixed size.
And the last value in is the first value out,
which all tends to be very fast because it's
so compact and predictable.
The heap, in contrast, stores values all over the place,
and values tend to get resized and don't
get dropped in any order that correspond
to when and where they were created,
which tends to make using the heap slower than the stack.
Well, what does that have to do with a value being moved?
Let's walk through it.
First, we create s1.
A pointer, a length, and a capacity
get pushed onto the stack.
The value of capacity, in this case, is 3, the value of length
is 3, and the pointer points to some newly allocated
bytes on the heap.
This, altogether, is the value of this string s1.
Then we move s1's value to s2 because we can only
have one owner.
It works like this.
The pointer, length, and capacity all get copied from s1
and pushed its new values on the stack as part of s2.
And if we stopped here, then we wouldn't
have memory safety, which is why Rust immediately
invalidates s1.
It's more than a shallow copy.
It's a move, which is why we can't use s1 anymore.
The value has moved to s2.
What if we don't want to move the value,
but we want to copy it instead.
To make a copy of s1, we would call the clone method.
Well, why is it called clone instead of copy?
Let me explain what it does under the hood,
and then I'll tell you.
Clone performs the same initial copy of the stack
but then also copies the heap data
and adjusts s2's pointer to point to it.
So in both the move and the clone situations,
the three rules are satisfied.
1, the values have owners, and 2, they only have one owner,
and 3, when the variables go out of scope,
the values will be immediately dropped.
They don't have to worry about someone else using it.
The stack and heap data, if there is heap data,
together make up a value.
Rust reserves the term copy for when only the stack data is
being copied.
If there's heap data and pointer updates involved,
then we use the term clone.
In other languages, you might call clone a deep copy.
There's a special trait called copy.
If your type is copy, then it will
be copied instead of moved in a move situation.
This makes sense for small values
that fit entirely on the stack, which
is why the simple primitive types, like integers, floats,
Booleans, and chars, implement copy.
If a type uses the heap at all, then it can't be copied.
You can opt in to implementing copy with your own types
if you make your type of only other copy types.
When a value is dropped, that means
that the destructor if there is one, is immediately run,
the heap portion is immediately freed,
and the stock portion is immediately popped.
So no leaks, no dangling pointers, yes,
happy programmer.
Another move situation, let's start with the same string
and make a function that takes a string and returns nothing.
If we pass s1 to the function, s1
is moved into the local variable in do_stuff,
our function, which means we can't use s1 anymore
because it got moved.
So what do we do?
Well, one option is to move it back when we're done.
We'll just make s1 mutable, add a return type to do_stuff,
and then return s, which gets moved back out of the function
and used to reinitialize s1.
That sounds like a pain.
That's not what we want to do, usually.
Passing ownership of a value to a function
usually means that the function is going
to consume the passed-in value.
For most other cases, you should use references,
which is why it's time to talk about references and borrowing.
Instead of moving our variable, let's use a reference.
Here's our do_stuff function again,
only this time, it takes a reference to a string.
The ampersand before the type indicates a reference
to a type.
When we call do_stuff, we pass it a reference to s1.
There's that ampersand again.
And s1 retains ownership of the value.
Do_stuff borrows a reference to the value.
After the function call, we can use s1 like normal
because the value never moved.
Under the hood, when we create a reference to s1,
Rust creates a pointer to s1.
But you'll almost never talk about pointers in Rust
because the language automatically handles
their creation and destruction for the most part
and makes sure that they're always valid
as well using a concept called lifetimes.
Lifetimes can be summed up as a rule
that references must always be valid,
which means the compiler won't let you create a reference that
outlives the data that it's referencing.
And you can never point to null.
References default to immutable even if the value being
referenced is mutable.
But if we make a mutable reference to a mutable value,
then we can use the reference to change the value.
The syntax for a mutable reference is a little special.
Ampersand mute space variable or type.
So let's stop and go over what references look like again.
So here's a variable, immutable reference
to a variable, mutable reference to a variable, type,
immutable reference to a type, mutable reference to a type.
Naturally, since references are implemented
via pointers, which are dangerous,
Rust has a special rule to keep us safe.
At any given time, you can have either
exactly one mutable reference or any number
of immutable references.
This rule applies across all threads.
So when you consider that references to a variable
may exist in different threads, it
makes it pretty obvious why it's not
safe to have multiple mutable references
to the same variable, at the same time,
without some type of locking.
All these rules I've been talking about
are enforced by the compiler, and by enforced, I
mean, oh, I mean compiler errors, lots
of compiler errors.
And at first, you're like, I hate the compiler.
It just keeps giving me errors.
But then, as you get the hang of it,
you realize you don't get mysterious segfaults anymore.
And the error messages are really pretty nice.
I mean, they're informative and everything.
And if the code compiles, it works.
And that's an amazing feeling.
That's all the toys I could fit in the toy box today.
We're at time.
So where do you go to learn more?
There is an entire set of documentation and a community
to explore it Rust-lang.org, some excellent books,
you could take my half day course on Safari
or at conferences like OSCON, you could poke around with some
fun open source Rust projects on my GitHub account
or in the community.
My username is cleancut at GitHub.
You've been a wonderful audience.
It's been my absolute pleasure to be here with you today.
Thank you very much.
[APPLAUSE]