Declarations, literals and types
Syntax · Lesson 3
Variables, constants, functions and types are declared with a keyword (let, const, fn, struct) and then a name. A type, where you write one, goes after the name behind a colon. Numbers, characters and strings have their own literal syntax, and a few small symbols (as, ::<>, <>, |x|) cover conversions, generics and closures.
Coming from Gox := 5 is let x = 5;. The type goes after a colon, there is no a, b int shorthand, and the result type follows ->. Go's int is 64 bits on a 64-bit machine; Rust's default integer is i32, so use i64 (or isize) where you mean Go's int. const always needs a type, and 'R' is a char, not a number.
Coming from KotlinThis is the nearest syntax to yours: name: Type, generics in <>. The changes are val -> let, var -> let mut, fun -> fn, and a return type after -> instead of :. Int is i32, and $name in a string template is {name}, written inside format-style macros such as println! and format!.
Coming from Pythonname: str = "Ada" looks like your annotations, but Rust checks them. Every first assignment needs let, and 'a' and "a" are different types (a character and a string). int(f) on a float becomes f as i32; int("42") on text becomes "42".parse::<i32>(). Underscores in numbers (1_000) work as you know them.
Coming from TypeScript: type after the name is already your habit, and 1_000 and 0xff work. const -> let, let -> let mut. Your one number becomes many types (i32, f64...), so let x: f64 = 5; is an error, and single quotes make a char, never a string.
Coming from JavaThe type moves after the name: int count = 5; is let count: i32 = 5;. let works like final and is the default, mut is the exception, and (int) x is x as i32. Generics use <> in types, like Vec<i32>, but need ::<> when you write them in an expression.
Coming from C#The type moves after the name, and var x = 5; is let mut x = 5; (a plain let cannot change). (int)x is x as i32, and a verbatim string @"C:\temp" is r"C:\temp". 1_000 and 0xff work as you know them; the f suffix is f32, so 2.5f is 2.5f32.
Coming from Cint x = 5; is let mut x: i32 = 5;, and #define MAX 100 is const MAX: i32 = 100;, which has a type. 'a' is a char, not an int, (int)x is x as i32, and a leading zero is not octal: 0o17 is octal and 017 is the decimal number 17.
Coming from C++auto x = 5; is let mut x = 5; and const auto x = 5; is let x = 5;. Types that take <> in declarations (Vec<i32>) need ::<> in expressions, static_cast<int>(x) is x as i32, and 5u, 2.5f suffixes exist too (5u32, 2.5f32).
The quick comparison
const LIMIT: u32 = 1_000; // a constant: the type is required
fn half(x: f64) -> f64 { x / 2.0}
fn main() { let name: &str = "Ada"; // type after the name let mut visits = 0u8; // `u8` is a suffix: this 0 is a u8 visits += 1; let (mask, ratio) = (0xff, 2.5f32); // two names, one pattern let letter = 'R'; // single quotes: one character let total = LIMIT as f64 + half(5.0); // `as` converts let parsed = "42".parse::<i32>().unwrap(); // `unwrap`: stop if not a number
println!("name={name} visits={visits}"); println!("mask={mask} ratio={ratio} letter={letter}"); println!("limit={LIMIT} total={total}"); println!("parsed={parsed}");}name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
package main
import ( "fmt" "strconv")
const limit uint32 = 1_000
func half(x float64) float64 { return x / 2.0}
func main() { var name string = "Ada" var visits uint8 = 0 visits++ mask, ratio := 0xff, float32(2.5) letter := 'R' total := float64(limit) + half(5.0) parsed, _ := strconv.Atoi("42")
fmt.Printf("name=%s visits=%d\n", name, visits) fmt.Printf("mask=%d ratio=%v letter=%c\n", mask, ratio, letter) fmt.Printf("limit=%d total=%v\n", limit, total) fmt.Printf("parsed=%d\n", parsed)}name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
const val LIMIT = 1_000
fun half(x: Double): Double = x / 2.0
fun main() { val name: String = "Ada" var visits: Byte = 0 visits++ val (mask, ratio) = Pair(0xff, 2.5f) val letter = 'R' val total = LIMIT.toDouble() + half(5.0) val parsed = "42".toInt()
println("name=$name visits=$visits") println("mask=$mask ratio=$ratio letter=$letter") println("limit=$LIMIT total=$total") println("parsed=$parsed")}name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
LIMIT = 1_000
def half(x: float) -> float: return x / 2.0
name: str = "Ada"visits = 0visits += 1mask, ratio = 0xFF, 2.5letter = "R"total = float(LIMIT) + half(5.0)parsed = int("42")
print(f"name={name} visits={visits}")print(f"mask={mask} ratio={ratio} letter={letter}")print(f"limit={LIMIT} total={total}")print(f"parsed={parsed}")name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
const LIMIT = 1_000;
function half(x: number): number { return x / 2.0;}
const name: string = "Ada";let visits = 0;visits += 1;const [mask, ratio] = [0xff, 2.5];const letter = "R";const total = LIMIT + half(5.0);const parsed = parseInt("42", 10);
console.log(`name=${name} visits=${visits}`);console.log(`mask=${mask} ratio=${ratio} letter=${letter}`);console.log(`limit=${LIMIT} total=${total}`);console.log(`parsed=${parsed}`);name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
public class Main { static final int LIMIT = 1_000; // Rust: u32. Java has no unsigned integers
static double half(double x) { return x / 2.0; }
public static void main(String[] args) { String name = "Ada"; byte visits = 0; // Rust: u8. Java's byte is signed, the closest it has visits++; int mask = 0xff; float ratio = 2.5f; char letter = 'R'; double total = (double) LIMIT + half(5.0); int parsed = Integer.parseInt("42");
System.out.println("name=" + name + " visits=" + visits); System.out.println("mask=" + mask + " ratio=" + ratio + " letter=" + letter); System.out.println("limit=" + LIMIT + " total=" + total); System.out.println("parsed=" + parsed); }}name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
const uint Limit = 1_000;
static double Half(double x) => x / 2.0;
string name = "Ada";byte visits = 0;visits++;var (mask, ratio) = (0xff, 2.5f);char letter = 'R';double total = (double)Limit + Half(5.0);int parsed = int.Parse("42");
Console.WriteLine($"name={name} visits={visits}");Console.WriteLine($"mask={mask} ratio={ratio} letter={letter}");Console.WriteLine($"limit={Limit} total={total}");Console.WriteLine($"parsed={parsed}");name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
#include <stdio.h>#include <stdlib.h>
#define LIMIT 1000u
static double half(double x) { return x / 2.0;}
int main(void) { const char *name = "Ada"; unsigned char visits = 0; visits += 1; int mask = 0xff; float ratio = 2.5f; char letter = 'R'; double total = (double)LIMIT + half(5.0); int parsed = atoi("42");
printf("name=%s visits=%d\n", name, visits); printf("mask=%d ratio=%.1f letter=%c\n", mask, ratio, letter); printf("limit=%u total=%.1f\n", LIMIT, total); printf("parsed=%d\n", parsed); return 0;}name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
#include <cstdint>#include <iostream>#include <string>
constexpr std::uint32_t LIMIT = 1'000;
double half(double x) { return x / 2.0;}
int main() { std::string name = "Ada"; std::uint8_t visits = 0; visits += 1; int mask = 0xff; float ratio = 2.5f; char letter = 'R'; double total = static_cast<double>(LIMIT) + half(5.0); int parsed = std::stoi("42");
std::cout << "name=" << name << " visits=" << int(visits) << "\n"; std::cout << "mask=" << mask << " ratio=" << ratio << " letter=" << letter << "\n"; std::cout << "limit=" << LIMIT << " total=" << total << "\n"; std::cout << "parsed=" << parsed << "\n";}name=Ada visits=1 mask=255 ratio=2.5 letter=R limit=1000 total=1002.5 parsed=42
What's different
- A variable, constant or function starts with a keyword and a name. A type, where you write one, follows the name after a colon. A function's result type follows
->, and a function without one returns(). - On
letthe type is optional (Rust infers it). Onconst,static, and function parameters it is required. - A number can carry its type as a suffix:
1u8,2.5f32._and the0x,0o,0bprefixes change how it is written, not its type. 'R'is onechar,"R"is a string.'ab'is an error.x as f64converts between number types.parse::<i32>()names the type for a generic call, and the::before<is required there.
Putting the type before the name
Looks fine: in Java, C#, C and C++ it runs fine. Rust won't compile it.
Go refuses this too. Same idea, different words.Kotlin refuses this too. Same idea, different words.Python refuses this too. Same idea, different words.TypeScript refuses this too. Same idea, different words.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() { i32 count = 5; println!("count={count}");}Go refuses: int count = 5 is a syntax error. Go declares with var or :=, and a type, if you write one, comes after the name: var count int = 5.
package main
import "fmt"
func main() { int count = 5 fmt.Printf("count=%d\n", count)}# command-line-arguments ./main.go:6:6: syntax error: unexpected name count at end of statement
Kotlin refuses. Declarations start with val or var, and the type goes after the name: val count: Int = 5.
fun main() { Int count = 5 println("count=$count")}Main.kt:2:9: error: unresolved reference 'count'.
Int count = 5
^^^^^
Main.kt:2:15: error: syntax error: Expecting an element.
Int count = 5
^
Main.kt:2:17: error: syntax error: Unexpected tokens (use ';' to separate expressions on the same line).
Int count = 5
^
Main.kt:3:21: error: unresolved reference 'count'.
println("count=$count")
^^^^^int count = 5print(f"count={count}") File "main.py", line 1
int count = 5
^^^^^
SyntaxError: invalid syntaxTypeScript refuses. A declaration needs let or const, and the type goes after the name: let count: number = 5.
number count = 5;console.log(`count=${count}`);main.ts(1,1): error TS1434: Unexpected keyword or identifier.
public class Main { public static void main(String[] args) { int count = 5; System.out.println("count=" + count); }}count=5
int count = 5;Console.WriteLine($"count={count}");count=5
#include <stdio.h>
int main(void) { int count = 5; printf("count=%d\n", count); return 0;}count=5
#include <iostream>
int main() { int count = 5; std::cout << "count=" << count << "\n";}count=5
What does the compiler say? Guess first, then open.
error: expected one of `!`, `.`, `::`, `;`, `?`, `{`, `}`, or an operator, found `count` --> src/main.rs:2:9 | 2 | i32 count = 5; | ^^^^^ expected one of 8 possible tokens
Go refuses: int count = 5 is a syntax error. Go declares with var or :=, and a type, if you write one, comes after the name: var count int = 5.
Kotlin refuses. Declarations start with val or var, and the type goes after the name: val count: Int = 5.
Python raises a SyntaxError. Python has no type-first declaration, only count: int = 5 as an optional annotation.
TypeScript refuses. A declaration needs let or const, and the type goes after the name: let count: number = 5.
Java prints count=5. The type before the name is the usual way to declare a local (var also exists).
C# prints count=5. int count = 5; is a normal local declaration.
C prints count=5. The type goes first.
C++ prints count=5. The type goes first.
Rust reads i32 count = 5; as an expression starting with the name i32, followed by something unexpected. A variable starts with let, then the name, then : i32.
Rust starts variables, constants, functions and types with a keyword (let, const, fn, struct), and the type is an extra after a colon. The error does not say "wrong order" because the parser sees i32 count as a name and then an unexpected name; the long list of tokens is its way of saying it did not expect a second name there. The fix is let count: i32 = 5;, or just let count = 5;. Go, Kotlin, Python and TypeScript refuse the C-style form for their own reasons, as the outputs show.
The fix
Compiles and runs.
fn main() { let count: i32 = 5; // `let`, then the name, then `: type` println!("count={count}");}count=5
The toolkit
Variables, constants, functions and types are declared with a keyword (let, const, fn, struct) and then a name. Where you write a type, it goes after the name, behind a colon. This page teaches only the spelling. The meaning is in the linked lessons: Variables, Numbers and types, Match.
The pieces in one table
| Syntax | What it is | Example | Learn more |
|---|---|---|---|
let, let mut |
A variable that cannot change, or can. | let x = 5; let mut n = 0; |
Variables |
const |
A name for a value known before the program runs. | const MAX: u32 = 99; |
Reference |
static |
A value stored once for the whole program. | static NAME: &str = "Ada"; |
Reference |
fn |
A function. The result type follows ->. |
fn add(a: i32, b: i32) -> i32 |
Functions |
struct |
A record: named fields, or numbered. | struct Point { x: i32 } struct Meters(f64); |
Structs |
enum |
One of several shapes. | enum Shape { Circle(f64), Empty } |
Enums |
(A, B) |
A few values of different types. | (7, "seven"), t.0 |
Tuples |
[T; N], &[T], Vec<T> |
A fixed-size array; a slice (a borrowed view of part of one); a growable list. | [0u8; 4], &v[1..4], vec![1, 2] |
Vectors |
a..b, a..=b |
A range, without and with the end. | 0..5, 2.., ..3 |
Loops |
<T> |
Type arguments. | Option<String> |
Generics |
|x| x + 1 |
A closure: a function with no name. | |n| n * 2 |
Closures |
_ |
“Ignore this part.” | let (a, _) = pair; |
Reference |
struct Point { x: i32, y: i32,}
struct Meters(f64);
fn main() { let p = Point { x: 3, y: 4 }; let m = Meters(2.5); let pair: (i32, &str) = (7, "seven"); let grid: [i32; 3] = [10, 20, 30]; let zeros = [0u8; 4]; let v: Vec<i32> = vec![1, 2, 3, 4, 5]; let middle: &[i32] = &v[1..4];
println!("point: {} {}", p.x, p.y); println!("meters: {}", m.0); println!("tuple: {} {}", pair.0, pair.1); println!("array: {grid:?} {zeros:?}"); println!("slice: {middle:?}"); println!("ranges: {:?} {:?} {:?} {:?}", 0..5, 0..=5, 2.., ..3);}point: 3 4 meters: 2.5 tuple: 7 seven array: [10, 20, 30] [0, 0, 0, 0] slice: [2, 3, 4] ranges: 0..5 0..=5 2.. ..3
Declaring: let, const, static
const MAX_LEVEL: u32 = 99;static GREETING: &str = "hello";
fn main() { let score = 10; // type inferred let mut lives = 3; // can change let ratio: f64 = 0.5; // type written lives -= 1;
println!("score: {score}, lives: {lives}, ratio: {ratio}"); println!("max level: {MAX_LEVEL}, greeting: {GREETING}");}score: 10, lives: 2, ratio: 0.5 max level: 99, greeting: hello
let makes a variable, and its type is optional because Rust works it out. const and static need one:
const LIMIT = 100;static NAME = "Ada";
fn main() { println!("{LIMIT} {NAME}");}error: missing type for `const` item --> src/main.rs:1:12 | 1 | const LIMIT = 100; | ^ help: provide a type for the constant: `: i32` error: missing type for `static` item --> src/main.rs:2:12 | 2 | static NAME = "Ada"; | ^ help: provide a type for the static variable: `: &str`
A const is copied into every place that uses it. A static is one value stored once, and every reference to it points to that one place. Both can sit outside any function. let cannot:
let LIMIT: u32 = 100;
fn main() { println!("{LIMIT}");}error: expected item, found keyword `let` --> src/main.rs:1:1 | 1 | let LIMIT: u32 = 100; | ^^^ | | | `let` cannot be used for global variables | help: consider using `static` or `const` instead of `let` | = note: for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>
The type goes after the name
Read count: i32 as “count is an i32”. Rust does not convert between types for you: text is not a number, a whole number is not a float, and an i32 is not an i64, even though it would fit.
fn main() { let count: i32 = "5"; let ratio: f64 = 1; let small: i32 = 7; let wide: i64 = small; println!("{count} {ratio} {wide}");}error[E0308]: mismatched types --> src/main.rs:2:22 | 2 | let count: i32 = "5"; | --- ^^^ expected `i32`, found `&str` | | | expected due to this error[E0308]: mismatched types --> src/main.rs:3:22 | 3 | let ratio: f64 = 1; | --- ^ expected `f64`, found integer | | | expected due to this | help: use a float literal | 3 | let ratio: f64 = 1.0; | ++ error[E0308]: mismatched types --> src/main.rs:5:21 | 5 | let wide: i64 = small; | --- ^^^^^ expected `i64`, found `i32` | | | expected due to this | help: you can convert an `i32` to an `i64` | 5 | let wide: i64 = small.into(); | +++++++
Number types spell their kind and size. i32 is a signed whole number in 32 bits, u8 an unsigned one (zero and up) in 8 bits, f64 a 64-bit float, and usize the unsigned type for lengths and indexes. An unsigned number cannot go below zero:
fn last_index(items: &[i32]) -> usize { items.len() - 1 // `len()` is a usize: a whole number that cannot go below 0}
fn main() { println!("last_index: {}", last_index(&[1, 2, 3])); println!("last_index: {}", last_index(&[]));}last_index: 2 thread 'main' (1) panicked at src/main.rs:2:5: attempt to subtract with overflow note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Your language, side by side
Literals
A literal is a value written directly in the code. This program prints each one with its type:
use std::any::type_name_of_val; // gives the name of a value's type
fn main() { println!("42 = {} ({})", 42, type_name_of_val(&42)); println!( "1_000_000 = {} ({})", 1_000_000, type_name_of_val(&1_000_000) ); println!("0xff = {} ({})", 0xff, type_name_of_val(&0xff)); println!("0o17 = {} ({})", 0o17, type_name_of_val(&0o17)); println!("0b1010 = {} ({})", 0b1010, type_name_of_val(&0b1010)); println!("7u8 = {} ({})", 7u8, type_name_of_val(&7u8)); println!("7_i64 = {} ({})", 7_i64, type_name_of_val(&7_i64)); println!("2.5 = {} ({})", 2.5, type_name_of_val(&2.5)); println!("2.5f32 = {} ({})", 2.5f32, type_name_of_val(&2.5f32)); println!("1e3 = {} ({})", 1e3, type_name_of_val(&1e3)); println!("'R' = {} ({})", 'R', type_name_of_val(&'R')); println!("\"R\" = {} ({})", "R", type_name_of_val(&"R")); println!("b'R' = {} ({})", b'R', type_name_of_val(&b'R')); println!("b\"Rs\" = {:?} ({})", b"Rs", type_name_of_val(&b"Rs")); println!("true = {} ({})", true, type_name_of_val(&true)); println!("() = {:?} ({})", (), type_name_of_val(&()));}42 = 42 (i32) 1_000_000 = 1000000 (i32) 0xff = 255 (i32) 0o17 = 15 (i32) 0b1010 = 10 (i32) 7u8 = 7 (u8) 7_i64 = 7 (i64) 2.5 = 2.5 (f64) 2.5f32 = 2.5 (f32) 1e3 = 1000 (f64) 'R' = R (char) "R" = R (&str) b'R' = 82 (u8) b"Rs" = [82, 115] (&[u8; 2]) true = true (bool) () = () (())
| You write | Type | Notes |
|---|---|---|
42, 1_000_000 |
i32, unless something else decides |
_ can go between digits and before a suffix. |
0xff, 0o17, 0b1010 |
integer | A leading 0 alone is not octal: 017 is 17. |
7u8, 7_i64, 2.5f32 |
the suffix’s type | i8..i128, u8..u128, isize, usize, f32, f64. |
2.5, 1e3 |
f64, unless something else decides |
1e3 is a float. |
'R', '\n', '\u{1F980}' |
char |
One Unicode scalar value (a code point). |
"R" |
&str |
A string. It may span lines. A \ at the end of a line skips the break and the indentation. |
r"C:\temp", r#"say "hi""# |
&str |
Raw: no escapes. The # on both ends lets the text contain ". |
b'R', b"Rs" |
u8, &[u8; 2] |
An ASCII byte, and a byte string. |
fn main() { let tab = "a\tb"; // escapes work in normal strings let raw = r"C:\temp\new"; // raw: backslashes stay let quoted = r#"she said "hi""#; // raw with quotes inside: add # on both ends let long = "one \ two"; // a backslash at the end of a line skips the newline and the indent let lines = "firstsecond"; // a normal string may span lines
println!("tab: {tab}"); println!("raw: {raw} ({} bytes)", raw.len()); println!("quoted: {quoted}"); println!("long: {long}"); println!("lines: {lines:?}"); println!("crab: {} {}", '\u{1F980}', "\u{e9}");}tab: a b raw: C:\temp\new (11 bytes) quoted: she said "hi" long: one two lines: "first\nsecond" crab: 🦀 é
Single quotes hold exactly one character. If you write a word in them, the compiler suggests double quotes:
fn main() { let word = 'hello'; println!("{word}");}error: character literal may only contain one codepoint --> src/main.rs:2:16 | 2 | let word = 'hello'; | ^^^^^^^ | help: if you meant to write a string literal, use double quotes | 2 - let word = 'hello'; 2 + let word = "hello";
as and the turbofish
as converts between number types. It also turns a char or bool into an integer (not a float), and a u8 into a char. It never fails at run time, which is why it can lose information silently:
fn main() { let big: i32 = 300; let neg: i32 = -1; let x: f64 = -2.9;
println!("big as u8: {}", big as u8); println!("-1 as u8: {}", neg as u8); println!("-1 as u32: {}", neg as u32); println!("200u8 as i8: {}", 200u8 as i8); println!("-2.9 as i32: {}", x as i32); println!("300.0 as u8: {}", 300.0f64 as u8); println!("-5.0 as u8: {}", -5.0f64 as u8); println!("NaN as i32: {}", f64::NAN as i32); println!("'a' as u32: {}", 'a' as u32); println!("97u8 as char: {}", 97u8 as char); println!("true as i32: {}", true as i32); println!("i64::from(7i32): {}", i64::from(7i32)); println!("u8::try_from(300i32): {:?}", u8::try_from(300i32));}big as u8: 44 -1 as u8: 255 -1 as u32: 4294967295 200u8 as i8: -56 -2.9 as i32: -2 300.0 as u8: 255 -5.0 as u8: 0 NaN as i32: 0 'a' as u32: 97 97u8 as char: a true as i32: 1 i64::from(7i32): 7 u8::try_from(300i32): Err(TryFromIntError(PosOverflow))
Integers that do not fit keep only their low bits (big as u8 is 44 when big is 300, because a u8 holds 0 to 255). The same bits can be read the other way: 200u8 as i8 is -56, because the top bit of a signed integer is its sign. Floats cut toward zero and saturate, stopping at the smallest or largest value, and NaN becomes 0. The standard library has From (i64::from) only for conversions that cannot lose data, and u8::try_from(300) returns an Err instead of cutting the value, as the last line shows.
A literal that does not fit is refused, not cut: 300 as u8 does not compile, because the overflowing_literals lint is deny by default. The big as u8 above compiles, because big is a variable.
fn main() { println!("300 as u8: {}", 300 as u8);}error: literal out of range for `u8` --> src/main.rs:2:31 | 2 | println!("300 as u8: {}", 300 as u8); | ^^^ | = note: the literal `300` does not fit into the type `u8` whose range is `0..=255` = note: `#[deny(overflowing_literals)]` on by default
A conversion that makes no sense is refused too:
fn main() { let code = 'a' as u32; // fine: a char becomes an integer let wrong = 'a' as f64; // not allowed println!("{code} {wrong}");}error[E0606]: casting `char` as `f64` is invalid --> src/main.rs:3:17 | 3 | let wrong = 'a' as f64; // not allowed | ^^^^^^^^^^ | = help: cast through an integer first
The turbofish ::<T> gives a type argument inside an expression, because parse<i32>() would read as comparisons. unwrap takes the number out of parse’s result and stops the program if the text was not one (Errors shows better handling). _ in Vec<_> means “you work this part out”, and collect gathers pieces into a list (Iterators):
fn main() { let a = "42".parse::<i32>().unwrap(); // turbofish: ::<type> let b: i32 = "42".parse().unwrap(); // or annotate the variable let words = "a b c".split(' ').collect::<Vec<_>>(); // gather the pieces into a Vec let empty = Vec::<u8>::new();
println!("a: {a}, b: {b}"); println!("words: {words:?}, empty: {empty:?}");}a: 42, b: 42 words: ["a", "b", "c"], empty: []
Leave the type out and Rust has nothing to go on:
fn main() { let n = "42".parse().unwrap(); println!("{n}");}error[E0284]: type annotations needed --> src/main.rs:2:9 | 2 | let n = "42".parse().unwrap(); | ^ ----- type must be known at this point | = note: cannot satisfy `<_ as FromStr>::Err == _` = note: the type must also implement `FromStr` help: consider giving `n` an explicit type | 2 | let n: /* Type */ = "42".parse().unwrap(); | ++++++++++++ ... (notes pointing into the standard library trimmed)
Patterns in let, and shadowing
The left side of a let is a pattern: a shape that a value is taken apart by, where each name takes its part.
struct Point { x: i32, y: i32,}
fn main() { let (a, b) = (1, "two"); let Point { x, y } = Point { x: 3, y: 4 }; let [first, _, third] = [10, 20, 30]; let (lo, _) = (5, 6); // `_` ignores a part
println!("tuple: {a} {b}"); println!("struct: {x} {y}"); println!("array: {first} {third}"); println!("lo: {lo}");}tuple: 1 two struct: 3 4 array: 10 30 lo: 5
A pattern in a plain let must match every possible value. When it might not, add an else block that does not fall through (return, continue, break or a panic). Some(...) and Ok(...) are the shapes you will use most (Option, Errors):
fn main() { let maybe = Some(3); let Some(n) = maybe; println!("{n}");}error[E0005]: refutable pattern in local binding --> src/main.rs:3:9 | 3 | let Some(n) = maybe; | ^^^^^^^ pattern `None` not covered | = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html = note: the matched value is of type `Option<i32>` help: you might want to use `let...else` to handle the variant that isn't matched | 3 | let Some(n) = maybe else { todo!() }; | ++++++++++++++++
fn first_word_len(text: &str) -> usize { // `split_once` gives `Some((before, after))`, or `None` when there is no space let Some((word, _)) = text.split_once(' ') else { return text.len(); // the `else` block must leave: return, continue, break or panic }; word.len()}
fn main() { println!( "first_word_len: {} {}", first_word_len("hello world"), first_word_len("hi") ); for text in ["1", "x", "3"] { let Ok(n) = text.parse::<i32>() else { continue; // skip the texts that are not numbers }; println!("number: {n}"); }}first_word_len: 5 2 number: 1 number: 3
Writing let again with the same name makes a new variable that hides the old one. The type may change, and an inner block’s version disappears at its closing brace:
fn main() { let spaces = " "; let spaces = spaces.len(); // a new variable that reuses the name, with a new type println!("spaces: {spaces}");
let x = 1; { let x = x * 10; // shadows only inside this block println!("inner x: {x}"); } println!("outer x: {x}");}spaces: 3 inner x: 10 outer x: 1
Naming
Rust expects snake_case for values and UpperCamelCase for types. The compiler warns when you break this (the lints non_snake_case, non_camel_case_types and non_upper_case_globals, listed here). #![deny(nonstandard_style)] at the top of a file (an “inner attribute”, a setting for the whole file) turns all three into errors, which shows what the compiler thinks:
#![deny(nonstandard_style)]
const max_users: u32 = 10;
struct user_account { UserName: String,}
fn main() { let myCount = 3; let account = user_account { UserName: String::from("ada"), }; println!("{myCount} {max_users} {}", account.UserName);}error: type `user_account` should have an upper camel case name --> src/main.rs:5:8 | 5 | struct user_account { | ^^^^^^^^^^^^ help: convert the identifier to upper camel case: `UserAccount` | note: the lint level is defined here --> src/main.rs:1:9 | 1 | #![deny(nonstandard_style)] | ^^^^^^^^^^^^^^^^^ = note: `#[deny(non_camel_case_types)]` implied by `#[deny(nonstandard_style)]` error: constant `max_users` should have an upper case name --> src/main.rs:3:7 | 3 | const max_users: u32 = 10; | ^^^^^^^^^ | = note: `#[deny(non_upper_case_globals)]` implied by `#[deny(nonstandard_style)]` help: convert the identifier to upper case | 3 - const max_users: u32 = 10; 3 + const MAX_USERS: u32 = 10; error: structure field `UserName` should have a snake case name --> src/main.rs:6:5 | 6 | UserName: String, | ^^^^^^^^ help: convert the identifier to snake case: `user_name` | = note: `#[deny(non_snake_case)]` implied by `#[deny(nonstandard_style)]` error: variable `myCount` should have a snake case name --> src/main.rs:10:9 | 10 | let myCount = 3; | ^^^^^^^ help: convert the identifier to snake case: `my_count`
| What | Convention | Example |
|---|---|---|
| Variables, functions, methods, modules, fields | snake_case |
user_count, parse_line |
| Structs, enums, traits, enum variants | UpperCamelCase |
UserAccount, Shape::Circle |
| Constants and statics | SCREAMING_SNAKE_CASE |
MAX_USERS |
| Type parameters, lifetimes (Lifetimes) | one capital letter; short lowercase after ' |
T, 'a |
Acronyms count as one word (Uuid), and the built-in types are lowercase (i32, str).
A strict or reserved keyword such as type cannot be a name. The compiler suggests a raw identifier, r#type (or a different name such as kind):
struct Event { type: String,}
fn main() { let e = Event { type: String::from("click"), }; println!("{}", e.type);}error: expected identifier, found keyword `type` --> src/main.rs:2:5 | 1 | struct Event { | ----- while parsing this struct 2 | type: String, | ^^^^ expected identifier, found keyword | help: escape `type` to use it as an identifier | 2 | r#type: String, | ++ error: expected identifier, found keyword `type` --> src/main.rs:7:9 | 7 | type: String::from("click"), | ^^^^ expected identifier, found keyword | help: escape `type` to use it as an identifier | 7 | r#type: String::from("click"), | ++ error: expected identifier, found keyword `type` --> src/main.rs:9:22 | 9 | println!("{}", e.type); | ^^^^ expected identifier, found keyword
Going deeper
The type is often optional
Rust infers types from how a value is used, including from lines further down. Only when nothing decides does it fall back to i32 for integers and f64 for floats (see the Reference):
use std::any::type_name_of_val;
fn main() { let mut bytes = Vec::new(); // no type yet... bytes.push(1u8); // ...this line makes it a Vec<u8>
let whole = 7; // nothing decides: an integer defaults to i32 let fraction = 7.0; // and a float to f64 let small = 7; let total: u64 = small + 1; // `small` becomes u64, because it is added to a u64
println!("bytes[0]: {}", type_name_of_val(&bytes[0])); println!("whole: {}", type_name_of_val(&whole)); println!("fraction: {}", type_name_of_val(&fraction)); println!("small: {}", type_name_of_val(&small)); println!("total: {total}");}bytes[0]: u8 whole: i32 fraction: f64 small: u64 total: 8
When even the later lines cannot decide, you get a “type annotations needed” error. The fix is a type on the let, or a turbofish:
fn main() { let v = Vec::new(); println!("{}", v.len());}error[E0282]: type annotations needed for `Vec<_>` --> src/main.rs:2:9 | 2 | let v = Vec::new(); | ^ ---------- type must be known at this point | help: consider giving `v` an explicit type, where the type for type parameter `T` is specified | 2 | let v: Vec<T> = Vec::new(); | ++++++++
Declared now, set later
Go gives every variable a zero value, and reading a C local that was never assigned is undefined behaviour. Rust allows a let without a value, but you cannot read the variable until every path has assigned it:
fn main() { let label; // declared, no value yet let n = 3; if n > 2 { label = "big"; } else { label = "small"; } println!("label: {label}");}label: big
fn main() { let count: i32; println!("{count}");}error[E0381]: used binding `count` isn't initialized --> src/main.rs:3:16 | 2 | let count: i32; | ----- binding declared here but left uninitialized 3 | println!("{count}"); | ^^^^^ `count` used here but it isn't initialized | help: consider assigning a value | 2 | let count: i32 = 42; | ++++
Constants and array lengths
An array length is part of the type, so it must be a constant, not a variable:
fn main() { let n = 3; let grid = [0; n]; println!("{}", grid.len());}error[E0435]: attempt to use a non-constant value in a constant --> src/main.rs:3:20 | 3 | let grid = [0; n]; | ^ non-constant value | help: consider using `const` instead of `let` | 2 - let n = 3; 2 + const n: /* Type */ = 3;
A const may be built from other constants. The second half of the next program shows that two references to the same static have one address. The Reference promises that for a static, not for a const.
const WIDTH: usize = 3;const CELLS: usize = WIDTH * 2; // a constant can use other constants
static GREETING: &str = "hello";
fn main() { let grid = [0u8; CELLS]; println!("cells: {}", grid.len());
let a = &GREETING; let b = &GREETING; println!("same address: {}", std::ptr::eq(a, b));}cells: 6 same address: true
A char is not a byte
A Rust char is one Unicode scalar value and is 4 bytes wide, while a u8 is one byte. A "string" is bytes in UTF-8, so its len() can be larger than its number of characters:
fn main() { println!("size_of::<char>(): {}", size_of::<char>()); println!("size_of::<u8>(): {}", size_of::<u8>()); println!("'e'.len_utf8(): {}", 'e'.len_utf8()); println!("'é'.len_utf8(): {}", 'é'.len_utf8()); println!("'🦀'.len_utf8(): {}", '🦀'.len_utf8()); println!("\"é\".len(): {}", "é".len());}size_of::<char>(): 4 size_of::<u8>(): 1 'e'.len_utf8(): 1 'é'.len_utf8(): 2 '🦀'.len_utf8(): 4 "é".len(): 2
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. The first one trains reading: it shows a program and asks what it makes.
Read this: what do the values come to?
Seven lines of casts, literals and shadowing. Say what each variable holds as text. as, a raw string and a second let n each change what the obvious reading gives.
// Read this program without running it. Then replace each `todo!()` below with the text// that `show` makes for that variable (what `to_string()` gives).
fn show(input: &str) -> Vec<String> { let n = input.trim().len(); let n = n as f64 / 4.0; let big = 1_000; let wrapped = big as u8; let byte = 0xff_u8; let signed = byte as i8; let raw = r"\n".len(); let cooked = "\n".len(); let half = "7".parse::<i32>().unwrap() / 2; let next = (b'a' + 1) as char; vec![ n.to_string(), wrapped.to_string(), signed.to_string(), raw.to_string(), cooked.to_string(), half.to_string(), next.to_string(), ]}
// What `show(" abcde ")` makes, in that order:/// Hint: how long is `input` once `trim()` drops the spaces, and what does the second `let n` make of it?fn n_is() -> &'static str { todo!( "how long is `input` once `trim()` drops the spaces, and what does the second `let n` make of it?" )}
/// Hint: `as u8` keeps only the low 8 bits, which is the value modulo 256fn wrapped_is() -> &'static str { todo!("`as u8` keeps only the low 8 bits, which is the value modulo 256")}
/// Hint: `0xff` has all eight bits set; read as an `i8` (the top bit is the sign), what number is that?fn signed_is() -> &'static str { todo!( "`0xff` has all eight bits set; read as an `i8` (the top bit is the sign), what number is that?" )}
/// Hint: a raw string processes no escapes, so count the charactersfn raw_is() -> &'static str { todo!("a raw string processes no escapes, so count the characters")}
/// Hint: in an ordinary string, how many characters is the escape for a newline?fn cooked_is() -> &'static str { todo!("in an ordinary string, how many characters is the escape for a newline?")}
/// Hint: `parse::<i32>()` makes an integer, and integer `/` drops the fraction (7 / 2 is not 3.5)fn half_is() -> &'static str { todo!( "`parse::<i32>()` makes an integer, and integer `/` drops the fraction (7 / 2 is not 3.5)" )}
/// Hint: `b'a'` is a number; `as char` turns the sum back into a characterfn next_is() -> &'static str { todo!("`b'a'` is a number; `as char` turns the sum back into a character")}
fn check(variable: &str, said: &str, truth: &str) { assert!(said == truth, "misread `{variable}`: you said {said:?}"); println!("ok: {variable} is {said:?}");}
fn main() { let made = show(" abcde "); check("n", n_is(), &made[0]); check("wrapped", wrapped_is(), &made[1]); check("signed", signed_is(), &made[2]); check("raw", raw_is(), &made[3]); check("cooked", cooked_is(), &made[4]); check("half", half_is(), &made[5]); check("next", next_is(), &made[6]); 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 without running it. Then replace each `todo!()` below with the text// that `show` makes for that variable (what `to_string()` gives).
fn show(input: &str) -> Vec<String> { let n = input.trim().len(); let n = n as f64 / 4.0; let big = 1_000; let wrapped = big as u8; let byte = 0xff_u8; let signed = byte as i8; let raw = r"\n".len(); let cooked = "\n".len(); let half = "7".parse::<i32>().unwrap() / 2; let next = (b'a' + 1) as char; vec![ n.to_string(), wrapped.to_string(), signed.to_string(), raw.to_string(), cooked.to_string(), half.to_string(), next.to_string(), ]}
// What `show(" abcde ")` makes, in that order:/// Hint: how long is `input` once `trim()` drops the spaces, and what does the second `let n` make of it?fn n_is() -> &'static str { "1.25"}
/// Hint: `as u8` keeps only the low 8 bits, which is the value modulo 256fn wrapped_is() -> &'static str { "232"}
/// Hint: `0xff` has all eight bits set; read as an `i8` (the top bit is the sign), what number is that?fn signed_is() -> &'static str { "-1"}
/// Hint: a raw string processes no escapes, so count the charactersfn raw_is() -> &'static str { "2"}
/// Hint: in an ordinary string, how many characters is the escape for a newline?fn cooked_is() -> &'static str { "1"}
/// Hint: `parse::<i32>()` makes an integer, and integer `/` drops the fraction (7 / 2 is not 3.5)fn half_is() -> &'static str { "3"}
/// Hint: `b'a'` is a number; `as char` turns the sum back into a characterfn next_is() -> &'static str { "b"}
fn check(variable: &str, said: &str, truth: &str) { assert!(said == truth, "misread `{variable}`: you said {said:?}"); println!("ok: {variable} is {said:?}");}
fn main() { let made = show(" abcde "); check("n", n_is(), &made[0]); check("wrapped", wrapped_is(), &made[1]); check("signed", signed_is(), &made[2]); check("raw", raw_is(), &made[3]); check("cooked", cooked_is(), &made[4]); check("half", half_is(), &made[5]); check("next", next_is(), &made[6]); println!("all checks passed");}ok: n is "1.25" ok: wrapped is "232" ok: signed is "-1" ok: raw is "2" ok: cooked is "1" ok: half is "3" ok: next is "b" all checks passed
Split a colour
A colour is written 0xRRGGBB. Write rgb, which returns the three bytes. The last check puts a stray extra byte on top, as 0xAARRGGBB does. Which cast throws it away?
/// Split a colour written as 0xRRGGBB into its (red, green, blue) bytes.fn rgb(colour: u32) -> (u8, u8, u8) { todo!( "shift the colour right to bring a byte to the bottom, then `as u8` keeps only the low 8 bits" )}
fn main() { assert_eq!(rgb(0xff8800), (255, 136, 0)); assert_eq!(rgb(0x000000), (0, 0, 0)); assert_eq!(rgb(0x123456), (0x12, 0x34, 0x56)); // The case to watch: a stray top byte (alpha) must not leak into the result. assert_eq!(rgb(0xff_12_34_56), (0x12, 0x34, 0x56)); 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
/// Split a colour written as 0xRRGGBB into its (red, green, blue) bytes.fn rgb(colour: u32) -> (u8, u8, u8) { // `as u8` throws away every bit above the lowest 8, so the stray top byte disappears. ((colour >> 16) as u8, (colour >> 8) as u8, colour as u8)}
fn main() { assert_eq!(rgb(0xff8800), (255, 136, 0)); assert_eq!(rgb(0x000000), (0, 0, 0)); assert_eq!(rgb(0x123456), (0x12, 0x34, 0x56)); // The case to watch: a stray top byte (alpha) must not leak into the result. assert_eq!(rgb(0xff_12_34_56), (0x12, 0x34, 0x56)); println!("all checks passed");}all checks passed
An integer and a float
Turn text like "3, 2.5" into Some((3, 2.5)), a whole number and a float, and anything else into None. parse::<i32>() gives Ok(n) or Err(_), so write let Ok(n) = ... else { return None };. Use trim() to drop spaces.
/// "3, 2.5" -> Some((3, 2.5)). Anything that is not "integer, number" -> None.fn int_float(text: &str) -> Option<(i32, f64)> { todo!( "split_once(',') gives Some((left, right)); parse::<i32>() gives Ok(n) or Err(_), so try `let Ok(n) = ... else` with `return None`; trim() drops spaces" )}
fn main() { assert_eq!(int_float("3,2.5"), Some((3, 2.5))); assert_eq!(int_float(" 7 , 0.5 "), Some((7, 0.5))); assert_eq!(int_float("3,4"), Some((3, 4.0))); assert_eq!(int_float("x,1.0"), None); assert_eq!(int_float("3"), None); assert_eq!(int_float("3,abc"), None); // The case to watch: the first part must be a whole number. assert_eq!(int_float("2.5,3"), 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
/// "3, 2.5" -> Some((3, 2.5)). Anything that is not "integer, number" -> None.fn int_float(text: &str) -> Option<(i32, f64)> { let Some((left, right)) = text.split_once(',') else { return None; }; let Ok(whole) = left.trim().parse::<i32>() else { return None; }; let Ok(fraction) = right.trim().parse::<f64>() else { return None; }; Some((whole, fraction))}
fn main() { assert_eq!(int_float("3,2.5"), Some((3, 2.5))); assert_eq!(int_float(" 7 , 0.5 "), Some((7, 0.5))); assert_eq!(int_float("3,4"), Some((3, 4.0))); assert_eq!(int_float("x,1.0"), None); assert_eq!(int_float("3"), None); assert_eq!(int_float("3,abc"), None); // The case to watch: the first part must be a whole number. assert_eq!(int_float("2.5,3"), None); println!("all checks passed");}all checks passed
Drop the ends
Return a slice without its first and last items, using a range. A length is a usize, which cannot go below zero, so items.len() - 1 panics on an empty slice (see the panic above). Check the length first, and return &[] when nothing is left.
/// Everything except the first and last item. A slice with fewer than 2 items has none left.fn without_ends(items: &[i32]) -> &[i32] { todo!("a range like `1..items.len() - 1` builds the slice; check the length first")}
fn main() { assert_eq!(without_ends(&[1, 2, 3, 4]), [2, 3]); assert_eq!(without_ends(&[1, 2, 3]), [2]); assert_eq!(without_ends(&[1, 2]), []); assert_eq!(without_ends(&[1]), []); // The case to watch: `0 - 1` on an unsigned length panics. assert_eq!(without_ends(&[]), []); 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
/// Everything except the first and last item. A slice with fewer than 2 items has none left.fn without_ends(items: &[i32]) -> &[i32] { if items.len() < 2 { return &[]; } &items[1..items.len() - 1]}
fn main() { assert_eq!(without_ends(&[1, 2, 3, 4]), [2, 3]); assert_eq!(without_ends(&[1, 2, 3]), [2]); assert_eq!(without_ends(&[1, 2]), []); assert_eq!(without_ends(&[1]), []); // The case to watch: `0 - 1` on an unsigned length panics. assert_eq!(without_ends(&[]), []); println!("all checks passed");}all checks passed
Read more
- The Rust Reference: literal expressions, tokens, type cast expressions, constant items, static items and patterns.
- The Rust Book: 3.1 Variables and Mutability and 3.2 Data Types.
- Rust API Guidelines: Naming
- Rust by Example: Literals and operators, Casting and Inference.
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.