EnriqueMark
Essays

Rust concepts

2026-07 → updated 2026-09 English

The interesting thing about Rust itself is that it’s a strongly typed, memory-safe language, so a lot of things are nailed down hard. It gives up some dynamism and gets a lot of safety and stability back.

Take variables. In Rust a variable is a constant by default, and only the ones you declare specially with let mut are mutable. Then there’s the explicit handling of memory, which you can see in strings:

// this is a pointer to a string view, purely read-only
let s1: &str = "hello";
// this is the actual string object, and it can be modified
let s2: String = String::from("hello");

Rust has a key concept called “ownership”, and you could say it’s the core of the language. But the word ownership can mislead you, especially its literal meaning, owning. What it actually corresponds to is a “responsibility”, and it’s easier to understand as responsibility. At bottom the mechanism exists to handle memory better: what ownership comes down to is, when a scope ends, who has the responsibility of freeing the memory. The owner of that variable does, of course. But “responsible” here means “when the free happens follows the owner”, not that we free it by hand (if you still had to write free by hand, there’d be no feature advantage to talk about). The compiler automatically inserts the freeing code before the owner’s scope ends, so there’s nothing to do by hand. As long as the handoff of responsibility is clear, the free is clear too.

The point of this mechanism is to make sure memory gets freed when it should be. Any value in Rust can only have one owner at a time. The principle is simple too: once the owner’s scope ends, there’s no point in it still taking up memory, so it should be freed right away. Because ownership is single, the free can happen very tightly, and every piece of memory in use is pinned down clearly at compile time. That directly means any piece of memory can be freed when it should be. And since it’s all laid down at compile time, of course you no longer need a separate GC watching over it at run time. Compile time lays memory problems out in plain view, and you also skip the performance overhead a GC brings.

How is it clear? Look at what happens when you pass an argument or assign. There are two cases. First, a transfer of ownership. The operation behind it is move, which stands for the switch of owner, that is, handing the freeing responsibility off. The other is copy. No transfer of ownership happens here; it copies a new value outright (which is also a new owner), and the original value is still valid. From the ownership angle, copy creates a new owner and brings new ownership with it, sitting alongside the old one. move doesn’t: the old owner loses ownership, so only one of the two is left.

Of course there are special cases. An object that owns heap resources, for example, can’t just be copy’d as a matter of course. The default is only ever a transfer, and you have to trigger clone by hand to create a new value and owner. I think anyone who knows shallow vs. deep copy will find this part fairly easy. When an object owns heap resources, it holds both a pointer and the heap memory that pointer points to. So to avoid a copy leaving one heap resource with two owners that can use it, the original owner has to hand over ownership. clone, on the other hand, copies the whole set outright in memory, which is a deep copy. One thing to watch, though: clone in Rust isn’t necessarily tied to a deep copy (Rc, for example); it depends on how the type being operated on defines it. But most of the time it’s a deep copy.

Once ownership is clear, the things built on top of it make more sense, like taking and borrowing:

fn consume(s: String) {
    println!("{}", s);
}

fn main() {
    let a = String::from("hello");
    consume(a);
    // println!("{}", a); // error: a has already been moved
}

This shows something that looks confusing at first. Why is it an error? Going by the habits of other languages, in TS say, an object, or a value (which is an object underneath anyway), can be reused by anyone, and there’s no reason using it after you’ve assigned it to something else should be an error. But in Rust only the owner has permission to operate on it. Once that owner loses ownership, it loses the right to operate on it. s: String here means a transfer of ownership: when a passes itself into the function as the argument, it’s handing its own ownership over to s. s takes over the “cleanup responsibility” that used to be a’s, and what happens here is a move. After that a can’t be used anymore, so the variable is invalid. Because its responsibility/ownership has been marked by the compiler as transferred to s, once s’s scope finishes running, this value’s memory gets freed right then too. After the consume function finishes, the hello assigned to it is cleaned up directly when the scope ends.

