In no more than 5 minutes we'll show you the difference between an empty binding and a named silenced binding and when to use them.
In Rust, aside from variables and struct fields, you can also bind memory to patterns. Some of these patterns are used by the compiler to signal certain conditions, like when a variable will never be used or when it may be used. This is the point of this article: how does the Rust compiler know you'll use a variable?
It all boils down to the _
and _variable_name
patterns, which we call empty binding and named silenced bindings respectively. With these two patterns, Rust can know if you intend on using a variable even though you haven't used it yet (in your current code structure) or you don't have any expected usage of that variable.
The Two Underscore Patterns: _
vs _name
Although these two patterns seem alike, as their aliases imply, they're not the same. Let's look at some of the purposes and differences between both.
_: Empty Binding
This type of binding signals to the compiler that this value won't be used further in your code, which means it can be ignored or discarded.
This binding can be used anywhere a variable can be used: structure destructuring, tuple bindings, scoped bindings, function return bindings, and more.
Here's an example to help you understand it better:
rust
fn main() {
let _ = "Ignored very big string";
// println!("Very big string: {}", _);
// The above code won't run as _ cannot be printed since it binds to nothing
}
Since the _
wildcard pattern doesn't create a binding, it can be used multiple times in the same pattern, including in tuple destructuring or as a catch-all for the rest of the values in a slice.
rust
fn main() {
let tuple = (1, 2, 3, 4);
// You can use `_` multiple times — each one discards the value at that position
let (_, second, _, fourth) = tuple;
println!("second: {}, fourth: {}", second, fourth);
let array = [10, 20, 30, 40, 50];
// Match the first two elements, ignore the rest with `..` and use `_` for positions you don't care about
// Notice how we can bind two values with _ at the same time
// Try changing this to any other variable name and running the code
match array {
[first, _, _, ..] => {
println!("first: {}", first);
}
}
// Use `_` in multiple match arms — always matches, doesn't bind
let value = Some(42);
match value {
Some(_) => println!("Got a value (but not binding it)"),
None => println!("No value"),
}
}
_named_silenced Binding
In this binding, the value is stored just like a normal variable and can be assigned and used like a normal variable. It only signals to Rust that this variable may not be used, which is important for linting and some slight performance optimizations.
Since this is a valid variable and not a keyword pattern like _
, you can use it just like any variable.
Look at this code sample, showing how it can be used just like a normal variable:
rust
// Notice how it can also be used in function signatures, signaling that the variable may not be used
fn print_value(_val: i32) {
// Even though, as you can see here, it is being used
println!("Value: {}", _val);
}
fn main() {
// `_used_var` is a regular binding — you can use it like any other variable
let mut _used_var = 10;
// You can perform operations on it
_used_var += 5;
// You can pass it to functions
print_value(_used_var);
// You can reassign it
_used_var = 42;
// Compiler is totally fine with this — it's a normal variable
println!("Final _used_var: {}", _used_var);
// `_not_used` is bound but never used — no compiler warning
// Because of the leading underscore, Rust assumes this is intentional
let _not_used = expensive_computation();
// `normal_unused` would trigger a warning, because it's unused and has no underscore
// let normal_unused = 123; // <-- Uncommenting this will cause a warning!
}
fn expensive_computation() -> i32 {
println!("Running expensive computation...");
99
}
In context: the _
binding will be completely ignored while the _named_variable
binding won't be ignored, but signals to the compiler that this variable may not be used.
When—and When Not—to Use These Patterns
Underscore tricks are powerful but should be applied thoughtfully. Here's when to use each pattern:
Use _
When You Truly Don't Need the Value
The empty binding works perfectly for placeholder loops, partial destructures, and situations where you need to consume a value but don't care about its contents:
rust
fn main() {
// Placeholder in loops
for _ in 0..5 {
println!("Hello!");
}
// Partial destructuring
let (important_value, _) = get_coordinates();
println!("X coordinate: {}", important_value);
// Consuming Result when you only care about success/failure
let _ = risky_operation().map_err(|e| eprintln!("Error: {}", e));
}
fn get_coordinates() -> (i32, i32) {
(10, 20)
}
fn risky_operation() -> Result<String, &'static str> {
Ok("Success".to_string())
}
Use `_name` for Self-Documenting Code
Named silenced bindings work best when you want descriptive variable names but don't plan to reference them immediately:
rust
fn process_user_data(user: User) {
let _user_id = user.id; // May be used for logging later
let _creation_timestamp = user.created_at; // Reserved for audit features
// Current logic only processes the name
println!("Processing user: {}", user.name);
}
struct User {
id: u32,
name: String,
created_at: String,
}
Avoid Silencing Variables You May Actually Use Later
Don't use underscore prefixes to silence warnings for variables you genuinely plan to use. Silent discards can mask real bugs and make code maintenance harder:
rust
// Don't do this
fn bad_example() {
let _important_data = fetch_critical_info();
// ... lots of code later ...
// Oops! Forgot to use _important_data, and no warning to remind us
}
// Do this instead
fn good_example() {
let important_data = fetch_critical_info();
// Compiler will warn if we forget to use it
process_data(important_data);
}
fn fetch_critical_info() -> String {
"Critical data".to_string()
}
fn process_data(_data: String) {
// Processing logic here
}
Remember: _
Still Evaluates Expressions
One important detail: plain _
still evaluates the expression, so side effects happen. You just can't refer back to the value:
rust
fn main() {
let _ = expensive_computation(); // This still runs!
// But you can't access the result
}
fn expensive_computation() -> i32 {
println!("Computing..."); // This will print
42
}
Summary
Understanding Rust's underscore patterns helps you write cleaner, more intentional code while working effectively with the compiler's warning system.
The key distinctions are:
_ = discard entirely, no warning, no binding created
_name = keep a named binding, but silence "unused" warnings
Use _
when you truly don't need a value, and use _name
when you want descriptive, self-documenting variable names that may not be immediately used.
For more advanced warning control and linting options, explore Clippy's lint settings. The Rust Book's pattern-matching chapter provides deeper dives into these concepts and other powerful pattern-matching techniques in Rust.