콘텐츠로 이동

Rust 에러 E0716: temporary value dropped while borrowed (임시 값이 먼저 해제됨)

E0716은 이런 뜻이에요. 변수 이름이 없는 값을 대여해 놓고, 그 대여를 다음 줄에서도 계속 썼어요. Rust는 이름 없는 값을 문장이 끝날 때 버리기 때문에, 대여가 아무것도 없는 곳을 가리키게 돼요. 무섭게 생겼지만, 가장 고치기 쉬운 대여 에러 중 하나예요. 보통은 한 줄을 두 줄로 나누기만 하면 돼요. (이 페이지는 Rust 컴파일러 에러 모음의 일부예요.)

error[E0716]: temporary value dropped while borrowed
--> src/main.rs:2:13
|
2 | let s = String::from("hi").as_str(); // s가 이름 없는 String을 대여해요
| ^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary value which is freed while still in use
3 | println!("{s}"); // ❌ 그 String은 이미 사라졌어요
| - borrow later used here
|
help: consider using a `let` binding to create a longer lived value
|
2 ~ let binding = String::from("hi");
3 ~ let s = binding.as_str(); // s가 이름 없는 String을 대여해요
|

“대여 중에 임시 값이 해제됐다”는 뜻이에요. 표시는 “아직 쓰이는 중인데 해제되는 임시 값을 만듦”, “이 문장이 끝날 때 임시 값이 해제됨”, “대여가 나중에 여기서 쓰임”이고, help:는 “let 바인딩으로 더 오래 사는 값을 만들어 보라”고 제안해요.

이 에러를 만드는 프로그램은 이거예요.

fn main() {
let s = String::from("hi").as_str(); // s가 이름 없는 String을 대여해요
println!("{s}"); // ❌ 그 String은 이미 사라졌어요
}

임시 값(temporary)은 식 중간에 만들어지지만 변수에 저장되지 않는 값이에요. 위 줄에서 String::from("hi")는 진짜 String을 만들지만, 그걸 소유하는 쪽이 없어요. 그 값을 받는 let name = ...이 없거든요. Rust는 임시 값을 문장이 끝날 때, 즉 ;에서 해제(drop, 정리)해요.

세 표시를 왼쪽에서 오른쪽으로 읽어 보세요.

  1. String::from("hi") 아래의 ^^^^: 여기서 임시 String이 만들어져요.
  2. 줄 끝의 -: 여기, ;에서 그 String이 해제돼요.
  3. 3번째 줄 s 아래의 -: 그런데 s(그 String 안을 가리키는 &str)는 여기서도 아직 쓰여요.

주문을 포스트잇에 적었다가 자리를 뜨자마자 포스트잇을 버리는 웨이터를 떠올려 보세요. 친구에게 “주문은 저 포스트잇에 적혀 있어”라고 말해도, 친구는 아무것도 찾지 못해요. 메모를 직접 갖고 있거나, 남아 있는 곳에 주문을 적어 둬야 해요.

메시지의 help: 부분이 대개 정답이에요. 변수를 하나 만들어서(rustc는 이걸 binding이라고 불러요) 값에 더 오래 사는 주인을 붙여 주세요.

E0597과 같은 규칙이에요. 참조자는 절대로 자기가 가리키는 값보다 오래 살아서는 안 돼요. 그렇지 않으면 해제된 메모리를 가리키는 댕글링 참조가 돼요. 차이는 딱 하나예요. E0597에서는 값에 이름이 있고 }에서 사라지지만, E0716에서는 값에 이름이 없고 ;에서 사라져요.

임시 값을 같은 문장 안에서 쓰는 건 전혀 문제없어요. println!("{}", String::from("hi").as_str());는 컴파일돼요. ; 전에 대여가 끝나니까요. 에러는 다음 줄에서도 대여가 필요할 때만 나요. 대여는 언제 끝날까요?와 라이프타임을 참고하세요.

도움이 되는 예외가 하나 있어요. 임시 값 수명 연장(temporary lifetime extension)이라고 해요. let에서 임시 값 바로 앞에 &(또는 &mut)를 붙이면, Rust가 조용히 그 임시 값을 변수만큼 오래 살려 둬요. 그래서 이 줄들은 컴파일돼요.

