Rust for Java developers, part 2
Where did we get so far?
In the first installment of this now
series of Rust for Java developers, we saw some basic
syntax, some pattern matching, briefly touched on enum
and, most importantly,
tried to reason about where the data in our programs resides.
When we quickly tried to save a line of code, we inadvertently changed the scope of an important resource because of RAII and the borrow checker gave us an error when we tried compiling the code:
$ rustc greeter.rs
error[E0716]: temporary value dropped while borrowed
--> greeter.rs:6:43
|
6 | let anybody_at_all = env::args().collect::<Vec<String>>().get(1);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary which is freed while still in use
7 |
8 | let who: &str = if let Some(one) = anybody_at_all {
| -------------- borrow later used here
|
The dreaded borrow checker with borrow later used here
is effectively telling
us a reference (a borrow) to a value owned by, in this case, a temporary is
used while the owner was freed. This is an error message too well known to
developers trying out Rust coming from managed languages. Chances are that if
you started playing with Rust yourself, you’ve came across such a message sooner
or later on your adventure.
Java not only has the garbage collector to free memory only when it isn’t used, it also has the compiler that can optimize heap allocations away entirely transparently. This frees you from thinking about memory, about freeing it or where it is allocated: on the heap or the stack. Rust, on the other hand, pushes that decision of where and how to allocate memory on the developer. Which comes with great power, but also… yeah, responsibility. At least the compiler will not let you produce unsafe code. Which sometimes makes it feel as if the compiler won’t let you get your job done. This generally comes from the coding patterns not accounting for how the data "flows" through your program, because the JVM has spoiled us in not having to think about those things and create a large sea of object where any one can point to, well… everything. So what does Rust do?
Ownership and moves
We’ll start from where we left off in the previous post, i.e. a brand new Rust
project, as created with cargo new
. We’ll change the code a bit, in not too
many surprising ways again, to get the first argument passed, if any, and greet:
use std::env;
fn main() {
let who = env::args().nth(1).unwrap_or(String::from("World"));
println!("Hello, {who}!");
}
Wait a second! This is nothing like we’ve seen previously? Why is all we did
before now a one-liner? Well… yes. We could have made it easier to read. Or at
least I hope this is easier to read. All we need now to get the second,
.nth(1)
, item from the Args
iterator, which still is an Option<String>
; we
then .unwrap
to move the String
out of the Option
if there was Some
argument provided to our program. On the other hand, _or
if there was None
,
we create a heap allocated String
instance from the argument provided to the
method: "World": &str
instead.
There is one new bit of syntax in this code above: String::from()
, which is a
function from()
invoked on the String
type. This would be the equivalent of
a static method call. The from()
we refer to here is to be found on the
struct String
. A struct
holds related data in named fields, like a class
in Java does. But we’ll get back to that in a bit.
So what’s this business with moving the String
out of the Option::Some
?
As we saw in the previous post, the String
has to live somewhere. That’s
what Rust calls ownership. And there can only be one owner of any piece of
data. If there was an argument passed in to our program, the call to
env::args().nth(1)
would return that String
in an Option::Some
variant,
which is the owner of the said String
. Which means that as soon as it goes out
of scope, its content would be Drop
-ped as well and the String
would be
freed. But we don’t want to deal with the Option
indirection throughout our
little program. We only care about having a String
, whether it’s the argument
passed in, or "World"
, we don’t really care beyond getting who
to point the
proper String
for us to print. Which is what this .unwrap_or()
does for us.
Let’s break the two individual bits down, first we’ll only deal with the Option<String>
for simplicity’s sake, so let’s refactor our code to first get that:
let anybody_at_all: Option<String> = env::args().nth(1);
let who: String = anybody_at_all.unwrap_or(String::from("World"));
Now that we have our anybody_at_all: Option<String>
, let’s assume that there
is always an argument provided to our program. We can replace the .unwrap_or
method call, by a straight .unwrap()
, like this:
let who: String = anybody_at_all.unwrap();
This code would now panic
in the absence of an argument actually being
provided. Let’s try this out, first providing an argument, then omitting it:
$ cargo run -- Jane
Compiling rusty-java-2 v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.75s
Running `target/debug/rusty-java-2 Jane`
Hello, Jane!
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/rusty-java-2`
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:5:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Right, so that’s bad, but let’s ignore this for now, we’ll get back to our
.unwrap_or
version soon enough. Focusing on the path that actually succeeds,
calling anybody_at_all.unwrap()
transfers the ownership of the String
it had
to the who: String
variable. The String
from the argument provided, i.e.
"Jane"
in this example, moved from the Option::Some
that was our
anybody_at_all
to who
. At which point you probably wonder what happened to
anybody_at_all
and you’d be right asking. Trial and error remains our approach
here again: let’s try to .unwrap()
the value again:
let anybody_at_all: Option<String> = env::args().nth(1);
let who: String = anybody_at_all.unwrap();
let _again: String = anybody_at_all.unwrap();
What would _again
be? (starting a variable name with underscore tells the
compiler we don’t plan on using it). It’s not a reference. It is also a
String
, as that’s what the Option
contains. So would it be a copy of the
original string? Let’s try and run this:
$ cargo run -- Jane
Compiling rusty-java-2 v0.1.0
error[E0382]: use of moved value: `anybody_at_all`
--> src/main.rs:6:26
|
4 | let anybody_at_all: Option<String> = env::args().nth(1);
| -------------- move occurs because `anybody_at_all` has type `Option<String>`, which does not implement the `Copy` trait
5 | let who: String = anybody_at_all.unwrap();
| -------------- -------- `anybody_at_all` moved due to this method call
| |
| help: consider calling `.as_ref()` or `.as_mut()` to borrow the type's contents
6 | let _again: String = anybody_at_all.unwrap();
| ^^^^^^^^^^^^^^ value used here after move
|
note: this function takes ownership of the receiver `self`, which moves `anybody_at_all`
--> .../lib/rustlib/src/rust/library/core/src/option.rs:775:25
|
775 | pub const fn unwrap(self) -> T {
| ^^^^
For more information about this error, try `rustc --explain E0382`.
error: could not compile `rusty-java-2` due to previous error
Ok, that’s a scary error message: move occurs because … does not implement
the Copy trait
?! Well, no copy of the original String
apparently. So much
on that hypothesis. But we do see something did move! anybody_at_all moved
says the error message! The Option
itself did. Because of some fn
unwrap(self)
method we did indeed invoke. We’ll get back to the syntax later.
But what happened is that the call did result in anybody_at_all
being
consumed, while it transferred ownership of the String
it contained to
who
. So there is nothing we can do with that anybody_at_all
, it "vanished"
and can’t be used beyond line 5, where we did invoke .unwrap()
.
Ownership is transferred from one variable to another within the same scope in a very similar fashion:
let who: String = anybody_at_all.unwrap();
let moved: String = who;
println!("Hello, {who}!");
println!("Hello, {moved}!");
This would not compile, as we transfer the ownership of our String
from the
who
variable to the moved
one. As such who
can not be used there after
and println!
can’t make use of it anymore.
What about the .unwrap_or(String::from("World"))
call? It behaves exactly the
same, other than it doesn’t panic
if we’re dealing with a Option::None
variant. In that case, it returns the argument provided instead. This is a
generic type Option<T>
and its unwrap_or
takes a T
as its default: T
argument. Which means we have to pass it a String
in this case. That’s why we
have to instantiate a string on the heap, unlike what we did in the previous
post and where we dealt with an &str
instead. This program allocates a string on
the heap for "World"
always. This is an argument passed in to a method, and
as such needs to be evaluated, which means the function String::from
is always
invoked. There is an alternative, running cargo clippy
will actually recommend
you use it, which makes use of Rust’s equivalent of Java’s lambda expression to
lazily evaluate the default String
to use and avoids unnecessarily allocate
"World"
on the heap if an argument was already provided. We’ll keep that for a
later post though - feel free to try it though!
struct
and borrowing
Now that we have a fairly good understanding of ownership and move, it’s time we
look a little closer at borrowing. As we said quickly earlier a reference is a
borrow of a value. We’ve been borrowing a fair amount in our previous post, as
we dealt with a reference to a str
from the loaded binary code, or a reference
to the String
that we got from the arguments passed to the executable. All
those borrows were "read-only" ones. Meaning that we couldn’t ever mutate what
the reference was pointing to. They come in opposition to &mut
references,
which are mutable references.
Here comes an important rule, that rustc
will impose on us:
At any given time, you can have either one mutable reference or any number of immutable references.
The Rules of References
Which means that there can be zero or more immutable references "alive" at the same time, but there can only be zero immutable references if there is one (and only one!) mutable reference. This guarantees that no one reads a value, while it is being mutated. While this is useful for concurrent code, we’ll see in a bit that even single threaded code benefits from that guarantee.
But before we look into how this helps us eliminate an entire category of bugs
from our code at compile time, rather than a random RuntimeException
thrown at
us in production, let us first look into how references and borrowing comes into
play with struct
and their impl
blocks. First, how is a struct
declared in
Rust:
struct Greeter {
person: String,
}
A struct
in Rust is the equivalent of a class
in Java. You first declare it
using the struct
keyword, followed by the name you want to give it. The fields
it declares follow the Rust syntax we’ve seen so far: <name>: <type>
. In the
example above, the line declaring the person
field is ended by a comma (,
).
This isn’t strictly necessary as it is the last (well the only) field in our
Greeter
struct. But fields that’d be declared before it would need the comma.
It is considered good practice to always end field declaration with comma, so
should you ever add a new one, the diff in your version control system would be
cleaner.
For now our struct
looks more like a C struct
than a Java class
. At this
stage you probably expect a constructor and some methods. So let’s start with
the former. In this particular case, you wouldn’t need a constructor per se, as
since we put all this within the same file, visibility doesn’t matter. Which, as a
matter of fact, is implicit in our examples, as the struct
isn’t explicitly
declared as pub
. Neither is the field person
. Which means that at any point
in our main
function, we could instantiate such as Greeter
with the
following syntax:
let greeter = Greeter {
person: String::from("Jane"),
};
And, well, there is no constructor as you know them from Java in Rust neither.
You could define a
function on our struct Greeter
that performs some argument manipulation for
instance. Adding a function to a struct
is done with an impl
block for that
same name. Below is an example of a Greeter::new
function that creates a
Person
and returns it (the struct
itself, not a reference to it):
impl Greeter {
fn new(person: String) -> Self {
Self {
person,
}
}
}
Again, the function would be declared public with pub fn new
, if we needed it
exposed outside of this scope. We can see that the function takes ownership of
a String
, which means the caller would move the String
ownership to this
function. And that the function returns, denoted with the arrow →
a… Self
?
Self
is a placeholder for the type of the impl
block. This is useful when
refactoring, or when the type is somewhat verbose. But you could just as well
have the method signature be fn new(person: String) → Greeter
. Follows the
block, within curly braces, that is the function’s body. When creating the
Greeter
this time though, we are not explicitly telling that the field
person
is to own the variable of the same name. Unlike when we previously did
associate it with a freshly created String
: person: String::from("Jane")
.
Them having the same name, is the reason we don’t have to prefix it with
person:
. It’s considered redundant and as such not necessary. Finally, the
single line of the body again misses a ;
at the end, as we are returning the
freshly created Greeter
instance to the caller, i.e. moving ownership of it to
the caller. It is also worth mentioning that this freshly created struct
was
created on the stack. Even if the bytes of the person
's name are eventually
on the heap, as this is a String
.
So Greeter::new
is again the equivalent of a static
method in Java. How
would you add an instance method then? First, let consider what an instance
method even is. An instance method is one that acts on the instance of the
object itself. What happens at runtime is that a reference to the instance is
implicitly passed to a plain old function. Rust makes this a little more
explicit. Below is the code that adds a .hi()
method to the Greeter
struct:
impl Greeter {
fn new(person: String) -> Self {
Self {
person,
}
}
fn hi(&self) {
let who = self.person.as_str();
println!("Hello, {who}!");
}
}
A reference to… self
? Lowercase self this time? Yes. That’s how Rust defines
an instance method. The first argument being &self
, which actually is a
shorthand for self: &Self
, so a binding named self
that is a reference to a
type of Self
, i.e. Greeter
. That first argument can either be a &self
, a
&mut self
or a self
. We’ve seen the self
variant with our Option.unwrap()
at the
beginning, it’s an instance method that consumes the instance. So once invoked
the binding used to invoke the method becomes useless. &mut self
tells the
compiler that we expect a mutable reference, because we’ll be mutating state
related to this struct. One example would be a setter in Java, or e.g. a
set_person
method on our struct Greeter
. In order to reassign a new provided
String
to our person
field, the reference to would need to be mutable:
fn set_person(&mut self, person: String) {
self.person = person;
}
If we’d declare it with &self
, the compiler would complain as we’re trying to mutate self
when assigning the new value to the person
field.
$ cargo run -- Jane
Compiling rusty-java-2 v0.1.0
error[E0594]: cannot assign to `self.person`, which is behind a `&` reference
--> src/main.rs:32:9
|
31 | fn set_person(&self, person: String) {
| ----- help: consider changing this to be a mutable reference: `&mut self`
32 | self.person = person;
| ^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written
For more information about this error, try `rustc --explain E0594`.
error: could not compile `rusty-java-2` due to previous error
Below is our entire main.rs
as it stands now with all these changes… and a few little ones more:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use std::env;
fn main() {
let who = env::args().nth(1).unwrap_or(String::from("World"));
let greeter = Greeter::new(who);
greeter.hi();
greeter.bye();
}
struct Greeter {
person: String,
}
impl Greeter {
fn new(person: String) -> Self {
Self {
person,
}
}
fn hi(&self) {
let who = self.person.as_str();
println!("Hello, {who}!");
}
fn bye(&self) {
println!("... and goodbye, {}!", &self.person);
}
fn person(&self) -> &String {
&self.person
}
fn set_person(&mut self, person: String) {
self.person = person;
}
}
We now also have a getter to the person
field, that returns a reference as
well as a bye()
method. That method’s body is slightly different than from the
hi()
one that we saw before. They both do effectively the same: get a
reference of the person to print their name. On line 23, in the hi()
method,
we get the self.person
field and invoke the .as_str()
which returns a &str
for us to use, while on line 28 , within the println!
macro invocation, we grab
a reference &
to the self.person
, i.e. a &String
. In both cases we do not
move the self.person
out of our struct. That move would be illegal, we only
reference the field.
Which, when we run it, now yields this result:
❯ cargo run -- Jane
Compiling rusty-java-2 v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Running `target/debug/rusty-java-2 Jane`
Hello, Jane!
... and goodbye, Jane!
If we now use this struct to play a bit with borrows, we can test the rule of reference from before:
At any given time, you can have either one mutable reference or any number of immutable references.
The Rules of References
For instance what do you think would this do?
let mut greeter = Greeter::new(String::from("Jane"));
let reference = &greeter;
greeter.set_person(String::from("John"));
reference.hi();
Well, it would not compile, as it violates our simple rule:
let reference = &greeter;
-------- immutable borrow occurs here
greeter.set_person(String::from("John"));
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
reference.hi();
-------------- immutable borrow later used here
The issue becomes visible when you look at the method declaration of
set_person(&mut self, person: Person)
. It takes a &mut self
, so when
invoking the method, we implicitly try to borrow greeter
mutably. Which we
cannot do because a immutable reference is still alive. If we would use
greeter
instead to invoke .hi()
on, the immutable reference would be "dead"
and the code would work…
What do you think would this other simple example does:
let mut greeter = Greeter::new(String::from("Jane"));
let person = greeter.person();
greeter.set_person(String::from("John"));
greeter.hi();
println!("Our previous person: {person}");
This one might be a little less obvious, as we are borrowing a reference to the
person
field of the greeter. But, thinking about it, you’ll probably not be
surprised that the result if pretty much the same as before:
let person = greeter.person();
---------------- immutable borrow occurs here
greeter.set_person(String::from("John"));
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
greeter.hi();
println!("Our previous person: {person}");
------ immutable borrow later used here
The "transitivity" of the borrow of .person
through greeter
itself is
tracked by the compiler and as such we get the same error back. We can’t have
set_person
replace the String
within greeter
with a new one, while we
still have a reference to it. Replacing it would imply Drop
-ping the old
value while it is still being in use. Remember there is no garbage collector to
only clean it up after the last reference is outlived.
You might want to use this very simple example to experiment with the lifecycle of references and see how the rule of references plays out for you. As a Java developer it does take some time to get used to this rule and think more of how your data is handled by your code. Rust requires a little more thinking as where and how to store data and references.
If now you’ve been wondering why this rule is there or might have felt that at times
it is more constraining than actually helpful. Other than the reference to
greeter.person
being modified concurrently by another thread, there isn’t much
added value to this guard than the freeing of the value… or is there? Let’s
consider this simple Java code:
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
for (Integer i : list) {
System.out.println(i);
list.remove(0);
}
We’re iterating over a list, printing out the values and removing them as we go. An this keeps on happening in "real" code, unlike this idiotic example. If you’ve been doing Java for a while, this one is probably obvious to you already, but here is the output of running this:
1
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
at Program.main(Program.java:12)
Well, we didn’t get too far on this one! A ConcurrentModificationException
, on a single thread! Now let’s try the same code in Rust:
let mut items = vec![1, 2, 3];
for i in &items {
println!("{i}");
items.remove(0);
}
Let’s first explain what is going on here. We’re creating a Vec<i32>
from the
handy vec!
a function-like macro again. The type of the elements in the vector
is i32
, signed 32 bit
integer - Java’s int
, as this is what integer literal evaluate to in Rust.
We then iterate over the reference to our vec?! Well… no. The for loop there
has some syntactic sugar added by the compiler. for i in items
- without the
ampersand - would be the equivalent of for i
in items.into_iter()
. Where into_iter()
moves the ownership of the Vec
from vec
to the iterator. Eventually each item’s ownership is moved into the
loop. While i in &items
is the equivalent of i in items.iter()
, which will
provide us with references to each element of vec
in our loop and leave the
collection items
untouched.
The loop block should be straightforward by now. So what happens if we run this?
for i in &items {
------
|
immutable borrow occurs here
immutable borrow later used here
println!("{i}");
items.remove(0);
^^^^^^^^^^^^^^^ mutable borrow occurs here
Well, the rule of references will make it impossible for us to grab a mutable
reference to our items
, which we implicitly do by calling .remove
which
unsurprisingly takes a &mut self
to the Vec
to mutate it. All that while we
are using an immutable reference to iterate over the elements within that Vec
.
What about this code tho:
let mut items = vec![1, 2, 3];
for i in &items {
for i in &items {
println!("{i}");
}
}
This does not violate our rule of references. While there is a keyword mut
present, it is not on a reference, but on the variable which owns the
Vec<i32>
. We then go on to nest iterations, which results in at most two
immutable references alive at the same time, which is not against our rule. If
anything, this code will have the compiler produce warnings if, after these
loops, we don’t mutate our items
as then: variable does not need to be
mutable
and the first i
isn’t used: if this is intentional, prefix it with
an underscore
.
So how are things Drop
-ped then?
When an instance goes out of scope, the compiler will insert some code for us
that is responsible for Drop
-ping the instance. That means freeing up all
resources it might hold on to: more instances, file descriptors, sockets, … and
eventually free up the memory it used, like the memory on heap for our String
instances.
Let’s start a little experiment in trying to understand how Rust deals with
freeing up resources. We’ll build on this hi/bye "idiom" we’ve built with the
Greeter
, but change it slightly. We want it to only say "hi!" once and make
sure it always says "bye". Let’s jump right into it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Scope {
name: String,
}
struct TrackedScope {
name: String,
}
impl Scope {
fn new(name: &str) -> Self {
Scope {
name: String::from(name),
}
}
fn start(self) -> TrackedScope {
println!("Scope {} started", &self.name);
TrackedScope {
name: self.name,
}
}
}
impl Drop for TrackedScope {
fn drop(&mut self) {
println!("Scope {} ended!", &self.name);
}
}
Ok, so what do we know already? We’re declaring two struct
types: Scope
and TrackedScope
. They both have the same structure, containing a heap
allocated String
field called name
.
The Scope
field has one function and one method. The function is straight
forward, but adds some nice usability to our previous Greeter
, it takes a
&str
and converts it to the heap allocated String
before creating the
struct Scope
for us. The method start
is slightly more interesting though.
It doesn’t take reference &self
but consumes self
instead! But before doing
so, it prints that the scope is now started and then returns a TrackedScope
moving the String
instance from the Scope
instance into the newly created
TrackedScope
one that is then returned. So the name is preserved, but the
original Scope
instance is dropped.
Talking about Drop
! We also have an impl
block for TrackedScope
, but it
implements the Drop
trait. The trait, your Java interface equivalent, declares
a single method fn drop(&mut self)
, which we implement here by printing that
the scope has ended. That drop
method invocation is what the compiler will add
for us automatically in the right spot. You cannot invoke that method manually
in your code yourself though, the compiler (again!) will prohibit that.
If you manually want to drop
something, and as such invoke the .drop()
on
the instance, there is a handy utility function available in the standard
library for you to use: std::mem::drop
. You might be wondering how special
that function is and what kind of magic it uses to drop
the actual instance that
you couldn’t possibly be trusted with?! Well, let’s take a look! Below it the
full source code of this mysterious utility function:
pub fn drop<T>(_x: T) {}
Wait?! What? It’s… it’s empty!
Yes it is. The magic happens in the function signature again, it take ownership of the
argument passed in! The method is generic over T
, without any bounds, so it’d
accept anything. And do absolutely nothing with it! But since, when invoking
that function, the ownership of _x
is moved to this scope and then… nothing,
well… it gets dropped! The compiler sees it goes out of scope and insert the
necessary cleanup code for you!
So let’s use our Scope
now to see how the drop
-ing occurs in code,
considering these few cases below:
1
2
3
4
5
6
7
8
9
10
11
let one = Scope::new("1");
let two = Scope::new("2");
let three = Scope::new("3");
let four = Scope::new("4");
let _one = one.start();
{
two.start();
let _three = three.start();
}
let _four = four.start();
Worth while to note is that we add a synthetic block on line 7, which ends on
line 10. Also, the scope two
is not bound to a variable when started. So
what do you think the output should be? Let’s run this and see:
Scope 1 started
Scope 2 started
Scope 2 ended!
Scope 3 started
Scope 3 ended!
Scope 4 started
Scope 4 ended!
Scope 1 ended!
Probably unsurprisingly, three
get dropped at the end of our block on line
10, right before four
gets started. two
though gets dropped right after it
is started, as we never bind the returned TrackedScope
to anything, so it can
be cleaned up right away. While three
only behaves similarly as it’s the last
statement in that block. In the outer scope, we first started one
then four
,
binding the result to a local variable and we can see that they get drop
-ped
in inverted order, first four
, then one
. Which makes sense. As a structure
binding declared later could hold a reference to something declared before,
hence dropping in reverse order cleans things up properly!
Let’s recap
That was quite some content again. We’ve by now explored the concept of
ownership much deeper. How there is only one owner of any given piece of data.
But that ownership can be transferred, otherwise known as move. We’ve then
looked at how we can borrow a reference to data, according to the rule of
references. And finally saw how the rust compiler frees resources by Drop
-ping our struct instances.
We’ve indeed done most of this on struct
by declaring our own new types and
adding functions and methods to them. By doing this, we’ve seen how three
different types of methods that can be declared on structs: &self
, &mut self
and self
and how these differ in their behavior with regards to the borrow
checker and move semantics.
Finally, we got a glimpse at how a for loop works in Rust and it differs from
Java’s. We’ve also illustrated some of the usages of the above principles with
code from the Rust standard library. Just like with Java, that source code is
available for you to read and it is strongly encouraged that you spend some time
reading some of it to get further acquainted with how the Rust standard library
developer write code to leveraging the language solve some basic problems, like
we did for std::mem::drop()
.
I can’t possibly end this, without reiterating one advice: run cargo clippy
often on your code! Spend the time refactoring what you’ve got once it compiled.
And re-run clippy
! Spending some time exploring the language on your own terms
is probably the best way to get a better handle on writing Rust! Coming from
Java, chances are that becoming comfortable with where data should reside, i.e.
which parts of your program own what data, and own to then efficiently share
that data might be somewhat challenging initially. But I do believe that finding
the path of least resistance to how data flows through your code will eventually
lead to you writing better Java too.
There is much more for us to cover. So expect some new post some time this month with the next adventure into Rust coming from Java! I hope this was insightful and, again, please don’t hesitate to leave comments, remarks, suggestions, fixes, complains…
This post is part of the series Rust for Java developers
- Part 1: Rust for Java developers, an introduction
- Part 2: Rust for Java developers, part 2