So here’s the problem. Done like this, what if I still want to use hello later? You cleaned it all up, so do I have to hand in a new one every time? This is where borrowing comes in:

// note the & below
fn consume(s: &str) {
    println!("{}", s);
}   

let a = String::from("hello");
consume(&a);
consume(&a);
println!("main 里 a 仍然可用: {}", a);    // ✅ all fine 

We said at the very start that &str is a read-only string view, and what & does here is “borrow”. a’s ownership doesn’t transfer at all in this, and naturally neither does the cleanup responsibility, so s only ever gets a view the whole way through, looks, and that’s it. That’s what guarantees a is still usable after consume. Only once a’s scope ends does the memory that belongs to it get freed.

That kind of borrowing is an “immutable borrow”. There’s another kind, the mutable borrow. It can only be lent to one thing at a time, and it has to be declared explicitly with mut.

let mut x = 5; // declared explicitly, this is a variable that can be mutably borrowed
let y = &mut x; // x is mutably borrowed to y
let z = &mut x; // ❌ error! the compiler flat out refuses to compile!

From the responsibility angle this is easy to follow. All it means is that the freeing scope still follows the owner (because the responsibility is still with the owner). Even if the borrower has permission to modify, when its scope ends that memory isn’t freed; the borrow ends and it goes back to the owner. And Rust has a strict rule here: once something’s lent out, the owner’s own range of operations gets frozen too. If you lent it read-only, you can’t modify it yourself anymore, so the borrower doesn’t read the wrong content. That’s a “write freeze”. If what you lent out is mutable permission, then for that stretch you’re “fully frozen”, no reading and no writing.

The borrower’s lifetime is worth a word. Early Rust had a flaw: the borrower’s temporary permission lasted all the way to the closing brace. So if I only wanted to lend something out briefly, I had to write a block by hand, so the owner didn’t stay frozen solid and completely unusable:

fn main() {
    let mut x = 5;
    
    let y = &mut x; // 1. mutable borrow starts, x goes "fully frozen"
    *y += 1;        // 2. y's work is actually completely [done] right here
                    // 3. but in old Rust, y's life is forced to run on to the closing brace
                    
    println!("{}", x); 
    // ❌ error! the old compiler thinks y isn't dead yet, x is still "fully frozen", no reading!
} // 4. in old versions y only really dies here

// what you used to have to write:
let mut x = 5;
{
    let y = &mut x;
    *y += 1;
} // force in a brace so y dies, which unfreezes x
println!("{}", x); // now it can be read

Later Rust brought in NLL, Non-Lexical Lifetimes, which lets a borrower give it back right after its last use and unfreezes the owner. “Lexical” here refers to how it used to follow the braces (a brace is a lexical thing), and now it doesn’t have to (it follows use). There’s nothing hard about it really. Take the example above: the return used to happen at the final }, and in current versions it happens at the *y += 1 step. As long as it isn’t used again anywhere else, the current compiler treats it as legal.

Picking up from What Rust is good for in AI development. Among the things that make Rust hard to learn, there’s also a part that’s easy to mix up, which is the difference between the double colon (::) and the dot (.). Other languages blend these two, but under Rust’s philosophy they have to be kept apart. The first means namespace navigation, indexing into the “structure”, the “thing that exists without a concrete value”, the abstract blueprint in general; unless a constant’s value is predefined right in the blueprint, what you get by default is the member itself. The second is a call on a concrete “value”, the “thing that only exists once there’s a concrete value”, the actual realization of the blueprint.

struct Point {
    x: i32,
    y: i32,
}

impl Point {
	// associated function (no self). By analogy it's a lot like a static method on a class, usable directly without instantiating
    fn new(x: i32, y: i32) -> Point {    
        Point { x, y }
    }
    // method (has self), which means you need a value before you can use this method
    fn distance(&self) -> f64 {          
        ((self.x * self.x + self.y * self.y) as f64).sqrt()
    }
}

