Fundamentals
Variables, built-in types, strings, and functions — the syntax you’ll learn in a day.
Variables
val x = 5 // immutable
var y = 10 // mutable
y = 20
val name: String = "Ada"let x = 5; // immutable by default
let mut y = 10; // opt in with `mut`
y = 20;
let name: String = "Ada".to_string();Two things surprise Kotlin devs:
- Shadowing is idiomatic. You can re-declare the same name with a new
let, even changing its type. This is not mutation — it’s a new binding.
let spaces = " "; // &str
let spaces = spaces.len(); // now usize — totally fine, not `mut`constis compile-time only and needs a type. There’s alsostaticfor globals. Neither is your everyday tool —letis.
const MAX_POINTS: u32 = 100_000;Built-in types
Rust is explicit about integer width and signedness. There is no single Int.
| Kotlin | Rust |
|---|---|
Int | i32 (default integer) |
Long | i64 |
Short / Byte | i16 / i8 |
UByte / UShort / UInt / ULong (unsigned, stable since 1.5) | u8 / u16 / u32 / u64 — plus usize (index/length type, no Kotlin equivalent) |
Float / Double | f32 / f64 (default float) |
Boolean | bool |
Char | char (a full Unicode scalar, 4 bytes — not a UTF-16 unit) |
String | String (owned, growable) and &str (borrowed string slice) |
List<T> | Vec<T> (growable) and [T; N] (fixed array), &[T] (slice) |
Map<K,V> | HashMap<K, V> (from std::collections) |
Pair/Triple | tuples: (i32, String), (a, b, c) |
Unit | () (the unit type) |
Nothing | ! (the never type) |
let sum: i64 = 1_000_000 * 2;
let tuple: (i32, f64, char) = (500, 6.4, 'z');
let (a, b, c) = tuple; // destructuring, like Kotlin
let first = tuple.0; // tuple index accessInteger overflow panics in debug builds and wraps in release — use wrapping_add, checked_add, saturating_add when you mean it.
Strings — the one that trips everyone up
Kotlin has one String. Rust has two you’ll use constantly:
String— owned, heap-allocated, growable. LikeStringBuilder-meets-String.&str— a borrowed view into string data (a “string slice”). String literals are&str.
val s = "hello"
val owned = buildString { ... }let s: &str = "hello"; // literal is &str
let owned: String = "hello".to_string();
let owned = String::from("hello");You take &str as a function parameter (accepts both), return String when you own the data.
String interpolation looks familiar but only takes simple names by default:
let name = "Ada";
let age = 36;
println!("{name} is {age}"); // inline capture (Rust 1.58+, any edition)
println!("{} is {}", name, age); // positional
let msg = format!("{name} is {age}"); // returns a StringIndexing by integer (s[0]) is not allowed — bytes vs chars vs graphemes are genuinely ambiguous in UTF-8. Iterate instead:
for c in "héllo".chars() { /* ... */ }
let bytes = "hi".as_bytes();Functions
fun add(a: Int, b: Int): Int {
return a + b
}fn add(a: i32, b: i32) -> i32 {
a + b // no `;`/`return`: last expr is returned
}The single biggest syntax idea in Rust: blocks are expressions. The final line without a semicolon is the block’s value.
let y = {
let x = 3;
x + 1 // no semicolon → this block evaluates to 4
};return exists but is mostly for early exit. Everything else “falls out” the bottom.
Closures (lambdas):
// Kotlin: { a, b -> a + b }
let add = |a, b| a + b;
let nums: Vec<i32> = (1..=5).map(|x| x * 2).collect();