Constants in Rust
const, static, and compile-time evaluation
const: Compile-Time Constants
Rust's const declares a value that is known at compile time. The compiler evaluates the expression and inlines the result at every use site. Constants have no fixed memory address — they are literally copied into the code wherever they appear. This makes them ideal for values that are used in array sizes, match patterns, and generic parameters.
const MAX_BUFFER_SIZE: usize = 64 * 1024; // 64 KiB
const DEFAULT_PORT: u16 = 8080;
const PI: f64 = std::f64::consts::PI;
const MAGIC_BYTES: [u8; 4] = [0x7F, b'E', b'L', b'F']; // ELF header
// const can be used in array sizes and generic contexts
let buffer = [0u8; MAX_BUFFER_SIZE];Unlike let bindings, const requires an explicit type annotation and must be initialized with a constant expression. The compiler evaluates constant expressions at compile time using Rust's const eval system, which supports a growing subset of Rust including arithmetic, control flow, and calls to const fn functions.
static: Constants with a Fixed Address
The static keyword declares a value with a fixed memory address that lives for the entire duration of the program. Unlike const, a static item exists in exactly one location in memory and can be referenced by pointer. Use static when you need a stable address or when the value is too large to inline at every use site.
static GLOBAL_CONFIG: &str = "production";
static VERSION: &str = env!("CARGO_PKG_VERSION");
// Mutable statics require unsafe and are rarely used
static mut COUNTER: u32 = 0;
// For thread-safe mutable statics, use std::sync types
use std::sync::LazyLock;
static SETTINGS: LazyLock<Settings> = LazyLock::new(|| {
Settings::from_env()
});Mutable statics (static mut) are almost always the wrong choice. They require unsafe for every access because the compiler cannot guarantee freedom from data races. Use std::sync::LazyLock (stabilized in Rust 1.80), std::sync::OnceLock, or atomic types for thread-safe global state.
const fn: Compile-Time Function Evaluation
Functions marked const fn can be called in constant contexts — their result can be assigned to a const or used as an array size. This enables complex compile-time computations while keeping code readable and DRY.
const fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
const FIB_20: u64 = fibonacci(20); // Computed at compile time: 6765
const fn kilobytes(kb: usize) -> usize {
kb * 1024
}
const fn megabytes(mb: usize) -> usize {
kilobytes(mb * 1024)
}
const MAX_UPLOAD: usize = megabytes(50); // 52_428_800The set of operations allowed in const fn has expanded significantly across Rust editions. As of recent stable Rust, const fn supports control flow, loops, pattern matching, references, and calls to other const functions. Features like heap allocation and trait objects remain unavailable in const contexts.
Enums as Constants
Rust enums are algebraic data types that can carry associated data, making them far more powerful than simple constant enumerations. For plain enumerations without data, they serve the same purpose as enum constants in other languages but with exhaustive pattern matching.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
}
impl HttpMethod {
const fn as_str(&self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
Self::Patch => "PATCH",
}
}
}
// Exhaustive matching — the compiler ensures every variant is handled
fn handle(method: HttpMethod) {
match method {
HttpMethod::Get => println!("Reading"),
HttpMethod::Post => println!("Creating"),
HttpMethod::Put => println!("Updating"),
HttpMethod::Delete => println!("Deleting"),
HttpMethod::Patch => println!("Patching"),
}
}Built-in Constants
Rust's standard library provides constants through dedicated consts submodules for each numeric type:
use std::f64::consts;
consts::PI // 3.141592653589793
consts::E // 2.718281828459045
consts::TAU // 6.283185307179586
consts::SQRT_2 // 1.4142135623730951
consts::LN_2 // 0.6931471805599453
consts::LN_10 // 2.302585092994046
// Integer limits
i32::MAX // 2_147_483_647
i32::MIN // -2_147_483_648
u64::MAX // 18_446_744_073_709_551_615
usize::BITS // 64 (on 64-bit platforms)
// Float limits
f64::INFINITY // Positive infinity
f64::NEG_INFINITY // Negative infinity
f64::NAN // Not a Number
f64::EPSILON // ~2.2e-16
f64::MAX // ~1.8e308Common Patterns
- Use
constfor values known at compile time that do not need a fixed address. Usestaticwhen you need a stable reference or for large values. - Name constants in
SCREAMING_SNAKE_CASE. Associated constants on types use the same convention. - Prefer
const fnover macros for compile-time computation — it is type-safe and easier to debug. - Use
#[non_exhaustive]on public enums that may gain variants in the future to prevent downstream code from relying on exhaustive matching. - Associated constants on traits enable generic code to define per-type constant values:
trait Limit { const MAX: usize; }.