I do understand the technical facts, I think. But what I really don’t understand (and I haven’t found an explanation for my beginners mind yet):
why would I use a const if a let does about the same?
what problem is solved by shadowing?
I do understand the technical facts, I think. But what I really don’t understand (and I haven’t found an explanation for my beginners mind yet):
why would I use a const if a let does about the same?
what problem is solved by shadowing?
It's particularly important for things like
if let Some(foo) = foo {
or
let x: i32 = x.parse()?;
where it's changing the type, but not really changing the intent.
Without shadowing, you end up with unfortunate hungarian things like
if let Some(foo_really) = foo {
or
let x: i32 = x_text.parse()?;
It's also really handy in #[test]
s.
const
binds a name for a compile time constant, which means you can use it where only compile time constants are expected. This includes for example initializers of static
variables, arrays lengths, non-copy initializers of by-length array literals, const generics and more.
let
instead binds a name for any runtime value, so you can't do any of the above with it.
Please explain if you will, why then would one ever want to use let, if what you really want is a const or a let mut?
So is it more a thing of beauty than a thing of functionality?
And I do understand beauty is functional in a language!
Why would it be the case that all you ever want is a const
or a let mut
? This is a false dichotomy. Sometimes you need a run-time value which you don't want to change subsequently. It can't be a const
(because it's not a value known at compile-time), but it can't be a let mut
either (because you don't want to allow changing it).
There are operations that cannot be done at compile time that you none the less need to store in a variable, even when you don't need the capabilites a mut binding confers.
That said, non-mut bindings are effectively a lint (albeit one some people feel quite strongly about).
I see, thanks, the penny dropped!
I’d suggest, you try actually using these things in some programs and let the compiler tell you what const
and let
can or cannot do.
The meaning of let mut
is probably unsurprising…
fn my_function(n: u8) {
let mut x = n + 10;
x += 1;
}
but if we don’t mutate it anymore
fn my_function(n: u8) {
let mut x = n + 10;
}
warning: variable does not need to be mutable
--> src/lib.rs:2:9
|
2 | let mut x = n + 10;
| ----^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
The compiler suggests to remove the “mut
” and use let x = …
, which will work fine. But let’s writing “const
” instead, if we don’t really know what let
without mut
is there for.
fn my_function(n: u8) {
const x = n + 10;
}
error[E0435]: attempt to use a non-constant value in a constant
--> src/lib.rs:2:15
|
2 | const x = n + 10;
| ------- ^ non-constant value
| |
| help: consider using `let` instead of `const`: `let x`
error: missing type for `const` item
--> src/lib.rs:2:12
|
2 | const x = n + 10;
| ^ help: provide a type for the item: `: <type>`
Well, this didn’t work at all! I mean, let’s look at the errors individually. For once, it suddenly wants a type annotation
fn my_function(n: u8) {
const x: u8 = n + 10;
}
but even then no luck
error[E0435]: attempt to use a non-constant value in a constant
--> src/lib.rs:2:19
|
2 | const x: u8 = n + 10;
| ------- ^ non-constant value
| |
| help: consider using `let` instead of `const`: `let x`
The compiler even kindly suggests moving back to a let
.
When does const
work? The const
’s value must be independent from anything that happens at run-time, including function inputs, or evaluating functions that compile-time-computation features of Rust do not (or not yet) support, like allocations, randomness, etc…
So this works
fn my_function(n: u8) {
const x: u8 = 42 + 10;
}
but it gives another warning
warning: constant `x` should have an upper case name
--> src/lib.rs:2:11
|
2 | const x: u8 = 10 + 10;
| ^ help: convert the identifier to upper case (notice the capitalization): `X`
|
= note: `#[warn(non_upper_case_globals)]` on by default
So apparently const
s are stylistically very different from let
; they ought to have UPPER_CASE
names, and are required to have a type signature. What else? The compiler doesn’t hint at this in this case, but const
s declarations are items not statements, which is a fancy way of saying, you can write them in a lot more places than just function bodies (or blocks), in particular at the top level of the file
const X: u8 = 10 + 10;
fn my_function(n: u8) {}
Can’t do that with let
:
let X: u8 = 10 + 10;
fn my_function(n: u8) {}
error: expected item, found keyword `let`
--> src/lib.rs:1:1
|
1 | let X: u8 = 10 + 10;
| ^^^ consider using `const` or `static` instead of `let` for global variables
Because they are items, just like it’s the case for functions, Rust took the decision to make type signatures on const
s mandatory. And the UPPER_CASE naming scheme is important to not accidentally confuse them with local variables, which is especially important since you can even pattern-match against the value of a constant[1].
Now, looking at the last compiler suggestion above… what is static
?! I’d say that’s where the information the compiler can teach us straightforwardly ends, as the difference between const
and static
is a bit more subtle. I do go into it in the bottom part of this answer (including code examples with some compilation errors, and behavioral differences), but I’d say it’d be hard to understand the differences here by experimentation, without any guidance.
One more thing that const
allows us, and one of the most relevant things actually, is that it can be used in places where constants are required. A typical example are array lengths.
fn my_function(n: u8) {
let x: [i32; 42] = todo!(); // works fine
let y: [i32; 40 + 2] = todo!(); // works fine
let y: [i32; n] = todo!(); // doesn't work
}
error[E0435]: attempt to use a non-constant value in a constant
--> src/lib.rs:4:18
|
1 | fn my_function(n: u8) {
| - this would need to be a `const`
...
4 | let y: [i32; n] = todo!(); // doesn't work
| ^
That using the input parameter as an array length won't work is a fundamental limitation that cannot be worked around using arrays on the stack. (One would want to use Vec<i32>
or perhaps Box<[i32]>
in this case.)
But what if we want to give the length a name, e.g. to not repeat ourselves when it’s used a lot?
fn my_function() {
let n = 42;
let x: [i32; n] = todo!(); // length n
let y: [i32; n] = todo!(); // same length, DRY
}
error[E0435]: attempt to use a non-constant value in a constant
--> src/lib.rs:3:18
|
2 | let n = 42;
| ----- help: consider using `const` instead of `let`: `const n`
3 | let x: [i32; n] = todo!(); // length n
| ^ non-constant value
error[E0435]: attempt to use a non-constant value in a constant
--> src/lib.rs:4:18
|
2 | let n = 42;
| ----- help: consider using `const` instead of `let`: `const n`
3 | let x: [i32; n] = todo!(); // length n
4 | let y: [i32; n] = todo!(); // same length, DRY
| ^ non-constant value
The compiler suggestions are – once again – perfectly helpful. In this case, a let
won’t work, and a const
is required!
fn my_function() {
const N: usize = 42;
let x: [i32; N] = todo!(); // length N
let y: [i32; N] = todo!(); // same length, DRY
}
To also get into that topic really quickly, let’s do a very short comparison with static
, too.
static
are items like const
, in UPPER_CASE, with a type signature and you can write them on top level. So far so similar
const N: usize = 42;
static M: usize = 100;
fn main() { println!("{}", N + M) }
If you use both, you might notice that static
s can be defined using const
s
const N: usize = 42;
static M: usize = 100;
static SUM: usize = N + M;
fn main() { println!("{}", SUM) }
but not vice-versa
const N: usize = 42;
static M: usize = 100;
const SUM: usize = N + M;
fn main() { println!("{}", SUM) }
error[E0013]: constants cannot refer to statics
--> src/main.rs:4:24
|
4 | const SUM: usize = N + M;
| ^
|
= help: consider extracting the value of the `static` to a `const`, and referring to that
Also, static
s cannot be used in places like array lengths where const
s are required:
const N: usize = 3;
static M: usize = 3;
fn main() {
let array: [i32; N] = [1, 2, 3]; // fine
let array: [i32; M] = [1, 2, 3]; // not okay
}
error[E0013]: constants cannot refer to statics
--> src/main.rs:6:22
|
6 | let array: [i32; M] = [1, 2, 3]; // not okay
| ^
|
= help: consider extracting the value of the `static` to a `const`, and referring to that
So the first conclusion is that const
s are more useful, if you really have a constant value, because you can then use them for other const
s and for array lengths and the like, so prefer using const
if possible. Now, when are const
s not usable? That’s when you truly need a physically present global variable, in particular when the value in the variable must be mutable, using things like a Mutex in std::sync - Rust, or OnceLock in std::sync - Rust, or atomics, etc…
(another use case for static
instead of const
can - occasionally - be that static
s are guaranteed to exist as one global value in one singular place, so they can sometimes be more reliably efficient when using a const
that contains a huge (in size) constant like a super large array, would create too much overhead in case this data was ever duplicated into different places)
use std::sync::Mutex;
static GLOBAL: Mutex<i32> = Mutex::new(0);
fn incr() {
*GLOBAL.lock().unwrap() += 1;
}
fn read() -> i32 {
*GLOBAL.lock().unwrap()
}
fn main() {
incr();
incr();
incr();
println!("{}", read()); // -> 3
incr();
println!("{}", read()); // -> 4
}
This won’t work with a const
, as those are, unsurprisingly, not usable for non-constant values Let’s try nonetheless
use std::sync::Mutex;
const GLOBAL: Mutex<i32> = Mutex::new(0);
fn incr() {
*GLOBAL.lock().unwrap() += 1;
}
fn read() -> i32 {
*GLOBAL.lock().unwrap()
}
fn main() {
incr();
incr();
incr();
println!("{}", read());
incr();
println!("{}", read());
}
compiles fine… outputs
0
0
No warnings – yeah, the fact that this still works is a bit unfortunate, arguably we should get a warning here, too. At least
clippy
helps though:
warning: a `const` item should never be interior mutable
--> src/main.rs:3:1
|
3 | const GLOBAL: Mutex<i32> = Mutex::new(0);
| -----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| make this a static item (maybe with lazy_static)
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#declare_interior_mutable_const
= note: `#[warn(clippy::declare_interior_mutable_const)]` on by default
warning: a `const` item with interior mutability should not be borrowed
--> src/main.rs:5:6
|
5 | *GLOBAL.lock().unwrap() += 1;
| ^^^^^^
|
= help: assign this const to a local or static variable, and use the variable here
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#borrow_interior_mutable_const
= note: `#[warn(clippy::borrow_interior_mutable_const)]` on by default
warning: a `const` item with interior mutability should not be borrowed
--> src/main.rs:8:6
|
8 | *GLOBAL.lock().unwrap()
| ^^^^^^
|
= help: assign this const to a local or static variable, and use the variable here
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#borrow_interior_mutable_const
Why this code still works is because mentioning const
s treats them like values, not like places (e.g. like a variable). So writing &MY_CONSTANT
will not create a reference to some global MY_CONSTANT
variable – as it would if MY_CONSTANT
was a static
instead – but instead it initializes a temporary variable with the contents copied from MY_CONSTANT
and creates a reference to that variable. Like writing &42
works, or in the above example if *GLOBAL.lock()
was re-written into *Mutex::new(0).lock()
on both occurrences – which also explains the behavior of always observing the 0
in the read()
function.
For example:
const N: u8 = 42;
fn test(x: u8) {
match x {
N => println!("it’s 42!"),
n => println!("It’s not 42, but {n}!"),
}
}
↩︎
This is great, thanks a lot!
I recently used const
and static
like this:
#[cfg_attr(unix, path = "gui_gtk.rs")]
#[cfg_attr(windows, path = "gui_win.rs")]
mod gui_impl;
use gui_impl::INTERFACE_CONST;
/// [`Lazy`] static holding the [`Interface`] to the GUI thread
///
/// Initialization is performed platform dependently, see
/// [`gui_impl::INTERFACE_CONST`].
static INTERFACE: Lazy<Interface> = INTERFACE_CONST;
And then in the submodule:
/// Implementation for [`super::INTERFACE`]
#[allow(clippy::declare_interior_mutable_const)]
pub(super) const INTERFACE_CONST: Lazy<Interface> = Lazy::new(|| {
/* … */
});
Note how I had to shut up clippy
(see also LazyLock in Nightly)
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.