if let
temporary scope
Summary
- In an
if let $pat = $expr { .. } else { .. }
expression, the temporary values generated from evaluating$expr
will be dropped before the program enters theelse
branch instead of after.
Details
The 2024 Edition changes the drop scope of temporary values in the scrutinee1 of an if let
expression. This is intended to help reduce the potentially unexpected behavior involved with the temporary living for too long.
Before 2024, the temporaries could be extended beyond the if let
expression itself. For example:
#![allow(unused)] fn main() { // Before 2024 use std::sync::RwLock; fn f(value: &RwLock<Option<bool>>) { if let Some(x) = *value.read().unwrap() { println!("value is {x}"); } else { let mut v = value.write().unwrap(); if v.is_none() { *v = Some(true); } } // <--- Read lock is dropped here in 2021 } }
In this example, the temporary read lock generated by the call to value.read()
will not be dropped until after the if let
expression (that is, after the else
block). In the case where the else
block is executed, this causes a deadlock when it attempts to acquire a write lock.
The 2024 Edition shortens the lifetime of the temporaries to the point where the then-block is completely evaluated or the program control enters the else
block.
#![allow(unused)] fn main() { // Starting with 2024 use std::sync::RwLock; fn f(value: &RwLock<Option<bool>>) { if let Some(x) = *value.read().unwrap() { println!("value is {x}"); } // <--- Read lock is dropped here in 2024 else { let mut s = value.write().unwrap(); if s.is_none() { *s = Some(true); } } } }
See the temporary scope rules for more information about how temporary scopes are extended. See the tail expression temporary scope chapter for a similar change made to tail expressions.
The scrutinee is the expression being matched on in the if let
expression.
Migration
It is always safe to rewrite if let
with a match
. The temporaries of the match
scrutinee are extended past the end of the match
expression (typically to the end of the statement), which is the same as the 2021 behavior of if let
.
The if_let_rescope
lint suggests a fix when a lifetime issue arises due to this change or the lint detects that a temporary value with a custom, non-trivial Drop
destructor is generated from the scrutinee of the if let
. For instance, the earlier example may be rewritten into the following when the suggestion from cargo fix
is accepted:
#![allow(unused)] fn main() { use std::sync::RwLock; fn f(value: &RwLock<Option<bool>>) { match *value.read().unwrap() { Some(x) => { println!("value is {x}"); } _ => { let mut s = value.write().unwrap(); if s.is_none() { *s = Some(true); } } } // <--- Read lock is dropped here in both 2021 and 2024 } }
In this particular example, that's probably not what you want due to the aforementioned deadlock! However, some scenarios may be assuming that the temporaries are held past the else
clause, in which case you may want to retain the old behavior.
The if_let_rescope
lint is part of the rust-2024-compatibility
lint group which is included in the automatic edition migration. In order to migrate your code to be Rust 2024 Edition compatible, run:
cargo fix --edition
After the migration, it is recommended that you review all of the changes of if let
to match
and decide what is the behavior that you need with respect to when temporaries are dropped. If you determine that the change is unnecessary, then you can revert the change back to if let
.
If you want to manually inspect these warnings without performing the edition migration, you can enable the lint with:
#![allow(unused)] fn main() { // Add this to the root of your crate to do a manual migration. #![warn(if_let_rescope)] }