Rust 를 잘 쓰는 법 2 — clone 과 unwrap 을 줄이고 타입으로 상태를 적는다
컴파일러를 달래려고 clone 과 unwrap 을 붙이는 습관이 Rust 를 어렵게 만든다. 값이 필요 없으면 빌려서 받고, 실패할 수 있으면 어디서 왜 실패했는지 담은 Result 로 돌려주고, bool 여러 개 대신 enum 으로 상태를 적는다. 세 가지를 실행해 비교한다.
01 1. 가질 필요가 없으면 빌려서 받는다
입문 17편에서 String 을 넘기면 함수가 주인이 된다고 했다. 그래서 넘긴 뒤에도 쓰려고 .clone() 을 붙이기 쉽다.
fn shout(text: String) -> String {
text.to_uppercase()
}
fn main() {
let name = String::from("ferris");
let loud = shout(name.clone());
println!("{name} -> {loud}");
}이 코드는 경고 없이 컴파일된다. 기본 clippy 도 조용하다. 기본으로 꺼져 있는 린트 둘(값으로 받을 필요가 없는 매개변수 · 필요 없는 clone)을 켜고 돌리면 이렇게 나온다.
cargo clippy -- -W clippy::needless_pass_by_value -W clippy::redundant_clonewarning: this argument is passed by value, but not consumed in the function body
--> src/main.rs:1:16
|
1 | fn shout(text: String) -> String {
| ^^^^^^ help: consider changing the type to: `&str`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.95.0/index.html#needless_pass_by_value
= note: requested on the command line with `-W clippy::needless-pass-by-value`
warning: `greet` (bin "greet") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12sshout 은 글자를 읽기만 하고 가지지 않는다(not consumed). 그런데 String 을 받으니 부르는 쪽이 주인을 넘기거나 복사해야 했다. &str 로 받으면(입문 20편) 복사도 이동도 없다.
fn shout(text: &str) -> String {
text.to_uppercase()
}
fn main() {
let name = String::from("ferris");
let loud = shout(&name);
println!("{name} -> {loud}");
} Checking greet v0.1.0 (~/projects/greet)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s경고가 사라졌다. 판단 기준은 하나다. 함수가 값을 저장하거나 바꿔서 돌려줄 때만 주인으로 받고, 읽기만 하면 빌려서 받는다. clone 이 보이면 "정말 둘이 필요한가" 를 한 번 묻는다.
02 2. unwrap 대신 어디서 왜를 담는다
가격 목록을 더하다가 쉼표가 든 값을 만났다.
fn total(lines: &[&str]) -> u32 {
lines.iter().map(|l| l.parse::<u32>().unwrap()).sum()
}
fn main() {
let lines = ["1200", "800", "1,500", "300"];
println!("{}", total(&lines));
} Compiling prices v0.1.0 (~/projects/prices)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/prices`
thread 'main' (213392076) panicked at src/main.rs:2:43:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace멈춘 자리와 InvalidDigit 만 남았다. 어느 줄의 어떤 값이 문제였는지는 알 수 없다. 사용자 입력이나 파일을 읽는 곳이라면 일어날 수 있는 실패이니(입문 30편), 이유를 담아 돌려준다.
fn total(lines: &[&str]) -> Result<u32, String> {
let mut sum = 0;
for (i, line) in lines.iter().enumerate() {
let n: u32 = line
.parse()
.map_err(|e| format!("line {}: '{line}' is not a price ({e})", i + 1))?;
sum += n;
}
Ok(sum)
}
fn main() {
match total(&["1200", "800", "1,500", "300"]) {
Ok(sum) => println!("total {sum}"),
Err(message) => println!("error: {message}"),
}
} Compiling prices v0.1.0 (~/projects/prices)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/prices`
error: line 3: '1,500' is not a price (invalid digit found in string)이제 "3번째 줄의 '1,500' 이 가격이 아니다" 라고 정확히 말한다. 이 문장 그대로 사용자에게 보여 줘도 된다. unwrap 은 테스트 코드나, 정말 실패할 수 없다고 증명할 수 있는 곳에만 남긴다. 그때도 expect("왜 실패할 수 없는지") 로 이유를 적어 두면 나중에 읽는 사람이 고맙다.
03 실습 — bool 여럿 대신 enum
주문 상태를 is_paid: bool, is_refunded: bool 두 칸으로 두면 "결제 안 됐는데 환불됨" 같은 말이 안 되는 조합이 생긴다. 그런 상태는 아예 만들 수 없게 enum 으로 적는다(입문 24·25편).
enum Order {
Pending,
Paid { amount: u32 },
Refunded { amount: u32, reason: String },
}
fn describe(order: &Order) -> String {
match order {
Order::Pending => "waiting for payment".to_string(),
Order::Paid { amount } => format!("paid {amount} won"),
Order::Refunded { amount, reason } => format!("refunded {amount} won ({reason})"),
}
}
fn main() {
let orders = [
Order::Pending,
Order::Paid { amount: 12000 },
Order::Refunded { amount: 12000, reason: "wrong size".to_string() },
];
for o in &orders {
println!("{}", describe(o));
}
} Compiling orders v0.1.0 (~/projects/orders)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/orders`
waiting for payment
paid 12000 won
refunded 12000 won (wrong size)- 환불 사유는
Refunded일 때만 있다.bool두 칸 방식이었다면 사유 칸이 늘 있고, 환불이 아닐 때는 비워 둬야 했다. match가 빠짐없이 다루라고 강제하니(입문 25편), 나중에Shipped상태를 더하면 다루지 않은 곳을 컴파일러가 모두 짚어 준다.
| 습관 | 대신 |
|---|---|
넘기기 전에 .clone() | 읽기만 하면 &str·&[T] 로 받기 |
.unwrap() | 어디서·왜를 담은 Result + ? (8편) |
bool 여러 칸 | 상태마다 필요한 값을 가진 enum |
자기 코드에서 .clone() 과 .unwrap() 을 검색해 하나씩 "정말 필요한가" 를 따져 보자. 다음 편부터는 AI 코딩 에이전트와 함께 Rust 를 쓰는 법이다.