What Rust doesn't have
Syntax · Lesson 4
Some things you reach for by habit do not exist in Rust: x++, a ? b : c, null, C-style for, default arguments, classes, exceptions, goto. Each one fails with a compiler message, and each has a Rust way. This page lists them, with the real error and what to write instead.
Coming from Goi++ is a statement in Go, and Rust has no ++ at all. Much else matches: Go has no ternary operator, no function overloading by type, no default arguments and no mixing of typed number types either. What you give up is nil, goto, defer, for cond {} and the if err != nil boilerplate: Rust has Option, labeled break, Drop, while and ?. Watch the break inside a switch: in Rust it leaves the loop.
Coming from KotlinYou are used to a lot of this already: if is an expression, there is no ternary operator, and T? is a checked null. What Rust takes away is ++, ?., ?: and !! (Option has map, unwrap_or and unwrap), default and named arguments, open classes with inheritance, and exceptions (Result and ? replace try/catch).
Coming from PythonPython has no x++ either, so x += 1 is already your habit. What is new is that an if needs a bool (an empty list or a 0 is not false), a call has no keyword arguments and a definition has no default values, // starts a comment, and/or/not/elif are &&/||/!/else if, a name has one type, and braces replace the indentation.
Coming from TypeScriptx++, a ? b : c, null and undefined, ??, truthiness (if (name)), throw and optional parameters all have a different Rust form: x += 1, if a { b } else { c }, Option, unwrap_or, an explicit test, Result and ?, and a struct. Careful: u?.name is valid Rust, and it is not a safe call but an early return. Rust also does not turn a number into another number type for you.
Coming from Javai++, ?:, null, new, this, extends, function overloading, switch that falls through, finally and checked or unchecked exceptions all have a different Rust form. Java widens int to long on its own; Rust makes you write the conversion. A break in a switch inside a loop leaves the switch in Java; in Rust it leaves the loop.
Coming from C#i++, ?:, ?., new, this, : BaseClass, function overloads, optional and named arguments, and try/catch have a different Rust form, and null becomes Option. C# widens int to long on its own; Rust makes you write the conversion. A break in a switch inside a loop leaves the switch in C#; in Rust it leaves the loop.
Coming from Ci++, ?:, for (i = 0; i < n; i++), NULL, a switch that falls through, goto, (double)n, -> and #define do not exist or fail in Rust, as does if (n) on a number. A global you assign without unsafe fails to compile too, and C's silent number conversions (int n = 3.9;) need an explicit as. A break; at the end of a ported case compiles and leaves the loop.
Coming from C++Everything in the C list, plus classes with inheritance, function overloads, default arguments, constructors and exceptions: Rust has none of them. A struct holds data, an impl block holds methods, a trait shares behaviour (and impl Add is your operator+), a Drop runs where a destructor would, and a Result carries a failure. auto is let.
The quick comparison
// What other languages write with ?:, i++, null and switch, written in Rust.fn size(n: i32) -> &'static str { match n { 1 | 2 => "small", 3 => "medium", _ => "large", }}
fn first_even(items: &[i32]) -> Option<i32> { for &x in items { if x % 2 == 0 { return Some(x); } } None}
fn main() { let mut total = 0; for i in 0..4 { total += i; } let label = if total > 5 { "big" } else { "small" }; println!("total: {total} ({label})"); println!("size: {} {}", size(2), size(9)); match first_even(&[3, 5, 8]) { Some(n) => println!("first even: {n}"), None => println!("no even number"), } println!("none: {}", first_even(&[1, 3]).unwrap_or(-1)); let wide = total as i64 * 1_000_000_000; println!("wide: {wide}");}total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
package main
import "fmt"
// What other languages write with ?:, i++, null and switch, written in Go.func size(n int) string { switch n { case 1, 2: return "small" case 3: return "medium" default: return "large" }}
func firstEven(items []int) (int, bool) { for _, x := range items { if x%2 == 0 { return x, true } } return 0, false}
func main() { total := 0 for i := 0; i < 4; i++ { total += i } label := "small" if total > 5 { label = "big" } fmt.Printf("total: %d (%s)\n", total, label) fmt.Printf("size: %s %s\n", size(2), size(9)) if n, ok := firstEven([]int{3, 5, 8}); ok { fmt.Printf("first even: %d\n", n) } else { fmt.Println("no even number") } n, ok := firstEven([]int{1, 3}) if !ok { n = -1 } fmt.Printf("none: %d\n", n) wide := int64(total) * 1_000_000_000 fmt.Printf("wide: %d\n", wide)}total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
// What other languages write with ?:, i++, null and switch, written in Kotlin.fun size(n: Int): String = when (n) { 1, 2 -> "small" 3 -> "medium" else -> "large"}
fun firstEven(items: List<Int>): Int? { for (x in items) { if (x % 2 == 0) return x } return null}
fun main() { var total = 0 for (i in 0 until 4) { total += i } val label = if (total > 5) "big" else "small" println("total: $total ($label)") println("size: ${size(2)} ${size(9)}") val even = firstEven(listOf(3, 5, 8)) if (even != null) println("first even: $even") else println("no even number") println("none: ${firstEven(listOf(1, 3)) ?: -1}") val wide = total.toLong() * 1_000_000_000 println("wide: $wide")}total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
# What other languages write with ?:, i++, null and switch, written in Python.from typing import Optional
def size(n: int) -> str: if n in (1, 2): return "small" if n == 3: return "medium" return "large"
def first_even(items: list[int]) -> Optional[int]: for x in items: if x % 2 == 0: return x return None
total = 0for i in range(4): total += ilabel = "big" if total > 5 else "small"print(f"total: {total} ({label})")print(f"size: {size(2)} {size(9)}")even = first_even([3, 5, 8])if even is not None: print(f"first even: {even}")else: print("no even number")none = first_even([1, 3])print(f"none: {-1 if none is None else none}")wide = total * 1_000_000_000print(f"wide: {wide}")total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
// What other languages write with ?:, i++, null and switch, written in TypeScript.function size(n: number): string { switch (n) { case 1: case 2: return "small"; case 3: return "medium"; default: return "large"; }}
function firstEven(items: number[]): number | undefined { for (const x of items) { if (x % 2 === 0) return x; } return undefined;}
let total = 0;for (let i = 0; i < 4; i++) { total += i;}const label = total > 5 ? "big" : "small";console.log(`total: ${total} (${label})`);console.log(`size: ${size(2)} ${size(9)}`);const even = firstEven([3, 5, 8]);if (even !== undefined) { console.log(`first even: ${even}`);} else { console.log("no even number");}console.log(`none: ${firstEven([1, 3]) ?? -1}`);const wide = total * 1_000_000_000;console.log(`wide: ${wide}`);total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
import java.util.List;import java.util.Optional;
public class Main { // What other languages write with ?:, i++, null and switch, written in Java. static String size(int n) { return switch (n) { case 1, 2 -> "small"; case 3 -> "medium"; default -> "large"; }; }
static Optional<Integer> firstEven(List<Integer> items) { for (int x : items) { if (x % 2 == 0) { return Optional.of(x); } } return Optional.empty(); }
public static void main(String[] args) { int total = 0; for (int i = 0; i < 4; i++) { total += i; } String label = total > 5 ? "big" : "small"; System.out.println("total: " + total + " (" + label + ")"); System.out.println("size: " + size(2) + " " + size(9)); Optional<Integer> even = firstEven(List.of(3, 5, 8)); if (even.isPresent()) { System.out.println("first even: " + even.get()); } else { System.out.println("no even number"); } System.out.println("none: " + firstEven(List.of(1, 3)).orElse(-1)); long wide = (long) total * 1_000_000_000; System.out.println("wide: " + wide); }}total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
// What other languages write with ?:, i++, null and switch, written in C#.static string Size(int n) => n switch{ 1 or 2 => "small", 3 => "medium", _ => "large",};
static int? FirstEven(int[] items){ foreach (var x in items) { if (x % 2 == 0) return x; } return null;}
var total = 0;for (var i = 0; i < 4; i++){ total += i;}var label = total > 5 ? "big" : "small";Console.WriteLine($"total: {total} ({label})");Console.WriteLine($"size: {Size(2)} {Size(9)}");var even = FirstEven(new[] { 3, 5, 8 });if (even is not null){ Console.WriteLine($"first even: {even}");}else{ Console.WriteLine("no even number");}Console.WriteLine($"none: {FirstEven(new[] { 1, 3 }) ?? -1}");long wide = (long)total * 1_000_000_000;Console.WriteLine($"wide: {wide}");total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
#include <stdio.h>
// What other languages write with ?:, i++, null and switch, written in C.const char *size(int n) { switch (n) { case 1: case 2: return "small"; case 3: return "medium"; default: return "large"; }}
// Returns a pointer to the first even number, or NULL when there is none.const int *first_even(const int *items, int len) { for (int i = 0; i < len; i++) { if (items[i] % 2 == 0) { return &items[i]; } } return NULL;}
int main(void) { int total = 0; for (int i = 0; i < 4; i++) { total += i; } const char *label = total > 5 ? "big" : "small"; printf("total: %d (%s)\n", total, label); printf("size: %s %s\n", size(2), size(9)); int a[] = {3, 5, 8}; const int *even = first_even(a, 3); if (even != NULL) { printf("first even: %d\n", *even); } else { printf("no even number\n"); } int b[] = {1, 3}; const int *none = first_even(b, 2); printf("none: %d\n", none != NULL ? *none : -1); long long wide = (long long)total * 1000000000LL; printf("wide: %lld\n", wide); return 0;}total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
#include <iostream>#include <optional>#include <vector>
// What other languages write with ?:, i++, null and switch, written in C++.const char *size(int n) { switch (n) { case 1: case 2: return "small"; case 3: return "medium"; default: return "large"; }}
std::optional<int> first_even(const std::vector<int> &items) { for (int x : items) { if (x % 2 == 0) { return x; } } return std::nullopt;}
int main() { int total = 0; for (int i = 0; i < 4; i++) { total += i; } const char *label = total > 5 ? "big" : "small"; std::cout << "total: " << total << " (" << label << ")\n"; std::cout << "size: " << size(2) << " " << size(9) << "\n"; auto even = first_even({3, 5, 8}); if (even) { std::cout << "first even: " << *even << "\n"; } else { std::cout << "no even number\n"; } std::cout << "none: " << first_even({1, 3}).value_or(-1) << "\n"; long long wide = static_cast<long long>(total) * 1000000000LL; std::cout << "wide: " << wide << "\n";}total: 6 (big) size: small large first even: 8 none: -1 wide: 6000000000
What's different
- Rust has no
x++and noa ? b : c. Writex += 1, andif a { b } else { c }, which is an expression. - There is no
null. A value that may be missing has the typeOption<T>, and the compiler makes you handleNone. - Numbers do not convert on their own:
i32plusi64is an error. Writeas,Fromortry_from. - No
switchfallthrough, no exceptions for ordinary errors, no default or named arguments, no function overloading by parameter type, no classes and nogoto. Each has a Rust form, shown on this page. Abreakin amatchinside a loop leaves the loop.
Writing x++
Looks fine: in Go, Kotlin, TypeScript, Java, C#, C and C++ it runs fine. Rust won't compile it.
Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Python refuses this too. Same idea, different words.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.fn main() { let mut count = 0; for _ in 0..3 { count++; } println!("count: {count}");}package main
import "fmt"
func main() { count := 0 for i := 0; i < 3; i++ { count++ } fmt.Printf("count: %d\n", count)}count: 3
fun main() { var count = 0 for (i in 0 until 3) { count++ } println("count: $count")}count: 3
count = 0for _ in range(3): count++print(f"count: {count}") File "main.py", line 3
count++
^
SyntaxError: invalid syntaxlet count = 0;for (let i = 0; i < 3; i++) { count++;}console.log(`count: ${count}`);count: 3
public class Main { public static void main(String[] args) { int count = 0; for (int i = 0; i < 3; i++) { count++; } System.out.println("count: " + count); }}count: 3
var count = 0;for (var i = 0; i < 3; i++){ count++;}Console.WriteLine($"count: {count}");count: 3
#include <stdio.h>
int main(void) { int count = 0; for (int i = 0; i < 3; i++) { count++; } printf("count: %d\n", count); return 0;}count: 3
#include <iostream>
int main() { int count = 0; for (int i = 0; i < 3; i++) { count++; } std::cout << "count: " << count << "\n";}count: 3
What does the compiler say? Guess first, then open.
error: Rust has no postfix increment operator --> src/main.rs:4:14 | 4 | count++; | ^^ not a valid postfix operator | help: use `+= 1` instead | 4 - count++; 4 + count += 1;
Go prints 3. count++ is a statement, not an expression, so x := count++ is an error in Go. ++count does not exist.
Kotlin prints 3. count++ and ++count both work, and can sit inside a larger expression.
Python refuses with a SyntaxError: it has no ++ operator, so write count += 1. It is the one language here with no ++. Note that ++count and --count are legal in Python and do nothing: they read as +(+count) and -(-count).
TypeScript prints 3. count++ and ++count work, as in JavaScript, and --count decrements. In Rust --count compiles and negates twice.
Java prints 3. count++ and ++count work, also inside expressions such as a[i++].
C# prints 3. count++ and ++count work, also inside expressions.
C prints 3. count++ works, also inside expressions such as a[i++].
C++ prints 3. count++ and ++count work, and they can be overloaded for your own types.
Rust has no ++ or -- operator. count++ is not a statement the parser knows, and the compiler stops with a message that says so and shows count += 1.
The compiler recognises the habit and names it: "Rust has no postfix increment operator". The fix is count += 1. Python is the other language here with no ++ operator. Elsewhere ++ is part of the language: Go allows it only as a statement, and the others also allow it inside larger expressions.
The fix
Compiles and runs.
fn main() { let mut count = 0; for _ in 0..3 { count += 1; } println!("count: {count}");}count: 3
The toolkit
This page lists habits that fail in Rust, with the real compiler message and what to write instead. A message with no error code means the parser did not know the syntax; a code such as E0308 means the syntax was fine and the types were not. Outputs come from Linux on 64-bit ARM with Rust 1.99.1.
Two warnings. When the parser gets lost it names the token where it gave up, and the cause is that token or the one before. And a help: line is a guess: count += 1 is the fix, but goto done gets “write goto.done” and while item = items.next() gets “compare with ==”. Read a guess before you apply it.
Three words recur: Option is a value that may be missing (Option), Result is a success or an error (Errors), and match is Rust’s switch (Match).
Your habits, in Rust
Your own list comes first; the shared list is for everyone.
The shared list
| You try | Rust way | Error | Learn more |
|---|---|---|---|
x++, x-- |
x += 1; |
syntax error | Operators and loops |
c ? a : b |
if c { a } else { b } |
syntax error | Statements and expressions |
null, nil, undefined |
Option<T> |
E0425, E0308 | Option |
i32 with i64, n * 1.5 |
as, From, try_from |
E0308, E0277 | Numbers and types |
if x on text, a list, a number |
an explicit test | E0308 | Statements and expressions |
break in a switch arm |
in a match inside a loop it ends the loop |
compiles | Match |
throw, catch |
Result, ? |
syntax error | Errors |
f(width = 3), default values |
a struct, an Option parameter |
E0425, E0061 | Methods |
| two functions, one name | two names, or a trait | E0428 | Methods |
class, extends, new, this |
struct, a trait, self |
syntax error, E0423, E0424, E0425 | Methods |
s[0] |
s.chars(), s.as_bytes() |
E0277 | Strings |
| a global assigned anywhere | an atomic or a Mutex |
E0594, E0133 | Shared state |
Operators and loops
Four habits, one function each so that one run shows every message:
fn increment() { let mut count = 0; count++;}
fn prefix() { let mut count = 0; ++count;}
fn decrement() { let mut count = 10; count--;}
fn ternary(n: i32) -> &'static str { n % 2 == 0 ? "even" : "odd"}
fn main() {}error: Rust has no postfix increment operator --> src/main.rs:3:10 | 3 | count++; | ^^ not a valid postfix operator | help: use `+= 1` instead | 3 - count++; 3 + count += 1; error: Rust has no prefix increment operator --> src/main.rs:8:5 | 8 | ++count; | ^^ not a valid prefix operator | help: use `+= 1` instead | 8 - ++count; 8 + count += 1; error: Rust has no postfix decrement operator --> src/main.rs:13:10 | 13 | count--; | ^^ not a valid postfix operator | help: use `-= 1` instead | 13 - count--; 13 + count -= 1; error: Rust has no ternary operator --> src/main.rs:17:5 | 17 | n % 2 == 0 ? "even" : "odd" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | help: use an `if-else` expression instead | 17 - n % 2 == 0 ? "even" : "odd" 17 + if n % 2 == 0 { "even" } else { "odd" }
Each message says what to write. a[i++] has no one-token form: write the increment as its own statement, or as a block that ends in the value, { let v = a[i]; i += 1; v }.
--x is not a decrement
++x is a syntax error, like x++. --x is valid, and its meaning depends on the language. In Rust it is -(-x), as in Python, so count is unchanged. In C, C++, Java, C#, Kotlin and TypeScript it decrements, and Go rejects it. A lint (a check for suspicious code) named double_negations, on by default, warns about it:
#![allow(double_negations)]
fn main() { let count = 5; let after = --count; println!("count: {count}"); println!("--count: {after}");}count: 5 --count: 5
On an unsigned number it is an error instead (E0600), because an unsigned value cannot be negated:
fn main() { let count = 5u32; let after = --count; println!("{after}");}error[E0600]: cannot apply unary operator `-` to type `u32` --> src/main.rs:3:18 | 3 | let after = --count; | ^^^^^^ cannot apply unary operator `-` | = note: unsigned values cannot be negated error[E0600]: cannot apply unary operator `-` to type `u32` --> src/main.rs:3:17 | 3 | let after = --count; | ^^^^^^^ cannot apply unary operator `-` | = note: unsigned values cannot be negated
No null
A value that may be missing has the type Option<T> (Option). String has no missing state, and None belongs to Option, so it does not fit where a String is declared:
fn main() { let a = null; let b: String = None; let c: Option<i32> = 5; println!("{a} {b} {c:?}");}error[E0425]: cannot find value `null` in this scope --> src/main.rs:2:13 | 2 | let a = null; | ^^^^ not found in this scope | help: consider importing this function | 1 + use std::ptr::null; error[E0308]: mismatched types --> src/main.rs:3:21 | 3 | let b: String = None; | ------ ^^^^ expected `String`, found `Option<_>` | | | expected due to this | = note: expected struct `String` found enum `Option<_>` error[E0308]: mismatched types --> src/main.rs:4:26 | 4 | let c: Option<i32> = 5; | ----------- ^ expected `Option<i32>`, found integer | | | expected due to this | = note: expected enum `Option<i32>` found type `{integer}` help: try wrapping the expression in `Some` | 4 | let c: Option<i32> = Some(5); | +++++ + Some errors have detailed explanations: E0308, E0425.
The first message carries a trap. std::ptr::null makes a raw pointer, which the Reference describes as “without safety or liveness guarantees”. Following the hint does not give you an Option; it gives a different error:
use std::ptr::null;
fn main() { let a = null; println!("{a}");}error[E0277]: `fn() -> *const _ {null::<_>}` doesn't implement `std::fmt::Display` --> src/main.rs:5:15 | 5 | println!("{a}"); | ^^^ `fn() -> *const _ {null::<_>}` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for fn item `fn() -> *const _ {null::<_>}` = note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
Raw pointers exist for talking to other languages, and dereferencing one is an unsafe operation. In ordinary code, do not follow that hint.
Numbers do not convert themselves
Rust does not turn one number type into another for you, not even a safe widening such as i32 to i64, or an integer into a float. A comparison (small == other) is E0308 too:
fn main() { let small: i32 = 5; let big: u64 = small; let other: i64 = 5; println!("{big} {}", small == other);}error[E0308]: mismatched types --> src/main.rs:3:20 | 3 | let big: u64 = small; | --- ^^^^^ expected `u64`, found `i32` | | | expected due to this | help: you can convert an `i32` to a `u64` and panic if the converted value doesn't fit | 3 | let big: u64 = small.try_into().unwrap(); | ++++++++++++++++++++ error[E0308]: mismatched types --> src/main.rs:5:35 | 5 | println!("{big} {}", small == other); | ----- ^^^^^ expected `i32`, found `i64` | | | expected because this is `i32` | help: you can convert `small` from `i32` to `i64`, matching the type of `other` | 5 | println!("{big} {}", i64::from(small) == other); | ++++++++++ +
fn main() { let n = 3; let scaled = n * 1.5; println!("{scaled}");}error[E0277]: cannot multiply `{integer}` by `{float}` --> src/main.rs:3:20 | 3 | let scaled = n * 1.5; | ^ no implementation for `{integer} * {float}` | = help: the trait `Mul<{float}>` is not implemented for `{integer}` = help: the following other types implement trait `Mul<Rhs>`: `&f128` implements `Mul<f128>` `&f128` implements `Mul` `&f16` implements `Mul<f16>` `&f16` implements `Mul` `&f32` implements `Mul<f32>` `&f32` implements `Mul` `&f64` implements `Mul<f64>` `&f64` implements `Mul` and 57 others
The help shows two fixes: i64::from(small) when no value can be lost, and try_into() when it might not fit. as is the third, and it cuts or rounds without a check (Declarations and types). For the float: f64::from(n) * 1.5.
No fall-through, and break in a match
A match arm runs on its own, so there is nothing to fall through to; several values share an arm with |. A break in an arm leaves the enclosing loop, as 2 => break does here:
fn main() { let mut n = 0; for i in 0..5 { match i { 2 => break, _ => n += 1, } } println!("n: {n}");}n: 2
In Go, Java, C#, C, C++ and TypeScript the same program with a switch leaves only the switch and counts to 4 (your own list above runs it). Rust and Kotlin agree on 2. Neither warns.
No exceptions
throw and catch are not Rust syntax, so the parser stops:
fn throws(n: i32) -> i32 { if n < 0 { throw "negative"; } n}
fn catches() { try { println!("work"); } catch (e) { println!("failed"); }}
fn main() {}error: expected one of `!`, `.`, `::`, `;`, `?`, `{`, `}`, or an operator, found `"negative"` --> src/main.rs:3:15 | 3 | throw "negative"; | ^^^^^^^^^^ expected one of 8 possible tokens error: keyword `catch` cannot follow a `try` block --> src/main.rs:11:7 | 11 | } catch (e) { | ^^^^^ | = help: try using `match` on the result of the `try` block instead
try is reserved for an experimental feature: the parser accepts a try { } block and stable refuses it (E0658). It is not an exception handler:
fn main() { try { println!("work"); }}error[E0658]: `try` expression is experimental --> src/main.rs:2:5 | 2 | / try { 3 | | println!("work"); 4 | | } | |_____^ | = note: see issue #154391 <https://github.com/rust-lang/rust/issues/154391> for more information error[E0277]: a `try` block must return `Result` or `Option` (or another type that implements `Try`) --> src/main.rs:4:5 | 4 | } | ^ could not wrap the final value of the block as `()` doesn't implement `Try` | = help: the nightly-only, unstable trait `Try` is not implemented for `()` Some errors have detailed explanations: E0277, E0658.
A function that can fail returns a Result, and ? passes the failure up to the caller (Errors). A bug is a panic!, which ends the thread. std::panic::catch_unwind exists, but its documentation says “It is not recommended to use this function for a general try/catch mechanism.”
use std::num::ParseIntError;
fn double(text: &str) -> Result<i32, ParseIntError> { let n = text.parse::<i32>()?; Ok(n * 2)}
fn main() { println!("double 21: {:?}", double("21")); println!("double x: {:?}", double("x"));}double 21: Ok(42)
double x: Err(ParseIntError { kind: InvalidDigit })No default or named arguments, no function overloading
width = 3 inside a call is an assignment, so the compiler looks for a variable called width; the message does not say “named arguments”. A default value in the parameter list is recognised and refused:
fn area(width: u32, height: u32) -> u32 { width * height}
fn main() { println!("{}", area(width = 3, height = 4)); println!("{}", area(3));}error[E0425]: cannot find value `width` in this scope --> src/main.rs:6:25 | 6 | println!("{}", area(width = 3, height = 4)); | ^^^^^ not found in this scope error[E0425]: cannot find value `height` in this scope --> src/main.rs:6:36 | 6 | println!("{}", area(width = 3, height = 4)); | ^^^^^^ not found in this scope error[E0061]: this function takes 2 arguments but 1 argument was supplied --> src/main.rs:7:20 | 7 | println!("{}", area(3)); | ^^^^--- argument #2 of type `u32` is missing | note: function defined here --> src/main.rs:1:4 | 1 | fn area(width: u32, height: u32) -> u32 { | ^^^^ ----------- help: provide the argument | 7 | println!("{}", area(3, /* u32 */)); | +++++++++++ Some errors have detailed explanations: E0061, E0425.
fn area(width: u32, height: u32 = 1) -> u32 { width * height}
fn main() { println!("{}", area(3, 1));}error: parameter defaults are not supported --> src/main.rs:1:33 | 1 | fn area(width: u32, height: u32 = 1) -> u32 { | ^^^
Two functions with one name are an error, and Methods shows the version for methods:
fn show(n: i32) -> String { format!("int {n}")}
fn show(s: &str) -> String { format!("text {s}")}
fn main() { println!("{}", show(1)); println!("{}", show("a"));}error[E0428]: the name `show` is defined multiple times --> src/main.rs:5:1 | 1 | fn show(n: i32) -> String { | ------------------------- previous definition of the value `show` here ... 5 | fn show(s: &str) -> String { | ^^^^^^^^^^^^^^^^^^^^^^^^^^ `show` redefined here | = note: `show` must be defined only once in the value namespace of this module error[E0308]: mismatched types --> src/main.rs:11:25 | 11 | println!("{}", show("a")); | ---- ^^^ expected `i32`, found `&str` | | | arguments to this function are incorrect | note: function defined here --> src/main.rs:1:4 | 1 | fn show(n: i32) -> String { | ^^^^ ------ Some errors have detailed explanations: E0308, E0428.
The Rust forms are a struct with #[derive(Default)] (fields start as 0, false or empty) and a literal that ends in ..Default::default(), which fills every field not written; or an Option parameter, where None means “use the default”:
#[derive(Default)]struct Window { width: u32, height: u32, border: bool,}
fn describe(w: &Window) -> String { format!("{}x{} border={}", w.width, w.height, w.border)}
fn scaled(width: u32, factor: Option<u32>) -> u32 { width * factor.unwrap_or(1)}
fn main() { let sized = Window { width: 80, height: 24, ..Default::default() }; let bordered = Window { border: true, ..Default::default() }; println!("sized: {}", describe(&sized)); println!("bordered: {}", describe(&bordered)); println!("scaled: {} {}", scaled(3, None), scaled(3, Some(4)));}sized: 80x24 border=false bordered: 0x0 border=true scaled: 3 12
This is more code than height=1; a type with many settings usually gets a builder (Methods). “No overloading” is narrow: a trait can be implemented for several types and called the same way, and impl Add overloads +.
Indexing text
s[0] does not compile for a string, and the note names the way out: s.chars().next() is an Option<char>, s.as_bytes()[0] a byte:
fn main() { let word = String::from("hello"); let first = word[0]; println!("{first}");}error[E0277]: the type `str` cannot be indexed by `{integer}` --> src/main.rs:3:22 | 3 | let first = word[0]; | ^ string indices are ranges of `usize` | = help: the trait `SliceIndex<str>` is not implemented for `{integer}` = note: you can use `.chars().nth()` or `.bytes().nth()` for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings> ... (notes pointing into the standard library trimmed)
No mutable globals
A static is a global with one fixed place in memory. A plain one cannot be assigned. static mut can, but every read or write needs an unsafe block (a block where you promise the compiler you checked what it cannot):
static VISITS: u32 = 0;
fn main() { VISITS += 1; println!("{VISITS}");}error[E0594]: cannot assign to immutable static item `VISITS` --> src/main.rs:4:5 | 1 | static VISITS: u32 = 0; | ------------------ this `static` cannot be written to ... 4 | VISITS += 1; | ^^^^^^^^^^^ cannot assign
static mut VISITS: u32 = 0;
fn main() { VISITS += 1;}error[E0133]: use of mutable static is unsafe and requires unsafe block --> src/main.rs:4:5 | 4 | VISITS += 1; | ^^^^^^ use of mutable static | = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
Inside unsafe it compiles, but edition 2024 denies even printing the value:
static mut VISITS: u32 = 0;
fn main() { unsafe { VISITS += 1; println!("{}", VISITS); }}error: creating a shared reference to mutable static --> src/main.rs:6:24 | 6 | println!("{}", VISITS); | ^^^^^^ shared reference to mutable static | = note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives = help: use a type that relies on "interior mutability" instead; to read more on this, visit <https://doc.rust-lang.org/reference/interior-mutability.html> = note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html> = note: `#[deny(static_mut_refs)]` (part of `#[deny(rust_2024_compatibility)]`) on by default
For a counter that several threads may change, use an atomic: a number whose changes happen as one step. For larger values use a Mutex (Shared state). Every atomic method takes an Ordering, which says how much other memory the operation makes visible to other threads. Ordering::Relaxed keeps the counter itself exact and orders nothing else, which is all a plain counter needs; the Ordering docs explain the stronger ones.
use std::sync::atomic::{AtomicU32, Ordering};
static VISITS: AtomicU32 = AtomicU32::new(0);
fn visit() -> u32 { VISITS.fetch_add(1, Ordering::Relaxed) + 1}
fn main() { visit(); visit(); println!("visits: {}", visit()); println!("loaded: {}", VISITS.load(Ordering::Relaxed));}visits: 3 loaded: 3
Going deeper
Where this costs you
- Numeric code that mixes widths, such as sizes (
usize) and counters (i32), has moreasandfromcalls. - Without inheritance, shared behaviour comes from a trait, or a type holds the other as a field.
Practice
Try it
Each exercise is a small program with checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed. Each is labelled with the readers it suits; the first two suit everybody, and the second asks you to read.
A mean with no division by zero (everyone)
Divide an i64 by a u32 after converting both yourself, and return None when there are no readings, so no NaN appears.
// The mean of `count` readings that add up to `total`.// Rust will not mix an i64 and a u32 in one division: convert both yourself.// With no readings there is no mean, so return None instead of a NaN.fn average(total: i64, count: u32) -> Option<f64> { todo!("rule out count == 0, then divide as floats")}
fn main() { assert_eq!(average(7, 2), Some(3.5)); assert_eq!(average(-7, 2), Some(-3.5)); assert_eq!(average(0, 4), Some(0.0)); assert_eq!(average(0, 0), None); assert_eq!(average(5, 0), None); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// The mean of `count` readings that add up to `total`.// Rust will not mix an i64 and a u32 in one division: convert both yourself.// With no readings there is no mean, so return None instead of a NaN.fn average(total: i64, count: u32) -> Option<f64> { if count == 0 { return None; } Some(total as f64 / f64::from(count))}
fn main() { assert_eq!(average(7, 2), Some(3.5)); assert_eq!(average(-7, 2), Some(-3.5)); assert_eq!(average(0, 4), Some(0.0)); assert_eq!(average(0, 0), None); assert_eq!(average(5, 0), None); println!("all checks passed");}all checks passed
Read this: what does it give? (everyone)
Reading, not writing: say what a loop with a break inside a match, -7 / 2, and a ? give, for None, an empty string and a real one. The checks run the real code and name the item you misread, not the right answer.
// Read this program. Replace each `todo!()` with what the marked call gives, then run// until the checks print `all checks passed`.fn tally() -> i32 { let mut total = 0; for i in 0..6 { match i { 1 | 2 => total += 10, 3 => break, _ => total += 1, } } total}
fn halves() -> (i32, i32) { (-7 / 2, 7 / 2)}
fn city_len(city: Option<&str>) -> Option<usize> { let c = city?; Some(c.len())}
/// Hint: how many times does the loop body run, and what does `break` leave?fn tally_is() -> i32 { todo!("how many times does the loop body run, and what does `break` leave?")}
/// Hint: the value of `halves()`: integer division with a negative numberfn halves_are() -> (i32, i32) { todo!("the value of `halves()`: integer division with a negative number")}
/// Hint: what does `city_len(None)` give? Look at what `?` doesfn no_city_gives() -> Option<usize> { todo!("what does `city_len(None)` give? Look at what `?` does")}
/// Hint: what does `city_len(Some(""))` give? Is an empty string a missing value?fn empty_city_gives() -> Option<usize> { todo!("what does `city_len(Some(\"\"))` give? Is an empty string a missing value?")}
/// Hint: what does `city_len(Some("Oslo"))` give?fn oslo_gives() -> Option<usize> { todo!("what does `city_len(Some(\"Oslo\"))` give?")}
fn check<T: PartialEq + std::fmt::Debug>(call: &str, said: T, truth: T) { assert!(said == truth, "misread `{call}`: you said {said:?}"); println!("ok: {call} = {said:?}");}
fn main() { check("tally()", tally_is(), tally()); check("halves()", halves_are(), halves()); check("city_len(None)", no_city_gives(), city_len(None)); check( "city_len(Some(\"\"))", empty_city_gives(), city_len(Some("")), ); check( "city_len(Some(\"Oslo\"))", oslo_gives(), city_len(Some("Oslo")), ); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Read this program. Replace each `todo!()` with what the marked call gives, then run// until the checks print `all checks passed`.fn tally() -> i32 { let mut total = 0; for i in 0..6 { match i { 1 | 2 => total += 10, 3 => break, _ => total += 1, } } total}
fn halves() -> (i32, i32) { (-7 / 2, 7 / 2)}
fn city_len(city: Option<&str>) -> Option<usize> { let c = city?; Some(c.len())}
/// Hint: how many times does the loop body run, and what does `break` leave?fn tally_is() -> i32 { 21}
/// Hint: the value of `halves()`: integer division with a negative numberfn halves_are() -> (i32, i32) { (-3, 3)}
/// Hint: what does `city_len(None)` give? Look at what `?` doesfn no_city_gives() -> Option<usize> { None}
/// Hint: what does `city_len(Some(""))` give? Is an empty string a missing value?fn empty_city_gives() -> Option<usize> { Some(0)}
/// Hint: what does `city_len(Some("Oslo"))` give?fn oslo_gives() -> Option<usize> { Some(4)}
fn check<T: PartialEq + std::fmt::Debug>(call: &str, said: T, truth: T) { assert!(said == truth, "misread `{call}`: you said {said:?}"); println!("ok: {call} = {said:?}");}
fn main() { check("tally()", tally_is(), tally()); check("halves()", halves_are(), halves()); check("city_len(None)", no_city_gives(), city_len(None)); check( "city_len(Some(\"\"))", empty_city_gives(), city_len(Some("")), ); check( "city_len(Some(\"Oslo\"))", oslo_gives(), city_len(Some("Oslo")), ); println!("all checks passed");}ok: tally() = 21
ok: halves() = (-3, 3)
ok: city_len(None) = None
ok: city_len(Some("")) = Some(0)
ok: city_len(Some("Oslo")) = Some(4)
all checks passedComing from TypeScript, Kotlin or C#
A `?.` chain (TypeScript, Kotlin, C#)
Find the length of a user’s city, where the user, the address and the city can each be missing, in a function that returns an Option. The ? of this page does the work of ?..
// user?.address?.city?.length ?? 0, in other languages, becomes a function that// returns an Option and uses `?`: each `?` returns None from the function when its// value is missing. An empty city is still a city, so its length is Some(0).struct Address { city: Option<String>,}
struct User { address: Option<Address>,}
fn city_len(user: Option<&User>) -> Option<usize> { todo!("user?.address.as_ref()?.city.as_ref().map(...)")}
fn main() { let home = User { address: Some(Address { city: Some("Paris".to_string()), }), }; let nowhere = User { address: None }; let unnamed = User { address: Some(Address { city: None }), }; let empty = User { address: Some(Address { city: Some(String::new()), }), }; assert_eq!(city_len(Some(&home)), Some(5)); assert_eq!(city_len(None), None); assert_eq!(city_len(Some(&nowhere)), None); assert_eq!(city_len(Some(&unnamed)), None); assert_eq!(city_len(Some(&empty)), Some(0)); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// user?.address?.city?.length ?? 0, in other languages, becomes a function that// returns an Option and uses `?`: each `?` returns None from the function when its// value is missing. An empty city is still a city, so its length is Some(0).struct Address { city: Option<String>,}
struct User { address: Option<Address>,}
fn city_len(user: Option<&User>) -> Option<usize> { let address = user?.address.as_ref()?; let city = address.city.as_ref()?; Some(city.len())}
fn main() { let home = User { address: Some(Address { city: Some("Paris".to_string()), }), }; let nowhere = User { address: None }; let unnamed = User { address: Some(Address { city: None }), }; let empty = User { address: Some(Address { city: Some(String::new()), }), }; assert_eq!(city_len(Some(&home)), Some(5)); assert_eq!(city_len(None), None); assert_eq!(city_len(Some(&nowhere)), None); assert_eq!(city_len(Some(&unnamed)), None); assert_eq!(city_len(Some(&empty)), Some(0)); println!("all checks passed");}all checks passed
Coming from Python
Floor division (Python)
Python’s // rounds toward minus infinity; Rust’s / truncates toward zero. Write floor_div. The negative cases are the checks.
// Python's `//` rounds toward minus infinity: -7 // 2 is -4.// Rust's `/` on integers cuts toward zero: -7 / 2 is -3.// Write floor_div for a divisor that is not zero.fn floor_div(a: i32, b: i32) -> i32 { todo!("a / b, minus one if inexact and the signs differ")}
fn main() { assert_eq!(floor_div(7, 2), 3); assert_eq!(floor_div(-7, 2), -4); assert_eq!(floor_div(7, -2), -4); assert_eq!(floor_div(-7, -2), 3); assert_eq!(floor_div(6, 3), 2); assert_eq!(floor_div(-6, 3), -2); assert_eq!(floor_div(0, 5), 0); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Python's `//` rounds toward minus infinity: -7 // 2 is -4.// Rust's `/` on integers cuts toward zero: -7 / 2 is -3.// Write floor_div for a divisor that is not zero.fn floor_div(a: i32, b: i32) -> i32 { let quotient = a / b; if a % b != 0 && ((a < 0) != (b < 0)) { quotient - 1 } else { quotient }}
fn main() { assert_eq!(floor_div(7, 2), 3); assert_eq!(floor_div(-7, 2), -4); assert_eq!(floor_div(7, -2), -4); assert_eq!(floor_div(-7, -2), 3); assert_eq!(floor_div(6, 3), 2); assert_eq!(floor_div(-6, 3), -2); assert_eq!(floor_div(0, 5), 0); println!("all checks passed");}all checks passed
Coming from C++, C#, Kotlin or Python
An operator (C++, C#, Kotlin, Python)
Implement + for a small point type with a trait, so that a + b works and a and b can still be used afterwards.
use std::ops::Add;
// C++: Point operator+(Point a, Point b). In Rust the operator is a trait:// `impl Add for Point` defines what `a + b` means.#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { todo!("add the two x values and the two y values") }}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 3, y: 4 }; assert_eq!(a + b, Point { x: 4, y: 6 }); // a and b are Copy, so they can still be used after the sum. assert_eq!(a + a, Point { x: 2, y: 4 }); assert_eq!(b + Point { x: -3, y: -4 }, Point { x: 0, y: 0 }); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::ops::Add;
// C++: Point operator+(Point a, Point b). In Rust the operator is a trait:// `impl Add for Point` defines what `a + b` means.#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } }}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 3, y: 4 }; assert_eq!(a + b, Point { x: 4, y: 6 }); // a and b are Copy, so they can still be used after the sum. assert_eq!(a + a, Point { x: 2, y: 4 }); assert_eq!(b + Point { x: -3, y: -4 }, Point { x: 0, y: 0 }); println!("all checks passed");}all checks passed
Read more
- The Rust Reference: Operator expressions (the table for
!and-), Loop expressions (labels), Match expressions, Static items and Pointer types. - The Rust Book, chapter 3.5: Control Flow, 6.1: Defining an Enum (
Option), 9: Error Handling, 10.2: Traits and 18.1: Characteristics of Object-Oriented Languages, for what replaces inheritance. catch_unwind,ptr::null,Default,atomic,OrderingandAddin the standard library docs.- Rust by Example: Nesting and labels, match and Casting.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.