In this lesson, you will learn how to schedule tasks that capture temporary references, ensuring they are only executed while the source data remains valid.
The Challenge of Deferred Execution
When you "defer" execution, you store a closure or a task object to run later. However, if that task captures a reference to a local variable, you face a memory safety risk: what if the variable goes out of scope before the task runs? Rust prevents this by requiring that any captured references live at least as long as the deferred task itself.
The Lifetime Constraint
To handle this, we tie the lifetime of our task queue to the lifetime of the data being borrowed. By using a lifetime parameter on our "TaskRunner," we guarantee that we cannot execute or store any task that outlives its captured data.
Consider this pattern:
struct TaskRunner<'a> {
tasks: Vec<Box<dyn Fn() + 'a>>,
}
impl<'a> TaskRunner<'a> {
fn schedule<F>(&mut self, task: F)
where F: Fn() + 'a
{
self.tasks.push(Box::new(task));
}
fn run(&self) {
for task in &self.tasks { task(); }
}
}
Why This Works
In the code above, the 'a lifetime annotation acts as a guard. If you attempt to pass a closure that references a temporary variable (like a local string created inside a function), the Rust compiler will verify that the TaskRunner instance cannot outlive that local variable.
If you try to move a TaskRunner containing a short-lived reference into a scope that expects a 'static execution, the compiler will catch it. This ensures that when run() is finally called, every captured reference inside your closures is still pointing to valid, initialized memory. By binding the task's lifetime to the TaskRunner, we safely move the "responsibility of existence" from the task itself to the object managing the execution.
Key Takeaway: By using explicit lifetime parameters on your task-running structures, you force the compiler to prove that all deferred operations safely expire before the data they reference is deallocated.