8 February 2023
Rust has built-in integer types in various sizes: from 8-bit to 128-bit, unsigned and signed, with two's complement semantics:
type description (approximate) range
i8 8-bit signed -128 to 127
u8 8-bit unsigned zero to 255
i16 16-bit signed -32768 to 32767
u16 16-bit unsigned zero to 65535
i32 32-bit signed -2 billion to 2 billion
u32 32-bit unsigned zero to 4 billion
i64 64-bit signed numbers with 19 decimal digits
u64 64-bit unsigned non-negative numbers with 20 digits
i128 128-bit signed numbers with 37 decimal digits
u128 128-bit unsigned non-negative numbers with 38 digits
(Not sure if that’s the best way to describe the ranges -- the numbers get pretty big!)
There’s two more integers whose size depends on the processor architecture that the program is compiled for. They’re the same size as a pointer:
type description (approximate) range
isize pointer-size signed usually same as i64
usize pointer-size unsigned usually same as u64
Any modern laptop, server, or smartphone will have a 64-bit processor, so those are usually 64-bit integers. The exception is when you’re writing embedded software -- maybe you’ve hacked your smart thermostat and you’re re-programming it in Rust -- then you may have a 32-bit or even 16-bit processor.
The usize
type is used for things like indexing an array or vector. I’m not sure if
isize
is used much.
When you define a variable with one of the integer types, you can set the variable’s type:
let signed8: i8 = -33;
let unsigned64: u64 = 333333333333333333;
let pointer_size: usize = 123;
Or add a suffix to set the literal’s type:
let signed8 = -33i8;
let unsigned64 = 333333333333333333u64;
let pointer_size = 123usize;
You can add underscores to make big numbers easier to read, and define numbers in octal,
hexadecimal, or binary notation with the 0x
, 0o
, and 0b
prefixes:
let two_billion = 2_000_000_000; // i32, underscores are ignored
let hex: u8 = 0xff; // 255 in hexadecimal
let oct: u8 = 0o77; // 63 in octal
let bin: u8 = 0b11; // 3 in binary
If you don’t set a type, the default is i32
, even with the hex/octal/binary notation.
By the way, the old C syntax for octal numbers, prefixing the number with just the digit zero, is
not used in Rust.
There’s one more format for integer literals: a byte literal defines a u8 with the ASCII value of a character:
let byte = b'A'; // u8 with value 65 (A is character 65 in ASCII)
Rust doesn’t convert from one integer type to another unless you tell it to. For example, this is not valid:
let unsigned16: u16 = 123u8; // not allowed, types don't match
The as
keywork is used for “type casting”, telling the compiler to change the type of
an integer:
let x = 123u8 as u16; // ok, x will be 123u16
For the simple cases, where the new type can represent the value, this does what you’d expect. If the new type is smaller, leading bits will just be discarded. When casting between between signed and unsigned, the bits of the integer will stay the same, only the type changes:
let a = 0xffffu16 as u8; // ok, a will be 0xffu8
let b = 0xffu8 as i8; // ok, b will be -1i8
let c = -1i8 as u8; // ok, c will be 0xffu8
If that last part didn’t make sense to you, just remember that casting from a larger type to a smaller one or casting between signed and unsigned is dangerous because you’ll get strange results if a number doesn’t “fit into” the new type.
To convert a string to a number, you call the parse
method (on &str
or
String
), which returns a Result
:
let five: u32 = "5".parse().unwrap();
let minus_one: i64 = String::from("-1").parse().unwrap();
let oops = "hello".parse::<i32>(); // Err
let also_oops = "-1".parse::<u32>(); // Err
let still_oops = "256".parse::<u8>(); // Err
This is one of those methods whose return type depends on a type parameter, so you have to either make sure the compiler can figure out the type with type inference or set it with the turbofish operator. As you can see in the last two, it’ll correctly handle the case where the string is a valid number but can’t be represented with the given type.
To go the other way around and turn an integer into a string, use the format!
macro. It
uses the same mini-language as the print!
and println!
ones:
let n = 42;
let decimal = format!("{}", n); // "42"
let hex = format!("{:x}", n); // "2a"
let oct = format!("{:o}", n); // "52"
let bin = format!("{:b}", n); // "101010"
let padded = format!("{:4}", n); // " 42"
See std::fmt for the details.
What should happen when the result of a calculation is outside the range of its type? For example, 255 is the largest value that a u8 can represent, so what should this print?
let x = 255u8;
let y = 1u8;
println!("{}", x + y);
Different languages answer that question in different ways. In Python, integers have unlimited size, so this never becomes a problem; in C and C++ integer overflow is undefined behavior; in Java and Go the numbers defined to “wrap around” according to two’s-complement arithmetic. Rust uses what seems to me an awkward compromise: integer overflow wraps around like in Java and Go, but also it’s an error.
There’s three different cases:
If you want more precise control over what happens in case of overflow, the integer types each have
a set of methods that lets you select the behavior: wrapping so numbers wrap around according
to two’s-complement arithmetic, checked to get an Option
with None
in case of overflow, overflowing to get a tuple with the result and a boolean that indicates
whether overflow occurred, and saturating for calculations that produce the highest value
possible instead of overflowing. Here’s all of them in action:
let x: u8 = 255;
let y: u8 = 1;
println!("{}", x.wrapping_add(y)); // prints 0
println!("{:?}", x.checked_add(y)); // prints None
println!("{:?}", x.overflowing_add(y)); // prints (0, true)
println!("{}", x.saturating_add(y)); // prints 255
All the examples so far were about going higher than the maximum value of a type; going lower than the minimum works the same way. For example, this prints -128, which is the lowest value for i8:
let x: i8 = -100;
let y: i8 = 50;
println!("{}", x.saturating_sub(y));