Rust中Child的take()方法作用及避免部分移动的原理
问题背景
以下代码用于生成子进程并异步读取其stdout/stderr做日志记录,可正常编译运行:
use std::error::Error; use std::process::{Stdio}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Command, Child}; use tracing::{info, warn}; macro_rules! relay_pipe_lines { ($pipe:expr, $handler:expr) => { tokio::spawn(async move { let mut reader = BufReader::new($pipe).lines(); loop { let line = reader .next_line() .await .unwrap_or_else(|_| Some(String::new())); match line { None => break, Some(line) => $handler(line) } } }); }; } pub fn start_and_log_command(mut command: Command) -> Result<Child, Box<dyn Error>> { command.stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = command.spawn()?; let child_stdout = child.stdout.take().unwrap(); // 移除这里的take()会编译失败 let child_stderr = child.stderr.take().unwrap(); // 移除这里的take()也会编译失败 let child_pid = child.id().unwrap(); relay_pipe_lines!(child_stdout, |line|info!("[pid {}:stdout]: {}", child_pid, line)); relay_pipe_lines!(child_stderr, |line|warn!("[pid {}:stderr]: {}", child_pid, line)); Ok(child) } #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> { tracing_subscriber::fmt::init(); info!("Tracing logging initialised."); let mut command = Command::new("ls"); command.arg("-l"); let mut child = start_and_log_command(command)?; let exit_status = child.wait().await.expect("Cannot reap child process"); dbg!(exit_status.success()); Ok(()) }
若移除标注位置的take()调用,编译会报错:child.stdout partially moved due to this method call,现需明确两个问题:
- 此处
take()的作用是什么? - 为什么使用
take()不会导致child.stdout发生部分移动?
问题解答
1. take()的作用
Child结构体的stdout字段类型是Option<AsyncRead>(tokio中的异步流类型),take()是Rust标准库中Option类型的内置方法,核心行为是:
- 取出
Option内部包裹的实际值(这里就是子进程的stdout流) - 同时将原
Option字段设置为None
我们需要把stdout流的所有权转移给后续的BufReader和异步任务处理,take()正好完成了“取出值并清空原字段”的操作,既拿到了流的所有权,又不破坏原Child结构体的完整性。
2. 为什么take()不会引发部分移动
如果直接写let child_stdout = child.stdout.unwrap();,相当于把child.stdout这个Option字段的所有权整个移走——此时Child结构体里的stdout字段已不存在,剩下的Child变成“部分字段被移动”的不完整状态,后续返回child时,Rust编译器会报错,因为它不允许返回部分字段已被移动的结构体。
而用take()的话,我们并没有移走child.stdout这个字段本身,只是修改了它的内容:把原来的Some(流)改成了None。整个Child结构体的所有字段都还存在(stdout现在是None,其他字段比如pid、stderr等都完好),所以child仍然是完整的、可正常返回的结构体,自然不会触发“部分移动”的编译错误。
内容的提问来源于stack exchange,提问作者Martin Cowie