let p = Point::new(3, 5);   // :: takes the associated function new off the type Point  ✅
p.x                          // .  takes a field off the instance p                     ✅
p.distance()                 // .  calls a method on the instance p                     ✅
Point::x                     // ❌ illegal, x is a field, not an associated item

One misunderstanding to head off here. The double colon takes members of the type, not the fields inside the type, because only “members” have a path identity and “fields” don’t.

Point::new(3, 5)        // new is an associated function, it has a path identity ✅
Point::ORIGIN_Y         // ORIGIN_Y is an associated constant, it has a path identity ✅
Point::y                // y is a field, no path identity, illegal everywhere ❌

Which brings up another concept along the way. impl looks a lot like a class at first, but it isn’t one. The core point is that Rust bans inheritance at the root, so impl here is only a construct for “attaching methods to a type”, and &self literally points at the type Point itself.

So the “associated function” above, the one that looks like a static method, also looks a lot like a class constructor at first, since new does return a Point instance. It isn’t one. Unlike an ordinary class constructor, which you’re only allowed one of, fn new sits inside the whole impl on completely equal footing with the other methods. Which means you can write as many as you like, and which means it isn’t a “constructor” at all, just an ordinary static (given that Rust has no such thing as a class, this is by analogy) factory method.

As for the other concepts from OOP, Rust does keep some of them. Encapsulation, for one, where the reserved keyword pub designates the interface you want to expose; and unlike other languages where you have to declare private explicitly, properties inside a type are private by default.

mod geometry {
    pub struct Point {
        x: i32,        // private field
        pub y: i32,    // public field
    }
}

// outside the module:
let p = geometry::Point { ... };
p.y    // ✅ accessible, because y is pub
p.x    // ❌ compile error: x is private, can't be touched from outside the module

trait is a more important concept, directly equivalent to interface in other languages, the interface (contract) idea. It’s an abstract definition of behavioral structure at bottom, and it’s a big part of why DI works smoothly in Rust: once the behavioral structure is defined by the interface, the composition that follows can program against the interface instead of the concrete implementation. But a Rust trait can carry a default implementation itself (which starts to look like a class, though obviously it’s the same as before, a predefined method).

trait Animal {
    fn speak(&self) -> String;
    fn greet(&self) -> String {           // default implementation
        format!("我说:{}", self.speak())  // reuses speak, whoever implements Animal gets this greet for free
    }
}

And the strongest, most distinctive thing about this is adding an implementation to the standard library, or to any other type that already exists.

trait Describe {
    fn describe(&self) -> String;
}
impl Describe for i32 {                            // adds behavior to the standard library's i32!
    fn describe(&self) -> String { format!("我是数字 {}", self) }
}

42.describe()    // ✅ "我是数字 42"

Next is that it can clearly separate compile time from run time. This one gets a bit complicated, and it involves how different languages dispatch polymorphism between themselves, that is, whether it’s pinned down at compile time or a vtable gets looked up at run time to work out which concrete function this generic points at. Most garbage-collected languages handle this kind of generic passed in through an interface with dynamic dispatch by default, and you don’t get to choose, so there’s a lookup cost and a runtime mixing problem. Rust makes the distinction explicit with T versus dyn, or rather, by designing two roads, “generics and trait objects”, it makes both of them visible. That in itself is Rust’s design philosophy showing up again, the one about “making the implicit explicit”. Whether it’s the memory control expressed through ownership or explicit boundaries, this is just one more instance of it.

// static dispatch (compile-time polymorphism): generics + trait bound
// T here ends up permanently monomorphized, i.e. whatever is fixed at compile time is what it stays, otherwise you get an error; it isn't left for run time to decide
fn make_speak<T: Animal>(a: T) -> String {
    a.speak()                    // T is any type that implements Animal, pinned at compile time, zero cost
}

