Skip to content
Learn Rust
Theme

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

Rustsrc/main.rsRun ↗
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}");
}
$ cargo run
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)
}
$ go run main.go
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")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
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 = 0
visits += 1
mask, ratio = 0xFF, 2.5
letter = "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}")
$ python3 main.py
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}`);
$ tsc --strict --module nodenext --types node main.ts && node main.js
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);
}
}
$ javac Main.java && java Main
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}");
$ dotnet run
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;
}
$ gcc -Wall -Wextra main.c -o main && ./main
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";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
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 let the type is optional (Rust infers it). On const, static, and function parameters it is required.
  • A number can carry its type as a suffix: 1u8, 2.5f32. _ and the 0x, 0o, 0b prefixes change how it is written, not its type.
  • 'R' is one char, "R" is a string. 'ab' is an error.
  • x as f64 converts between number types. parse::<i32>() names the type for a generic call, and the :: before < is required there.
Gotcha

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.
Rustsrc/main.rs✗ won't compile
fn main() {
i32 count = 5;
println!("count={count}");
}
Go✗ refuses too

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 too

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 = 5
print(f"count={count}")
$ python3 main.py✗ refuses too
  File "main.py", line 1
    int count = 5
        ^^^^^
SyntaxError: invalid syntax
TypeScript✗ refuses too

TypeScript 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);
}
}
$ javac Main.java && java Main✓ runs
count=5
int count = 5;
Console.WriteLine($"count={count}");
$ dotnet run✓ runs
count=5
#include <stdio.h>
int main(void) {
int count = 5;
printf("count=%d\n", count);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
count=5
#include <iostream>
int main() {
int count = 5;
std::cout << "count=" << count << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
count=5
What does the compiler say? Guess first, then open.
$ cargo build
error: expected one of `!`, `.`, `::`, `;`, `?`, `{`, `}`, or an operator, found `count`
 --> src/main.rs:2:9
  |
2 |     i32 count = 5;
  |         ^^^^^ expected one of 8 possible tokens
What happens in Go

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.

What happens in Kotlin

Kotlin refuses. Declarations start with val or var, and the type goes after the name: val count: Int = 5.

What happens in Python

Python raises a SyntaxError. Python has no type-first declaration, only count: int = 5 as an optional annotation.

What happens in TypeScript

TypeScript refuses. A declaration needs let or const, and the type goes after the name: let count: number = 5.

What happens in Java

Java prints count=5. The type before the name is the usual way to declare a local (var also exists).

What happens in C#

C# prints count=5. int count = 5; is a normal local declaration.

What happens in C

C prints count=5. The type goes first.

What happens in C++

C++ prints count=5. The type goes first.

What Rust sees

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.

Why Rust says no

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.

Rust, fixedsrc/main.rsRun ↗
fn main() {
let count: i32 = 5; // `let`, then the name, then `: type`
println!("count={count}");
}
$ cargo run
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
Rustsrc/main.rsRun ↗
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);
}
$ cargo run
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

Rustsrc/main.rsRun ↗
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}");
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
const LIMIT = 100;
static NAME = "Ada";
fn main() {
println!("{LIMIT} {NAME}");
}
$ cargo build
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:

Rustsrc/main.rs✗ won't compile
let LIMIT: u32 = 100;
fn main() {
println!("{LIMIT}");
}
$ cargo build
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.

Rustsrc/main.rs✗ won't compile
fn main() {
let count: i32 = "5";
let ratio: f64 = 1;
let small: i32 = 7;
let wide: i64 = small;
println!("{count} {ratio} {wide}");
}
$ cargo buildWhat is E0308?
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:

Rustsrc/main.rs! panics at runtimeRun ↗
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(&[]));
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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(&()));
}
$ cargo run
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.
Rustsrc/main.rsRun ↗
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 = "first
second"; // 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}");
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let word = 'hello';
println!("{word}");
}
$ cargo build
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:

Rustsrc/main.rsRun ↗
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));
}
$ cargo run
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.

Rustsrc/main.rs✗ won't compile
fn main() {
println!("300 as u8: {}", 300 as u8);
}
$ cargo build
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let code = 'a' as u32; // fine: a char becomes an integer
let wrong = 'a' as f64; // not allowed
println!("{code} {wrong}");
}
$ cargo buildWhat is E0606?
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):

Rustsrc/main.rsRun ↗
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:?}");
}
$ cargo run
a: 42, b: 42
words: ["a", "b", "c"], empty: []

Leave the type out and Rust has nothing to go on:

Rustsrc/main.rs✗ won't compile
fn main() {
let n = "42".parse().unwrap();
println!("{n}");
}
$ cargo buildWhat is E0284?
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.

Rustsrc/main.rsRun ↗
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}");
}
$ cargo run
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):

Rustsrc/main.rs✗ won't compile
fn main() {
let maybe = Some(3);
let Some(n) = maybe;
println!("{n}");
}
$ cargo buildWhat is E0005?
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!() };
  |                         ++++++++++++++++
Rustsrc/main.rsRun ↗
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}");
}
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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}");
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
#![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);
}
$ cargo build
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):

Rustsrc/main.rs✗ won't compile
struct Event {
type: String,
}
fn main() {
let e = Event {
type: String::from("click"),
};
println!("{}", e.type);
}
$ cargo build
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):

Rustsrc/main.rsRun ↗
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}");
}
$ cargo run
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let v = Vec::new();
println!("{}", v.len());
}
$ cargo buildWhat is E0282?
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:

Rustsrc/main.rsRun ↗
fn main() {
let label; // declared, no value yet
let n = 3;
if n > 2 {
label = "big";
} else {
label = "small";
}
println!("label: {label}");
}
$ cargo run
label: big
Rustsrc/main.rs✗ won't compile
fn main() {
let count: i32;
println!("{count}");
}
$ cargo buildWhat is E0381?
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:

Rustsrc/main.rs✗ won't compile
fn main() {
let n = 3;
let grid = [0; n];
println!("{}", grid.len());
}
$ cargo buildWhat is E0435?
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.

Rustsrc/main.rsRun ↗
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));
}
$ cargo run
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:

Rustsrc/main.rsRun ↗
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());
}
$ cargo run
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.

Starterexercises/read-literals.rs
// 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 256
fn 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 characters
fn 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 character
fn 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
One solutionexercises/read-literals.solution.rsRun ↗
// 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 256
fn 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 characters
fn 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 character
fn 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");
}
$ cargo run
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?

Starterexercises/rgb.rs
/// 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
One solutionexercises/rgb.solution.rsRun ↗
/// 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");
}
$ cargo run
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.

Starterexercises/int-float.rs
/// "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
One solutionexercises/int-float.solution.rsRun ↗
/// "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");
}
$ cargo run
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.

Starterexercises/without-ends.rs
/// 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
One solutionexercises/without-ends.solution.rsRun ↗
/// 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");
}
$ cargo run
all checks passed

Read more