struct Label<'a> {
text: &'a str,
}
fn make_title() -> String {
String::from("받은편지함")
}
fn main() {
let greeting = &String::from("안녕"); // ✅ 연장됨: let 안에 바로 &
let queue = &mut vec![1, 2, 3]; // ✅ 연장됨
queue.push(4);
let label = Label { text: &make_title() }; // ✅ 연장됨: 구조체 리터럴 안에 바로 &
println!("{greeting} {queue:?} {}", label.text);
}

실행 결과:

안녕 [1, 2, 3, 4] 받은편지함
▶ 직접 실행해 보기

이 연장은 메서드 호출이나 함수 호출을 통과하지 못해요. String::from("hi").as_str(), make_title().as_str(), vec![1, 2].iter(), shout(&make_title())은 모두 호출 안에서 임시 값을 대여하니까, 임시 값은 여전히 ;에서 사라지고 E0716이 나요. 모든 경우를 외우려고 하지 마세요. E0716을 보면 값에 이름을 붙여 주면 돼요.

방법 1: let으로 임시 값에 이름 붙이기

섹션 제목: “방법 1: let으로 임시 값에 이름 붙이기”

거의 매번 이걸 고르세요. 컴파일러가 제안하는 방법이고, 대부분의 E0716에 대한 정답이에요. 먼저 값을 자기 변수에 저장하고, 다음 줄에서 그 변수를 대여하세요.

아주 흔한 예가 입력 한 줄을 나누는 경우예요. read_command()는 새 String을 돌려주고, .trim()과 .split(' ')은 그 문자열을 대여하고, Vec<&str>이 그 대여들을 들고 있어요.

let parts: Vec<&str> = read_command().trim().split(' ').collect(); // ❌ E0716

Rust는 read_command()를 가리키면서 문장이 끝날 때 해제된다고 말해요. 이름을 붙여 주세요.

fn read_command() -> String {
// 실제 프로그램이라면 stdin에서 한 줄을 읽어 올 거예요.
String::from(" add milk eggs \n")
}
fn main() {
let line = read_command(); // 이제 String이 main이 끝날 때까지 살아요
let parts: Vec<&str> = line.trim().split(' ').collect();
println!("{parts:?}");
println!("명령: {}, 항목 수: {}", parts[0], parts.len() - 1);
}

실행 결과:

["add", "milk", "eggs"]
명령: add, 항목 수: 2
▶ 직접 실행해 보기

실제 입력에서도 똑같이 만나게 돼요. io::stdin().lines()에 lines.next().unwrap().unwrap().trim().split(' ').collect()를 쓰면 같은 이유로 E0716이 나요. 먼저 let line = lines.next().unwrap().unwrap();을 따로 한 줄로 쓰세요. 문자열을 참고하세요.

방법 2: 참조자 대신 소유한 값 갖고 있기

섹션 제목: “방법 2: 참조자 대신 소유한 값 갖고 있기”

대여가 여러분이 만들 수 있는 어떤 변수보다도 오래 살아야 할 때 고르세요. 이리저리 넘기는 구조체 안이나, 반복문 안에서 채우는 Vec 같은 경우예요. 반복문 안의 let은 매 회차가 끝날 때 해제되니까, E0716이 E0597로 바뀔 뿐이에요.

잘못된 버전 두 개를 볼게요. 임시 값에서 얻은 &str을 담는 구조체와, 새 문자열의 대여를 넣는 반복문이에요.

let label = Label { text: make_title().as_str() }; // ❌ E0716
let mut names: Vec<&str> = Vec::new();
for name in ["ferris", "corro"] {
names.push(name.to_uppercase().as_str()); // ❌ E0716
}

해결 방법은 구조체와 Vec가 문자열을 소유하게 하는 거예요. &str 대신 String을 쓰고, .as_str()을 지우세요.