// dynamic dispatch (runtime polymorphism): trait object. & means a reference, a pointer to a pointer to a
// this gets decided at run time, and what dyn Animal is here depends on what actually gets passed in at run time
fn make_speak_dyn(a: &dyn Animal) -> String {
    a.speak()                    // only at run time do you know whether it's a Dog or a Cat, look up the vtable
}

// note that vec![...] is only a list construction for the demo, simulating several types passed in at run time
let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for animal in &zoo {
    println!("{}", animal.speak());   // same loop, Dog and Cat each say their own thing -- classic polymorphism
}

Then there’s the point where trait differs most from interface, which is that you can write it empty.

// Send / Sync roughly look like this (simplified):
trait Send {}     // empty! no methods at all
trait Sync {}

Send and Sync here, or really an empty trait like this in general, are a marker that propagates automatically down the chain, and they can stamp a label on the behavior they belong to at compile time. That label lets the author control and declare particular operations explicitly, so the places where an operation shouldn’t happen get blocked at compile time. Take Rc in the standard library. Its purpose is to build an index, letting one piece of data be shared by several owners, and it keeps an owner counter inside, and modifying that thing has a concurrency race problem. That’s where the Send label earns its keep.

The author of Rc only has to declare explicitly that this thing is !Send, and thread::spawn on the cross-thread side plays along by requiring that only operations carrying the Send label get through. With that kind of agreement across methods, dangerous operations get stopped at the compiler instead of surfacing at run time. Generalized, this can be very flexible: concurrency, copying, anything where you can attach a label on one side and set a gate on the other can use a similar mechanism. Operations that don’t meet the gate fail to compile outright, so you’re never left finding out at run time. Once one side sets the label, the child objects that implement this interface all get it propagated to them automatically. A marker that propagates automatically like this is called an auto trait, and Send / Sync are the two most common ones.

That automatic propagation is a Rust specialty, other languages don’t have it, but the other custom empty-shell labels you write yourself don’t get the propagation. Say I wrote one, but later forgot to set up a ticket checker or to stamp the downstream, then the whole thing is void. In other words, how well this label mechanism works depends on the author’s design and on how well he controls the boundaries of his own code. With no design at all, the problems that were going to happen still happen. But then where’s Rust’s extra advantage? The core of it is the compile-time guarantee. Once a Rust empty trait is on, if it isn’t there later then it isn’t there, straight to an error, whereas in something like Java the problem can only surface at run time.

// ① define the label (empty shell)
trait A {}

// ② stamp some type with it
struct Dog;
impl A for Dog {}          // fits A onto Dog

struct Cat;
// Cat doesn't get stamped

// ③ downstream: "check the ticket" in the function signature -- <T: A> is the gate
fn need_ticket<T: A>(x: T) {
    // any x that gets into this function body necessarily has a type carrying A. The compiler has guaranteed it for you.
}

// ④ the consumer tries
need_ticket(Dog);   // ✅ Dog has A, gets through
need_ticket(Cat);   // ❌ Cat has no A, compile error: the trait `A` is not implemented for `Cat`

fn need_ticket<T: A>(x: T) {}              // form 1: mark it right on the generic parameter
fn need_ticket<T>(x: T) where T: A {}      // form 2: where clause, cleaner for complicated bounds
fn need_ticket(x: impl A) {}               // form 3: impl Trait syntax, the most compact

Rust’s advantage over TS here is that this label can’t be papered over downstream with an as assertion. If you insist on doing it you have to declare it explicitly, pin that object’s type onto the label, and the cost is plain: it means everyone downstream knows you added this label (it’s empty, so it doesn’t affect behavior, but the stamp is right there and it’s hard to sneak past anyone). An as assertion is only a temporary claim, and it doesn’t fundamentally change the original object’s type. The other thing is that in Rust the subject of A {} is A, while in TS the subject is {}. A TS type answers to the structure, which is to say an empty {} structure can correspond to any object at all, and that isn’t the semantics of a label.


Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: rust 概念.