Yet another Rust ownership tutorial

Published: (December 11, 2025 at 04:02 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

One of the most important concepts to master in Rust is ownership and borrowing. Tons of articles are dedicated to this narrow subject; this tutorial tries to explain the concept with examples.

Ownership is a set of rules that govern how a Rust program manages memory. Rust uses a system of ownership with compile‑time checks; if any rule is violated the program won’t compile, and there is no runtime overhead.

What Is Ownership?

First taste of ownership

Java version

public void own(String text) {}

public static void main(String[] args) {
    var text = "my text";
    own(text);
    System.out.println(text);
}

Rust translation

fn own(_: String) {}

fn main() {
    let text: String = String::from("my text"); // 1
    own(text);
    println!("{}", text);
}

The compiler complains:

error[E0382]: borrow of moved value: `text`
 --> src/main.rs:11:20
  |
9 |     let text: String = String::from("my text");
  |         ---- move occurs because `text` has type `String`, which does not implement the `Copy` trait
10|     own(text);
  |         ---- value moved here
11|     println!("{}", text);
  |                    ^^^^ value borrowed here after move
...
help: consider cloning the value if the performance cost is acceptable
10|     own(text.clone());

The own function takes ownership of text, so text cannot be used afterwards.

Clone and Copy

If we follow the compiler’s advice:

fn own(text: String) {}

fn main() {
    let text = String::from("my text");
    own(text.clone()); // deep copy
    println!("{}", text);
}

String implements Clone, which creates a deep copy. This doubles the memory usage for the value.

When a type also implements Copy, the value is duplicated implicitly and we don’t need to call clone():

#[derive(Debug, Copy, Clone)]
struct Dummy {}

fn own(_: Dummy) {}

fn main() {
    let dummy = Dummy {};
    own(dummy);               // copied automatically
    println!("{:?}", dummy); // still usable
}

Note: Implementing Copy for large structures can be memory‑hungry because every move creates a copy.

Passing by reference

Passing a value moves ownership. To let a function borrow the value, pass a reference with &:

#[derive(Debug)]
struct Dummy {}

fn borrow(_: &Dummy) {}

fn main() {
    let dummy = Dummy {};
    borrow(&dummy);          // borrow, not move
    println!("{:?}", dummy); // still usable
}

Reading fields through a reference:

#[derive(Debug)]
struct Dummy {
    foo: String,
}

fn borrow(dummy: &Dummy) {
    println!("{:?}", dummy.foo);
}

fn main() {
    let dummy = Dummy { foo: String::from("Foo") };
    borrow(&dummy);
    println!("{:?}", dummy); // Dummy { foo: "Foo" }
}

Mutating data

Trying to mutate without mut

#[derive(Debug)]
struct Dummy {
    foo: String,
}

fn main() {
    let dummy = Dummy { foo: String::from("Foo") };
    dummy.foo = String::from("Bar"); // error
}

Compiler error:

error[E0594]: cannot assign to `dummy.foo`, as `dummy` is not declared as mutable
 --> src/main.rs:8:5
  |
8 |     dummy.foo = String::from("Bar");
  |     ^^^^^^^^^ cannot assign
  |
help: consider changing this to be mutable
7 |     let mut dummy = Dummy { foo: String::from("Foo") };

Making the variable mutable

#[derive(Debug)]
struct Dummy {
    foo: String,
}

fn main() {
    let mut dummy = Dummy { foo: String::from("Foo") };
    dummy.foo = String::from("Bar");
}

Mutating a parameter by value

#[derive(Debug)]
struct Dummy {
    foo: String,
}

fn mutate(mut dummy: Dummy) {
    dummy.foo = String::from("Bar");
    println!("{:?}", dummy.foo);
}

fn main() {
    let dummy = Dummy { foo: String::from("Foo") };
    mutate(dummy);
}

Mutating through a mutable reference

#[derive(Debug)]
struct Dummy {
    foo: String,
}

fn mutate(dummy: &mut Dummy) {
    dummy.foo = String::from("Bar");
    println!("Inside: {:?}", dummy.foo);
}

fn main() {
    let mut dummy = Dummy { foo: String::from("Foo") };
    mutate(&mut dummy);
    println!("Outside: {:?}", dummy.foo);
}

Summary

GoalHow to achieve it
Take ownershipPass by value
Duplicate the valueImplement Clone and call clone()
Automatically duplicate the valueImplement Copy (implies Clone)
Borrow (read‑only)Pass a reference &T
Mutate a local variableDeclare it mut
Mutate through a referenceUse &mut T and declare the binding mut

Originally published at A Java Geek on December 7th, 2025

Back to Blog

Related posts

Read more »