struct Label {
text: String, // 소유한 값이라 라이프타임이 필요 없어요
}
fn make_title() -> String {
String::from("받은편지함")
}
fn main() {
let label = Label { text: make_title() }; // String이 Label 안으로 이동해요
println!("라벨: {}", label.text);
let mut names: Vec<String> = Vec::new();
for name in ["ferris", "corro"] {
names.push(name.to_uppercase()); // Vec가 새 String을 하나씩 소유해요
}
println!("{names:?}");
}

실행 결과:

라벨: 받은편지함
["FERRIS", "CORRO"]
▶ 직접 실행해 보기

초보자에게는 소유한 필드(String, Vec<T>)가 거의 항상 가장 쉬운 선택이에요. &str 필드가 있는 구조체는 라이프타임이 필요하고, 빌려 온 데이터보다 절대 오래 살 수 없어요. 참조자를 담는 구조체와 구조체를 참고하세요.

임시 값이 반복자 체인의 시작일 때 고르세요. 아래 줄들은 vec![...]로 Vec를 만들고, .iter()로 대여하고, 그 반복자(또는 결과)를 나중을 위해 남겨 둬요.

let doubled = &mut vec![1, 2, 3].iter().map(|n| n * 10); // ❌ E0716
let best = vec![72, 95, 88].iter().max(); // ❌ E0716

앞에 붙인 &mut는 도움이 안 돼요. 수명 연장은 반복자를 살려 두지만, .iter()가 대여하는 Vec는 살려 두지 않거든요. 빠져나갈 방법은 두 가지예요.

  • into_iter()를 써서 반복자가 Vec를 소유하게 하세요(rustc도 이걸 제안해요). 그러면 &mut도 필요 없고, let mut만 쓰면 돼요.
  • .copied(), .cloned(), .sum(), 숫자를 모으는 .collect::<Vec<_>>()처럼 소유한 결과로 체인을 끝내세요. 그러면 ; 뒤에 임시 값을 대여하는 게 남지 않아요.
fn main() {
// 반복자가 Vec를 소유하니까, 임시 값을 대여하는 게 없어요
let mut doubled = vec![1, 2, 3].into_iter().map(|n| n * 10);
println!("{:?}", doubled.next());
println!("{:?}", doubled.next());
// .copied()는 Option<&i32>를 대여가 없는 값인 Option<i32>로 바꿔요
let best = vec![72, 95, 88].iter().max().copied();
println!("최고: {best:?}");
}

실행 결과:

Some(10)
Some(20)
최고: Some(95)
▶ 직접 실행해 보기

나중에 Vec가 다시 필요하다면 방법 1로 돌아가세요. let scores = vec![72, 95, 88];을 쓰고 scores.iter().max()를 하면 돼요. 반복자를 참고하세요.

체인 중간에서 Mutex를 잠글 때. let first = names.lock().unwrap().first();는 E0716을 내요. 잠금 가드(lock guard)가 임시 값이기 때문이에요. 가드가 해제되면 잠금도 풀리니까, first가 그 안을 계속 가리킬 수 없어요. 해결: let guard = names.lock().unwrap();을 쓰고 guard.first()를 하거나, .first().cloned()로 데이터를 한 번에 복사해 오세요. 공유 상태를 참고하세요.

참조자를 돌려주는 함수에 &임시 값을 넘길 때. fn shout(s: &str) -> &str이 있을 때 let t = shout(&make_title());은 E0716을 내요. 돌려받은 &str이 임시 String을 대여하는데, 수명 연장은 함수 호출을 통과하지 못하거든요. 해결: let title = make_title();을 쓰고 shout(&title)을 하세요.

함수에서 돌려줄 때. 참조자를 돌려주는 함수(-> &'static str 같은)의 마지막 줄에 make_title().as_str()이라고 쓰면, 대신 E0515(“cannot return value referencing temporary value”, 임시 값을 참조하는 값은 돌려줄 수 없음)가 나요. 해결: String을 돌려주세요.

이해도 점검

1. Rust에서 "임시 값"(temporary)이란 무엇인가요?

2. let s = String::from("hi").as_str(); 다음에 println!("{s}");를 쓰면 E0716이 나요. 보통은 어떻게 고치나요?

3. 임시 값 수명 연장 덕분에 컴파일되는 줄은 어느 것인가요?

점수: 0 / 3