Borrowed value life time: `if let` and `RefCell`

Problem: Why need a semicolon; at the end of if let expression?

#![allow(unused)]
use std::rc::Rc;
use std::cell::RefCell;

// Definition for a binary tree node.
#[derive(Debug, PartialEq, Eq)]
pub struct TreeNode {
  pub val: i32,
  pub left: Option<Rc<RefCell<TreeNode>>>,
  pub right: Option<Rc<RefCell<TreeNode>>>,
}

impl TreeNode {
  #[inline]
  pub fn new(val: i32) -> Self {
    TreeNode {
      val,
      left: None,
      right: None
    }
  }
}

fn main() {
    {
        let root = Some(Rc::new(RefCell::new(TreeNode::new(1))));
        let node = root.unwrap();
        if let Some(left) = node.borrow().left.clone() {
            // do nothing
        }
    }
}

Compile Error:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `node` does not live long enough
  --> src/main.rs:29:29
   |
29 |         if let Some(left) = node.borrow().left.clone() {
   |                             ^^^^^^^^^^^^^
   |                             |
   |                             borrowed value does not live long enough
   |                             a temporary with access to the borrow is created here ...
...
32 |     }
   |     - `node` dropped here while still borrowed
33 | }
   | - ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `Ref<'_, TreeNode>`
   |
help: consider adding semicolon after the expression so its temporaries are dropped sooner, before the local variables declared by the block are dropped
   |
31 |         };
   |          +

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` due to previous error

Here is another way to avoid this error:

use std::rc::Rc;
use std::cell::RefCell;

// Definition for a binary tree node.
#[derive(Debug, PartialEq, Eq)]
pub struct TreeNode {
  pub val: i32,
  pub left: Option<Rc<RefCell<TreeNode>>>,
  pub right: Option<Rc<RefCell<TreeNode>>>,
}

impl TreeNode {
  #[inline]
  pub fn new(val: i32) -> Self {
    TreeNode {
      val,
      left: None,
      right: None
    }
  }
}

fn main() {
    {
        let root = Some(Rc::new(RefCell::new(TreeNode::new(1))));
        if let Some(node) = root {
            if let Some(left) = node.borrow().left.clone() {
                // do nothing
            }
        }
    }
}

I don't understand what difference between them. Thanks :smiley:

The issue here relates to temporary scopes. First, note that an if let expression is semantically equivalent to a match expression, so the first program is equivalent to the following:

fn main() {
    {
        let root = Some(Rc::new(RefCell::new(TreeNode::new(1))));
        let node = root.unwrap();
        match node.borrow().left.clone() {
            Some(left) => {
                // do nothing
            }
            None => {}
        }
    }
}

And indeed, the compiler generates the same error. The reason for this is hinted at by the "Notes" in the Rust Reference link above:

Temporaries that are created in the final expression of a function body are dropped after any named variables bound in the function body, as there is no smaller enclosing temporary scope.

The scrutinee of a match expression is not a temporary scope, so temporaries in the scrutinee can be dropped after the match expression. For example, the temporary for 1 in match 1 { ref mut z => z }; lives until the end of the statement.

Together, these explain the behavior we see. Since node is placed in a let binding, it is dropped at the end of the containing block. But since the if let/match expression is the last expression in the block, and since the block is the last expression in the function body, the node.borrow() in the scrutinee must last all the way to the end of the function, even though we aren't returning it. Together, these result in a lifetime error. As the message notes, placing the if let in its own statement is sufficient to reduce the lifetime of the scrutinee to be shorter than that of node.

As for your latter program, "the expression for a match arm" is a temporary scope, and the body of an if let is effectively a match arm; therefore, the node.borrow() is confined to the body of the outer if let, and no lifetimes are violated.

1 Like

Here's a related issue that links to various places of interested, such as the addition of the help suggestion and RFC 0066, which has seen some recent movement (a pleasant surprise).

1 Like

Thanks! :+1:

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.