Basics & Running
Hello, World
A Rust program is
fn main() { … }. The runnable cells here show the body; the runner wraps it in main (Rust even allows nested fn/struct/trait items inside a function), and println! — with the ! — is a macro, not a function.void main() {
print('Hello, World!');
} println!("Hello, World!"); The
! on println! marks it as a macro, expanded at compile time — that is how it type-checks its format arguments, something an ordinary function cannot do. vec!, format!, and assert! are macros too. Rust is compiled ahead of time to a native binary with no runtime or garbage collector, which is the root of nearly every difference on this page.Immutable by default
void main() {
final fixed = 1; // final: cannot reassign
var counter = 0; // var: mutable
counter += 1;
print('$fixed $counter');
} let fixed = 1; // immutable — the DEFAULT
let mut counter = 0; // opt IN to mutation with 'mut'
counter += 1;
// fixed = 2; // error: cannot assign twice to immutable 'fixed'
println!("{} {}", fixed, counter); Rust inverts Dart's default: a plain
let is immutable (like final), and you must write let mut to allow reassignment (Dart's var). Immutability here is deep and compiler-enforced — a let binding to a struct freezes its fields too — which the borrow checker relies on. Reaching for mut is a conscious, visible choice rather than the path of least resistance.Inference & format strings
void main() {
final answer = 42;
final ratio = 3.5;
final greeting = 'hello';
print('$answer $ratio $greeting');
} let answer = 42; // inferred i32
let ratio = 3.5; // inferred f64
let greeting = "hello"; // inferred &str
println!("{answer} {ratio} {greeting}"); // inline captured args
println!("{} {}", answer, ratio); // or positional Local type inference is strong, as in Dart, but the integer type still matters —
42 is an i32 by default, and Rust does no implicit numeric widening. Interpolation is unusual: println!("{answer}") captures a variable by name (edition 2021+), or you use positional {} holes filled by later arguments. There is no $ sigil and the placeholders are checked at compile time.if is an expression
void main() {
final score = 72;
final grade = score >= 60 ? 'pass' : 'fail'; // ternary
print(grade);
} let score = 72;
let grade = if score >= 60 { "pass" } else { "fail" }; // if is an expression
println!("{grade}"); Rust has no ternary operator, because it does not need one:
if/else is an expression that yields a value, so let grade = if … { … } else { … } does the ternary's job. Both arms must produce the same type. The same is true of match and block expressions, so assigning the result of a control-flow construct is idiomatic — much as in Dart's switch expressions.Ownership & Borrowing
Assignment moves ownership
Rust's defining idea, and the one with no Dart analog. Every value has a single owner; when the owner goes out of scope the value is freed — no garbage collector. Assigning a non-copy value moves ownership.
void main() {
final first = ['a', 'b'];
final second = first; // both refer to the SAME list
second.add('c');
print(first); // [a, b, c] — shared reference
} let first = String::from("hello");
let second = first; // ownership MOVES from first to second
// println!("{first}"); // error: 'first' was moved and is no longer usable
println!("{second}"); // second owns the string now In Dart
second = first aliases one heap object. In Rust it moves ownership: first is statically invalidated, so using it afterward is a compile error, not a run-time surprise. This guarantees exactly one owner frees the memory, exactly once, with no GC and no double-free. Types that are cheap to duplicate (integers, bool, char) are Copy instead and are duplicated rather than moved.Borrowing with &
int lengthOf(List<String> items) => items.length;
void main() {
final words = ['a', 'b', 'c'];
final count = lengthOf(words); // Dart passes the reference; no ownership rules
print('$count ${words.length}');
} fn length_of(items: &Vec<String>) -> usize {
items.len() // read through a shared borrow
}
let words = vec![String::from("a"), String::from("b")];
let count = length_of(&words); // LEND a reference, don't move
println!("{} {}", count, words.len()); // words is still owned here Passing a value to a function would normally move it, so to keep using it you borrow instead:
&words lends a read-only reference, and the caller retains ownership. You may have any number of shared & borrows at once. The compiler tracks these borrows and forbids using the value in ways that would conflict — the core of how Rust prevents dangling references without a garbage collector.Exclusive &mut borrows
void addItem(List<String> items, String value) => items.add(value);
void main() {
final basket = <String>[];
addItem(basket, 'apple');
addItem(basket, 'pear');
print(basket);
} fn add_item(items: &mut Vec<String>, value: &str) {
items.push(value.to_string()); // mutate through an exclusive borrow
}
let mut basket: Vec<String> = Vec::new();
add_item(&mut basket, "apple"); // lend it mutably
add_item(&mut basket, "pear");
println!("{:?}", basket); To mutate through a borrow you need a
&mut reference, and the receiver must itself be mut. The key rule: you may have either many shared & borrows or exactly one &mut, never both at once. This "shared XOR mutable" law is what makes data races impossible at compile time — a guarantee Dart provides only by having a single thread per isolate.Clone vs Copy
void main() {
// Dart never copies objects implicitly; assignment always shares.
final original = [1, 2, 3];
final independent = [...original]; // explicit copy via spread
independent.add(4);
print('$original $independent');
} let number = 5;
let also = number; // i32 is Copy — 'number' is still usable
println!("{number} {also}");
let text = String::from("hi");
let duplicate = text.clone(); // deep copy: opt in explicitly with .clone()
println!("{text} {duplicate}"); // both valid — clone did not move Small, stack-only types (
i32, bool, char, tuples of them) implement Copy, so assignment duplicates them and the original stays valid. Heap-owning types like String and Vec are not Copy — duplicating them is a potentially expensive deep copy you must request with .clone(). Rust makes that cost visible in the source, where Dart's implicit sharing hides it entirely.No null: Option<T>
Nullable → Option<T>
Rust has no
null at all. A value that might be absent has type Option<T> — either Some(value) or None — and the compiler forces you to handle both before you can touch the inner value.String? findName(int id) => id == 1 ? 'Ada' : null;
void main() {
final name = findName(1);
if (name != null) {
print(name.toUpperCase()); // promotion after the null check
}
} fn find_name(id: i32) -> Option<String> {
if id == 1 { Some(String::from("Ada")) } else { None }
}
match find_name(1) {
Some(name) => println!("{}", name.to_uppercase()),
None => println!("absent"),
} Where Dart tracks nullability with
? on the type, Rust makes absence a real value in an enum: Option<String>. You cannot accidentally use a None as a string — there is no value to dereference — so the entire category of null-dereference bugs is gone by construction, a step beyond even Dart's sound null safety. match is one way to unwrap it; the combinators below are terser.if let, unwrap_or, map
int? tryPort(String text) => int.tryParse(text);
void main() {
final port = tryPort('bad') ?? 80; // ?? default
print(port);
final maybe = tryPort('8080');
if (maybe != null) print(maybe * 2);
} fn try_port(text: &str) -> Option<i32> { text.parse().ok() }
let port = try_port("bad").unwrap_or(80); // ?? becomes unwrap_or
println!("{port}");
if let Some(value) = try_port("8080") { // ?. / promotion becomes if let
println!("{}", value * 2);
}
let doubled = try_port("21").map(|n| n * 2); // transform inside the Option
println!("{:?}", doubled); // Some(42) Rust's
Option comes with a rich toolkit that replaces Dart's null-aware operators: unwrap_or(default) is ??, if let Some(x) = … is a one-branch unwrap (like ?. plus a check), and .map(|x| …) transforms the value only when it is present. These combinators keep you from ever writing an explicit null test, and chain cleanly — opt.map(…).unwrap_or(…).No exceptions: Result<T,E>
Exceptions → Result<T,E>
Rust has no exceptions for ordinary failures. A fallible function returns
Result<T, E> — Ok(value) or Err(error) — and, like Option, the compiler makes you deal with the Err case.int parsePort(String text) {
final value = int.tryParse(text);
if (value == null) throw FormatException('bad: $text');
return value;
}
void main() {
try {
print(parsePort('nope'));
} catch (error) {
print('caught: $error');
}
} fn parse_port(text: &str) -> Result<i32, String> {
match text.parse::<i32>() {
Ok(value) => Ok(value),
Err(_) => Err(format!("bad: {text}")),
}
}
match parse_port("nope") {
Ok(port) => println!("{port}"),
Err(message) => println!("caught: {message}"),
} A
Result makes failure part of the return type, so a caller cannot ignore it the way an unchecked Dart exception can slip past. There is no stack unwinding for this — the error is a value you pattern-match, pass along, or transform. It is more explicit than Dart's try/catch, and the ? operator (next) removes almost all of the ceremony.The ? propagation operator
int doublePort(String text) {
final port = int.parse(text); // throws on failure, propagates up
return port * 2;
}
void main() {
print(doublePort('21'));
} fn double_port(text: &str) -> Result<i32, std::num::ParseIntError> {
let port: i32 = text.parse()?; // ? returns early on Err, unwraps on Ok
Ok(port * 2)
}
match double_port("21") {
Ok(value) => println!("{value}"),
Err(error) => println!("error: {error}"),
} The
? operator is Rust's answer to exception propagation: applied to a Result, it unwraps an Ok or returns the Err from the current function immediately. It turns a chain of fallible calls into readable straight-line code — let a = step_one()?; let b = step_two(a)?; — recovering the convenience of Dart's automatic exception propagation while keeping the failure visible in every signature.panic! for the unrecoverable
void main() {
final numbers = [10, 20, 30];
// A bad index throws RangeError; Dart uses exceptions for this.
print(numbers[1]);
} let numbers = vec![10, 20, 30];
println!("{}", numbers[1]); // 20 — in bounds
// An out-of-bounds index would panic! and abort the program:
// println!("{}", numbers[99]); // thread panicked: index out of bounds
// For a recoverable check, use .get, which returns Option:
println!("{:?}", numbers.get(99)); // None Rust does have
panic! — triggered by numbers[99], .unwrap() on a None, or an explicit call — but it is reserved for unrecoverable bugs, not normal error flow, and by default it aborts the thread. The recoverable alternative is a method that returns Option/Result, like .get(index) here. The rule of thumb: return a Result for anything a caller might reasonably handle, and reach for panic! only when continuing would be a mistake.Structs & impl
Classes → structs
class Point {
final int x;
final int y;
Point(this.x, this.y);
}
void main() {
final point = Point(3, 4);
print('${point.x}, ${point.y}');
} struct Point {
x: i32,
y: i32,
}
let point = Point { x: 3, y: 4 }; // field init, no constructor call
println!("{}, {}", point.x, point.y); A Rust
struct holds data and nothing else — there is no constructor, so you build one with field syntax Point { x: 3, y: 4 } (a convention is a Point::new associated function when you want validation). There are no classes and no inheritance; behavior is added separately through impl blocks (next) and shared through traits. Fields are private to the defining module unless marked pub.Methods in impl blocks
class Rectangle {
final int width, height;
Rectangle(this.width, this.height);
int area() => width * height;
static Rectangle square(int side) => Rectangle(side, side);
}
void main() {
print(Rectangle.square(5).area());
} struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn area(&self) -> i32 { // method: takes &self
self.width * self.height
}
fn square(side: i32) -> Rectangle { // no self = associated (static) fn
Rectangle { width: side, height: side }
}
}
println!("{}", Rectangle::square(5).area()); Methods live in an
impl block, separate from the data. A method takes &self (a borrow of the receiver), &mut self to mutate, or self to consume it — the ownership rules again. A function without a self parameter is an associated function, Dart's static method, called with :: — which is how constructors like Rectangle::square and String::from are written.derive: equality, clone, debug
class Point {
final int x, y;
const Point(this.x, this.y);
@override
bool operator ==(Object other) =>
other is Point && other.x == x && other.y == y;
@override
int get hashCode => Object.hash(x, y);
@override
String toString() => 'Point($x, $y)';
}
void main() {
print(Point(1, 2) == Point(1, 2)); // true
} #[derive(Debug, Clone, PartialEq)] // generate the implementations
struct Point {
x: i32,
y: i32,
}
let a = Point { x: 1, y: 2 };
let b = a.clone();
println!("{}", a == b); // true — PartialEq
println!("{:?}", b); // Point { x: 1, y: 2 } — Debug The
#[derive(...)] attribute auto-generates trait implementations, sparing you the Dart boilerplate on the left: PartialEq gives structural ==, Clone a deep copy, Debug the {:?} formatting, and Hash/Eq make a type usable as a map key. It is Rust's counterpart to Kotlin's data class — one line replacing dozens — but à la carte, deriving exactly the capabilities you ask for.Enums & match
Sealed class → enum with data
sealed class Shape {}
class Circle extends Shape { final double r; Circle(this.r); }
class Rect extends Shape { final double w, h; Rect(this.w, this.h); }
double area(Shape shape) => switch (shape) {
Circle(:final r) => 3.14159 * r * r,
Rect(:final w, :final h) => w * h,
};
void main() => print(area(Circle(2))); enum Shape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { r } => 3.14159 * r * r,
Shape::Rect { w, h } => w * h,
}
}
println!("{}", area(&Shape::Circle { r: 2.0 })); A Rust
enum is an algebraic data type: each variant can carry its own fields, so a Dart sealed hierarchy collapses into one enum Shape — the same shape as Swift and Kotlin's sealed answers, and the tool Rust reaches for constantly (Option and Result are just enums). It is closed and exhaustively matchable, which the next concept leans on.match is exhaustive
String describe(int n) => switch (n) {
0 => 'zero',
1 || 2 || 3 => 'small',
final v when v < 0 => 'negative',
_ => 'large',
};
void main() {
print(describe(2));
print(describe(-5));
} fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1..=3 => "small", // inclusive range pattern
v if v < 0 => "negative", // guard
_ => "large", // required: match must be exhaustive
}
}
println!("{}", describe(2));
println!("{}", describe(-5)); Rust's
match is the ancestor of Dart 3's pattern switch, and they are close cousins: range patterns (1..=3), guards (v if v < 0), and value binding all line up. The defining rule is exhaustiveness — the compiler rejects a match that misses a case, so you either handle every variant or add a _ catch-all. On an enum that means adding a variant surfaces every match that must be updated.Traits
Interfaces → traits
abstract class Speaker {
String speak();
}
class Dog implements Speaker {
@override
String speak() => 'Woof';
}
void main() => print(Dog().speak()); trait Speaker {
fn speak(&self) -> String;
}
struct Dog;
impl Speaker for Dog { // implement the trait for the type
fn speak(&self) -> String {
String::from("Woof")
}
}
println!("{}", Dog.speak()); A
trait is Rust's interface: a set of method signatures a type can implement with impl Trait for Type. Unlike Dart, conformance is explicit and lives outside the type — you can implement your own trait for a built-in type like i32 — and traits, not base classes, are the whole abstraction story, since there is no inheritance. Standard traits like Display, Iterator, and From wire your types into the language.Default methods (mixins)
mixin Greetable {
String get who;
String greet() => 'Hello, $who';
}
class Person with Greetable {
@override
final String who;
Person(this.who);
}
void main() => print(Person('Ada').greet()); trait Greetable {
fn who(&self) -> String;
fn greet(&self) -> String { // DEFAULT method body
format!("Hello, {}", self.who())
}
}
struct Person { name: String }
impl Greetable for Person {
fn who(&self) -> String { self.name.clone() } // only the required part
}
println!("{}", Person { name: String::from("Ada") }.greet()); A trait method can ship a default body, so a conformer only implements the required pieces and inherits the rest — exactly the role Dart mixins play, and the mechanism behind
Iterator giving you map, filter, and dozens more from a single required next. Combined with impl Trait for Type, default methods cover both the mixin and the "extension methods" use cases in one feature.Dynamic dispatch: dyn
abstract class Speaker {
String speak();
}
class Dog implements Speaker { @override String speak() => 'Woof'; }
class Cat implements Speaker { @override String speak() => 'Meow'; }
void main() {
final List<Speaker> animals = [Dog(), Cat()];
for (final a in animals) print(a.speak());
} trait Speaker { fn speak(&self) -> String; }
struct Dog;
struct Cat;
impl Speaker for Dog { fn speak(&self) -> String { "Woof".to_string() } }
impl Speaker for Cat { fn speak(&self) -> String { "Meow".to_string() } }
let animals: Vec<Box<dyn Speaker>> = vec![Box::new(Dog), Box::new(Cat)];
for a in &animals {
println!("{}", a.speak());
} A heterogeneous collection of "things that speak" needs dynamic dispatch, spelled
Box<dyn Speaker> — a trait object behind a pointer, resolved at run time like a Dart interface reference. The dyn keyword makes the cost explicit, because Rust's default is the opposite: generic <T: Speaker> bounds (next section) are monomorphized into specialized code with no run-time lookup. You choose per use whether you want the flexibility or the speed.Vec, HashMap & Strings
List → Vec
void main() {
final numbers = <int>[1, 2, 3];
numbers.add(4);
print(numbers.length);
print(numbers.first);
} let mut numbers = vec![1, 2, 3]; // vec! macro; must be mut to grow
numbers.push(4);
println!("{}", numbers.len());
println!("{:?}", numbers.first()); // Option: Some(1)
println!("{:?}", numbers); // {:?} debug-prints the whole Vec A Dart
List is a Rust Vec<T>, built with the vec! macro and homogeneous (every element is the same type T). It must be mut to push, and accessors that might miss — first, get(i) — return an Option rather than risking a null. Printing a whole collection uses the debug formatter {:?}, since Vec has no plain Display.Map → HashMap
void main() {
final ages = {'Ada': 36, 'Grace': 45};
print(ages['Ada']); // 36
print(ages['Nobody']); // null
ages['Ada'] = (ages['Ada'] ?? 0) + 1;
print(ages['Ada']);
} use std::collections::HashMap;
let mut ages: HashMap<&str, i32> = HashMap::new();
ages.insert("Ada", 36);
ages.insert("Grace", 45);
println!("{:?}", ages.get("Ada")); // Some(36)
println!("{:?}", ages.get("Nobody")); // None
*ages.entry("Ada").or_insert(0) += 1; // the entry API
println!("{}", ages["Ada"]); A
HashMap must be brought in with use std::collections::HashMap — it is not in the prelude — and lookups return an Option (get) rather than null. The entry(...).or_insert(...) API elegantly handles "insert if absent, otherwise update," which in Dart takes the ?? 0 dance on the left; it is the idiomatic way to count or accumulate into a map.String vs &str
void main() {
final greeting = 'hello'; // one String type in Dart
final shout = greeting.toUpperCase();
print('$greeting $shout');
} let owned: String = String::from("hello"); // heap-owned, growable
let borrowed: &str = &owned; // a borrowed view (slice)
let literal: &str = "world"; // string literals are &str
let shout = owned.to_uppercase(); // returns a new String
println!("{} {} {}", borrowed, literal, shout); Rust splits Dart's single
String into two types that trip up every newcomer: String is an owned, growable, heap-allocated string, while &str is a borrowed, immutable view into one (string literals are &str). The rule of thumb mirrors ownership: take &str as a function parameter to accept both cheaply, return or store an owned String. It is the string-sized version of the borrow-vs-own decision.Iterators
Lazy iterators: map/filter/collect
void main() {
final numbers = [1, 2, 3, 4, 5, 6];
final result = numbers
.where((n) => n.isEven)
.map((n) => n * n)
.toList();
print(result); // [4, 16, 36]
} let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers.iter()
.filter(|&&n| n % 2 == 0) // where -> filter
.map(|&n| n * n)
.collect(); // nothing runs until collect() consumes it
println!("{:?}", result); // [4, 16, 36] Rust iterators are lazy like Dart's
Iterable: filter and map build a pipeline that does no work until a consumer like collect, sum, or a for loop drives it. Two Rust specifics: you start the chain with .iter() (which yields references, hence the & patterns), and collect() often needs a target type annotation (Vec<i32>) to know what to build. The whole chain compiles down to a tight loop with no allocation per stage — "zero-cost."fold, sum & reduce
void main() {
final numbers = [1, 2, 3, 4];
final total = numbers.fold<int>(0, (sum, n) => sum + n);
final maxValue = numbers.reduce((a, b) => a > b ? a : b);
print('$total $maxValue');
} let numbers = vec![1, 2, 3, 4];
let total: i32 = numbers.iter().sum(); // dedicated sum
let folded = numbers.iter().fold(0, |acc, n| acc + n); // fold with a seed
let max_value = numbers.iter().max().unwrap(); // max returns Option
println!("{total} {folded} {max_value}"); Dart's
fold is Rust's fold(seed, |acc, x| …), and the same reductions have dedicated, clearer methods: sum(), product(), max()/min() (which return an Option, since the iterator might be empty). Rust's reduce exists too and, like Dart's seedless reduce, returns an Option rather than risking an empty-collection error — the null-safety discipline reaching into the collection API.Functions & Closures
No named/default arguments
String connect(String host, {int port = 5432, bool tls = true}) =>
'$host:$port tls=$tls';
void main() {
print(connect('db.example'));
print(connect('db.example', port: 6432, tls: false));
} struct ConnectOptions {
port: u16,
tls: bool,
}
fn connect(host: &str, opts: ConnectOptions) -> String {
format!("{}:{} tls={}", host, opts.port, opts.tls)
}
println!("{}", connect("db.example", ConnectOptions { port: 5432, tls: true }));
println!("{}", connect("db.example", ConnectOptions { port: 6432, tls: false })); Like Go, Rust has no named arguments, default parameter values, or overloading — a real adjustment from Dart's expressive parameter lists. The idiomatic replacements are an options struct (as here, often paired with
#[derive(Default)] and ..Default::default() to fill the rest) or the builder pattern for larger APIs. It keeps call sites explicit at the cost of the brevity Dart offers.Closures & capture
void main() {
final square = (int n) => n * n;
print(square(5));
final factor = 3;
final scale = (int n) => n * factor; // captures factor
print(scale(10));
} let square = |n: i32| n * n; // closure syntax: |params| body
println!("{}", square(5));
let factor = 3;
let scale = move |n: i32| n * factor; // 'move' takes ownership of factor
println!("{}", scale(10)); Closures are written with pipes,
|n| n * n, and capture their environment like Dart's. The Rust-specific wrinkle is how they capture: by reference by default, or by value when you write move (required when the closure outlives the captured variable, e.g. handed to a thread). Under the hood closures implement the Fn/FnMut/FnOnce traits depending on how they use their captures — detail Dart's uniform closures never expose.Generics & Bounds
Generics with trait bounds
T largest<T extends Comparable>(List<T> items) {
var winner = items.first;
for (final item in items) {
if (item.compareTo(winner) > 0) winner = item;
}
return winner;
}
void main() {
print(largest<int>([3, 9, 2]));
} fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut winner = items[0];
for &item in items {
if item > winner { winner = item; }
}
winner
}
println!("{}", largest(&[3, 9, 2]));
println!("{}", largest(&[1.5, 0.5, 2.5])); Generic syntax rhymes with Dart, but the bound is a trait:
<T: PartialOrd + Copy> means "any type that can be ordered and is cheap to copy," and you can require several traits with +. Rust monomorphizes generics — it stamps out a specialized copy for each concrete type at compile time — so there is no boxing and no run-time cost, unlike Dart's single reified implementation. The bound is also a contract the compiler checks at the definition, not just at each call.Fearless Concurrency
Isolates → threads
Future<int> compute() async {
return List.generate(100, (i) => i + 1).reduce((a, b) => a + b);
}
void main() async {
final result = await compute();
print(result); // 5050
} use std::thread;
let handle = thread::spawn(|| {
(1..=100).sum::<i32>() // runs on a real OS thread
});
let result = handle.join().unwrap(); // wait for it, get the value
println!("{}", result); // 5050 Rust has real OS threads with shared memory, spawned by
thread::spawn and awaited with join — closer to Dart's isolates in spirit than to its single-threaded Futures, but sharing one heap rather than copying messages. What makes it "fearless" is that the ownership and Send/Sync rules are enforced across threads too, so the compiler rejects code that could race. (Rust also has async/await, but it needs an external runtime like tokio, so it is beyond this std-only playground.)Shared state: Arc + Mutex
Dart isolates never share memory, so there is nothing to lock. Rust threads share one heap, and the type system makes you say so: shared ownership is
Arc, and safe mutation is Mutex.import 'dart:isolate';
Future<int> heavySum(int n) {
return Isolate.run(() { // separate heap, nothing shared
var total = 0;
for (var i = 1; i <= n; i++) total += i;
return total;
});
}
void main() async {
print(await heavySum(1000)); // 500500
} use std::sync::{Arc, Mutex};
use std::thread;
let total = Arc::new(Mutex::new(0)); // Arc = shared ownership, Mutex = safe mutation
let mut handles = vec![];
for chunk in 0..4 {
let total = Arc::clone(&total); // cheap: bumps the reference count
handles.push(thread::spawn(move || {
let start = chunk * 250 + 1;
let sum: i32 = (start..start + 250).sum();
*total.lock().unwrap() += sum; // lock, then mutate
}));
}
for handle in handles { handle.join().unwrap(); }
println!("{}", *total.lock().unwrap()); // 500500 To share mutable state across threads you wrap it in
Arc<Mutex<T>>: Arc (atomic reference count) gives several threads shared ownership, and Mutex guards the data so only one thread mutates at a time — lock() returns a guard that unlocks when it drops. The compiler will not let you share a plain value or forget the lock, so the data race that a Dart programmer never had to think about (isolates share nothing) is caught at compile time rather than left to discipline.Modules & Cargo
Libraries → modules
// geometry.dart
// double areaOfCircle(double r) => 3.14159 * r * r; // public
// double _helper(double r) => r * r; // private
//
// main.dart
import 'geometry.dart';
void main() {
print(areaOfCircle(2));
} mod geometry {
pub fn area_of_circle(r: f64) -> f64 { // pub = exported
3.14159 * r * r
}
fn helper(r: f64) -> f64 { r * r } // private to the module
}
use geometry::area_of_circle; // bring the name into scope
println!("{}", area_of_circle(2.0)); Rust organizes code into modules declared with
mod (which can nest and can live in separate files), and everything is private by default — you export with the pub keyword, the inverse of Dart's "public unless underscore-prefixed." You pull names into scope with use (Dart's import), and paths use ::. A whole compiled library is a crate, the unit Cargo manages next. The Dart side references a second file, so its column is display-only.pub → Cargo
// pubspec.yaml
// dependencies:
// http: ^1.2.0
//
// Then: dart pub get
// Then: import 'package:http/http.dart' as http;
void main() {
print('Dependencies are declared in pubspec.yaml and fetched by pub.');
} // Cargo.toml
// [dependencies]
// serde = "1.0"
//
// Then: cargo build (or cargo run / cargo test)
// Then in code: use serde::Serialize;
println!("Dependencies live in Cargo.toml and are fetched by Cargo."); Dart's
pubspec.yaml plus dart pub get corresponds to Cargo.toml plus Cargo, Rust's build tool and package manager (cargo build, cargo run, cargo test). Packages are crates from crates.io, and the version syntax rhymes: Cargo's "1.0" is a caret requirement by default, the same compatible-update rule as Dart's ^1.2.0.