|
| 1 | +# `if let` temporary scope |
| 2 | + |
| 3 | +🚧 The 2024 Edition has not yet been released and hence this section is still "under construction". |
| 4 | +More information may be found in the tracking issue at <https://github.com/rust-lang/rust/issues/124085>. |
| 5 | + |
| 6 | +## Summary |
| 7 | + |
| 8 | +- In an `if let $pat = $expr { .. } else { .. }` expression, the temporary values generated from evaluating `$expr` will be dropped before the program enters the `else` branch instead of after. |
| 9 | + |
| 10 | +## Details |
| 11 | + |
| 12 | +The 2024 Edition changes the drop scope of [temporary values] in the scrutinee[^scrutinee] of an `if let` expression. This is intended to help reduce the potentially unexpected behavior involved with the temporary living for too long. |
| 13 | + |
| 14 | +Before 2024, the temporaries could be extended beyond the `if let` expression itself. For example: |
| 15 | + |
| 16 | +```rust,edition2021 |
| 17 | +// Before 2024 |
| 18 | +# use std::sync::RwLock; |
| 19 | +
|
| 20 | +fn f(value: &RwLock<Option<bool>>) { |
| 21 | + if let Some(x) = *value.read().unwrap() { |
| 22 | + println!("value is {x}"); |
| 23 | + } else { |
| 24 | + let mut v = value.write().unwrap(); |
| 25 | + if v.is_none() { |
| 26 | + *v = Some(true); |
| 27 | + } |
| 28 | + } |
| 29 | + // <--- Read lock is dropped here in 2021 |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +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. |
| 34 | + |
| 35 | +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. |
| 36 | + |
| 37 | +<!-- TODO: edition2024 --> |
| 38 | +```rust |
| 39 | +// Starting with 2024 |
| 40 | +# use std::sync::RwLock; |
| 41 | + |
| 42 | +fn f(value: &RwLock<Option<bool>>) { |
| 43 | + if let Some(x) = *value.read().unwrap() { |
| 44 | + println!("value is {x}"); |
| 45 | + } |
| 46 | + // <--- Read lock is dropped here in 2024 |
| 47 | + else { |
| 48 | + let mut s = value.write().unwrap(); |
| 49 | + if s.is_none() { |
| 50 | + *s = Some(true); |
| 51 | + } |
| 52 | + } |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +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. |
| 57 | + |
| 58 | +[^scrutinee]: The [scrutinee] is the expression being matched on in the `if let` expression. |
| 59 | + |
| 60 | +[scrutinee]: ../../reference/glossary.html#scrutinee |
| 61 | +[temporary values]: ../../reference/expressions.html#temporaries |
| 62 | +[temporary scope rules]: ../../reference/destructors.html#temporary-scopes |
| 63 | +[tail expression temporary scope]: temporary-tail-expr-scope.md |
| 64 | + |
| 65 | +## Migration |
| 66 | + |
| 67 | +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`. |
| 68 | + |
| 69 | +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: |
| 70 | + |
| 71 | +```rust |
| 72 | +# use std::sync::RwLock; |
| 73 | +fn f(value: &RwLock<Option<bool>>) { |
| 74 | + match *value.read().unwrap() { |
| 75 | + Some(x) => { |
| 76 | + println!("value is {x}"); |
| 77 | + } |
| 78 | + _ => { |
| 79 | + let mut s = value.write().unwrap(); |
| 80 | + if s.is_none() { |
| 81 | + *s = Some(true); |
| 82 | + } |
| 83 | + } |
| 84 | + } |
| 85 | + // <--- Read lock is dropped here in both 2021 and 2024 |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +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. |
| 90 | + |
| 91 | +The `if_let_rescope` lint cannot deduce with complete confidence that the program semantics are preserved when the lifetime of such temporary values are shortened. For this reason, the suggestion from this lint is *not* automatically applied when running `cargo fix --edition`. It is recommended to manually inspect the warnings emitted when running `cargo fix --edition` and determine whether or not you need to apply the suggestion. |
| 92 | + |
| 93 | +If you want to manually inspect these warnings without performing the edition migration, you can enable the lint with: |
| 94 | + |
| 95 | +```rust |
| 96 | +// Add this to the root of your crate to do a manual migration. |
| 97 | +#![warn(if_let_rescope)] |
| 98 | +``` |
| 99 | + |
| 100 | +[`if_let_rescope`]: ../../rustc/lints/listing/allowed-by-default.html#if-let-rescope |
0 commit comments