Move, Copy, and Clone
Ownership moves, copy markers, explicit clone APIs, borrowing, RAII, and drops.
Zynx tracks ownership at compile time. Passing, assigning, returning, awaiting, or storing a move-only value transfers ownership; using the old binding after that transfer is a compile-time error.
struct Resource {
id: i32,
}
fn consume(resource: Resource) -> i32 {
return resource.id;
}
fn main() {
let resource = Resource { id: 42 };
let id = consume(resource);
assert id == 42;
}
The call to consume(resource) moves resource. Reading resource.id after
the call would be rejected.
Copy values stay usable after assignment or by-value calls.
| Copy by default | Notes |
|---|---|
| numeric scalars | integers and floating-point values |
bool, str, null | string literals are borrowed immutable data |
| enums | enum values are copyable |
| raw pointers | *T and const *T are low-level address values |
| SIMD vectors | vector<T, N> values are copyable |
| nullable Copy values | T? is Copy when T is Copy |
User structs are move-only unless they explicitly implement the Copy marker.
struct Point: Copy {
x: i32,
y: i32,
}
fn take(point: Point) -> i32 {
return point.x + point.y;
}
fn main() {
let point = Point { x: 3, y: 4 };
let alias = point;
let total = take(point);
assert point.x == 3;
assert alias.y == 4;
assert total == 7;
}
A Copy struct cannot declare drop, and every field must also be Copy.
Arrays, slices, references, owned strings, callable values, Unique<T>,
error unions, and ordinary resource-owning structs are move-only.
Generic APIs can require copyable values with T: Copy:
fn dup<T: Copy>(value: T) -> T {
return value;
}
fn main() {
assert dup(9) == 9;
assert dup("hello") == "hello";
}
See Interfaces for builtin marker interfaces.
Plain structs, arrays and slices, references, callable values, error unions,
Unique<T>, future.Future<T>, owned strings, sockets, channels, and most handles
are move-checked.
struct Box {
value: i32,
}
fn main() {
let first = Box { value: 7 };
let second = first;
assert second.value == 7;
}
The assignment to second moves first. A later use of first would be
rejected.
Moves are conservative in loops. Moving a value inside a loop is rejected when the next iteration could see the value as already moved.
&T is a mutable reference. const &T is a read-only reference. Assigning to a
mutable reference writes through the reference.
fn read(value: const &i32) -> i32 {
return *value;
}
fn set_to_two(value: &i32) {
value = 2;
}
fn main() {
let count = 1;
assert read(&count) == 1;
set_to_two(&count);
assert count == 2;
}
Ordinary function and callable arguments do not create reference arguments
implicitly. When a parameter expects &T or const &T, pass an explicit
borrow such as &value, or pass an expression that is already reference-valued.
Method receivers keep receiver auto-borrowing for self: &T ergonomics, and
operator overload operands keep receiver-style borrowing because operator syntax
has no explicit argument list.
Mutable borrows are exclusive. While a mutable borrow is live, the owner cannot be mutably borrowed again, immutably borrowed, moved, or reassigned. Borrowing a field or array element also protects the owning aggregate from whole-value reassignment.
Borrow scopes end when the reference binding leaves scope:
fn overwrite(value: &i32) {
value = 9;
}
fn main() {
let value = 1;
if true {
let borrowed: &i32 = &value;
borrowed = 5;
}
overwrite(&value);
assert value == 9;
}
In async fn, a mutable borrow may not live across await or select. End it
before the suspension point, usually by using a smaller block.
Clone is explicit API, not automatic language behavior. Owned standard-library types expose clone-style methods when independent storage is meaningful.
import {
String
} from std.string;
fn main() {
let text = try String("hello");
let copy = try text.clone();
assert text.str() == "hello";
assert copy.str() == "hello";
}
Owned constructors and clone helpers can allocate, so they often return error
unions. String.clone("text") remains available as a legacy alias, but
constructor syntax is the preferred way to create a new owned string from str.
Array<T> can store move-only element types, but methods that duplicate
storage or values require T: Copy. See
Collections.
Types can declare drop(self) to release owned resources. Drops run on scope
exit, reassignment of an owning binding, and explicit std.drop(value).
import std.os;
struct Tracer {
id: i32,
drop(self) {
_ = os.write("drop {self.id}\n");
}
}
fn main() {
let first = Tracer { id: 1 };
let second = first;
_ = os.write("mid {second.id}\n");
}
The value is dropped once, when second leaves scope. Moving transfers the
eventual drop to the new owner; the moved-from binding is not dropped.
When a struct owns fields that also drop, the outer drop(self) body runs
first, then owned fields are dropped.
The destructor syntax is the special struct member drop(self) { ... }, not a
normal fn drop(self) method. See
RAII and Drop for cleanup order,
defer, with, and manual std.drop.
Ownership moves happen in more places than assignment:
- passing a move-only value to a by-value parameter
- returning a move-only value
- capturing a move-only value in a closure
- assigning a callable value to another binding
- awaiting, spawning, detaching, grouping, racing, or timing out a future handle
- sending values through async/channel APIs
- wrapping a move-only value with
Unique(value)
See Closures and Captures, Async / Await, and Memory Model for deeper ownership interactions.