Rust 표준 라이브러리 활용 팁: 매일 쓰는 작은 기술들
부엌 서랍에는 작은 도구들이 들어 있죠. 감자칼, 병따개, 마늘 다지기 같은 것들요. 큰 칼 하나로도 모든 요리를 할 수는 있지만, 작은 도구가 있으면 더 빠르고 안전하게 할 수 있어요. Rust 표준 라이브러리에도 그런 서랍이 있어요.
이 페이지에서는 그 서랍 속을 하나씩 구경해요. 기술마다 짧게, 일상 속 비유 하나, 실행해 볼 수 있는 예제 하나, 그리고 “이럴 때 써요” 한 줄을 붙였어요. 한 번에 다 외울 필요는 없어요. 지금은 가볍게 훑어보고, 나중에 내 코드에서 “뭔가 어색한데?” 싶은 부분이 보이면 다시 찾아오세요.
&mut에서 값 꺼내기: mem::take, replace, swap
섹션 제목: “&mut에서 값 꺼내기: mem::take, replace, swap”봉투가 씌워진 쓰레기통을 떠올려 보세요. 가득 찬 봉투를 꺼내도 되지만, 곧바로 새 봉투를 끼워야 한다는 조건이 있어요. 쓰레기통이 봉투 없이 비어 있는 순간은 없어요.
Rust에도 똑같은 규칙이 있어요. &mut self만 가지고 있을 때는 필드를 그냥 이동(move)해서 꺼낼 수 없어요. 구조체에 구멍이 생기기 때문이에요. 그렇게 하려고 하면 컴파일러가 E0507로 막아 줘요. std::mem의 함수들은 항상 무언가를 대신 남겨 두는 방법으로 이 문제를 해결해요.
mem::take(&mut x)는 값을 꺼내고, 그 자리에 타입의 기본값(빈Vec, 빈String,0,None)을 남겨요.mem::replace(&mut x, new)는new를 넣고, 원래 있던 값을 돌려줘요.Option::take()는Option안의 값을 꺼내고None을 남겨요.
use std::mem;
struct Inbox { unread: Vec<String>, status: String, draft: Option<String>,}
impl Inbox { // 안 읽은 메시지를 전부 넘겨주고, 빈 Vec을 남겨요. fn read_all(&mut self) -> Vec<String> { mem::take(&mut self.unread) }
// 새 상태를 넣고, 이전 상태를 돌려받아요. fn set_status(&mut self, new: &str) -> String { mem::replace(&mut self.status, new.to_string()) }
// 임시 저장 글이 있으면 꺼내고, None을 남겨요. fn send_draft(&mut self) -> Option<String> { self.draft.take() }}
fn main() { let mut inbox = Inbox { unread: vec!["점심 같이 먹을래요?".to_string(), "청구서 #42".to_string()], status: "온라인".to_string(), draft: Some("6시에 봐요".to_string()), };
let messages = inbox.read_all(); println!("읽음 {:?}, 아직 안 읽음: {:?}", messages, inbox.unread);
let old = inbox.set_status("회의 중"); println!("이전 상태 '{old}', 지금 상태 '{}'", inbox.status);
println!("보냄: {:?}", inbox.send_draft()); println!("다시 보냄: {:?}", inbox.send_draft());}실행 결과:
읽음 ["점심 같이 먹을래요?", "청구서 #42"], 아직 안 읽음: []이전 상태 '온라인', 지금 상태 '회의 중'보냄: Some("6시에 봐요")다시 보냄: Noneclone()이 어디에도 없다는 점을 보세요. 메시지와 이전 상태는 복사되는 게 아니라 밖으로 이동해요. 그래서 이 함수들은 안전하면서도 비용이 거의 들지 않아요.
같은 가족이 둘 더 있어요. mem::swap(&mut a, &mut b)는 두 값을 맞바꿔요. 식탁 위의 접시 두 개를 서로 바꿔 놓는 것처럼요. Option::replace(new)는 Option에 새 값을 넣고 이전 값을 돌려줘요.
use std::mem;
fn main() { // 한쪽 목록을 처리하는 동안 다른 쪽 목록을 채워요. let mut filling = vec!["주문 1", "주문 2"]; let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing); println!("처리 중 {processing:?}, 채우는 중 {filling:?}");
// Option::replace: 새 값을 넣고, 이전 값을 돌려받아요. let mut now_playing = Some("노래 A"); let previous = now_playing.replace("노래 B"); println!("이전 {previous:?}, 지금 {now_playing:?}");}실행 결과:
처리 중 ["주문 1", "주문 2"], 채우는 중 []이전 Some("노래 A"), 지금 Some("노래 B")이럴 때 써요: &mut 접근만 있는데, 안에 든 값의 소유권이 필요할 때.
슬라이스: windows, chunks와 친구들
섹션 제목: “슬라이스: windows, chunks와 친구들”windows(2)는 자 위를 미끄러지는 돋보기라고 생각하면 돼요. 눈금 두 개를 한 번에 보여 주고, 한 칸씩 옆으로 움직여요. chunks(3)은 달걀을 세 개짜리 상자에 담는 것과 같아요. 마지막 상자는 덜 찰 수도 있어요. chunks_exact(3)은 꽉 찬 상자만 주고, 남은 달걀은 .remainder()에 따로 모아 둬요.
이 메서드들은 모든 슬라이스에서 쓸 수 있으니, 배열과 벡터에서도 쓸 수 있어요.
fn main() { let temps = [12, 15, 14, 19, 21];
// windows(2): 이웃한 두 값씩, 겹치면서 for pair in temps.windows(2) { println!("{} -> {}: 변화 {}", pair[0], pair[1], pair[1] - pair[0]); }
// chunks(3): 3개씩 묶음, 마지막 묶음은 더 작을 수 있어요 let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; for batch in emails.chunks(3) { println!("보내는 중: {batch:?}"); }
// chunks_exact(2): 꽉 찬 묶음만, 나머지는 remainder()에서 기다려요 let seats = [1, 2, 3, 4, 5]; let pairs = seats.chunks_exact(2); let leftover = pairs.remainder(); for pair in pairs { println!("짝 {pair:?}"); } println!("남은 것: {leftover:?}");}실행 결과:
12 -> 15: 변화 315 -> 14: 변화 -114 -> 19: 변화 519 -> 21: 변화 2보내는 중: ["[email protected]", "[email protected]", "[email protected]"]보내는 중: ["[email protected]", "[email protected]"]짝 [1, 2]짝 [3, 4]남은 것: [5]슬라이스 도우미 몇 개만 더 알면, 반복문 없이도 자주 하는 질문에 답할 수 있어요. split_first()는 첫 번째 항목과 나머지를 줘요. 선생님이 명단의 첫 이름을 부르고, 나머지 명단은 도우미에게 넘겨주는 것처럼요. split_last()는 같은 일을 끝에서부터 해요.
fn main() { let command = ["git", "commit", "-m", "오타 수정"];
// split_first: 첫 번째 항목과, 나머지의 슬라이스 if let Some((program, args)) = command.split_first() { println!("프로그램: {program}, 인자: {args:?}"); }
// split_last: 마지막 항목과, 그 앞의 전부 if let Some((message, _rest)) = command.split_last() { println!("메시지: {message}"); }
println!("git으로 시작해요? {}", command.starts_with(&["git"])); println!("-m이 있어요? {}", command.contains(&"-m"));
// iter().rev(): 슬라이스는 그대로 두고 거꾸로 걸어가요 let history = ["열기", "편집", "저장"]; let undo_order: Vec<_> = history.iter().rev().collect(); println!("되돌리기 순서: {undo_order:?}");}실행 결과:
프로그램: git, 인자: ["commit", "-m", "오타 수정"]메시지: 오타 수정git으로 시작해요? true-m이 있어요? true되돌리기 순서: ["저장", "편집", "열기"]split_first와 split_last는 Option을 돌려줘요. 빈 슬라이스에는 첫 번째 항목이 없으니까요. 그래서 if let을 썼어요.
이럴 때 써요: 이웃한 값(windows), 묶음(chunks), 또는 “맨 앞 하나와 나머지”(split_first)가 필요할 때.
늘 쓰게 되는 반복자 도우미
섹션 제목: “늘 쓰게 되는 반복자 도우미”반복자 페이지에서 map, filter, sum, collect를 다뤘어요. 여기서는 도구 상자의 다음 칸을 열어 봐요.
peekable: 꺼내기 전에 살짝 보기
섹션 제목: “peekable: 꺼내기 전에 살짝 보기”카드를 나눠 준다고 상상해 보세요. 가끔은 다음 카드를 뽑기 전에 먼저 슬쩍 보고 싶을 때가 있죠. .peekable()은 그렇게 할 수 있는 반복자를 만들어 줘요. .peek()은 다음 항목을 꺼내지 않고 보여 주고, .next_if(...)는 조건을 통과할 때만 꺼내요. 작은 파서(parser)에 딱 필요한 기능이에요.
fn main() { let input = "12+345-6"; let mut chars = input.chars().peekable(); let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() { if c.is_ascii_digit() { let mut number = c.to_string(); // 다음 글자도 숫자일 때만 꺼내요. while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) { number.push(d); } tokens.push(number); } else { tokens.push(c.to_string()); } }
println!("{tokens:?}");}실행 결과:
["12", "+", "345", "-", "6"]peekable이 없다면, 숫자를 더 찾다가 +를 읽어 버리고, 그 +는 그대로 사라져 버릴 거예요.
이럴 때 써요: 다음 항목을 보고 지금 항목을 어떻게 할지 정해야 할 때.
목록 훑기: zip, enumerate, step_by, take_while, skip_while, chain
섹션 제목: “목록 훑기: zip, enumerate, step_by, take_while, skip_while, chain”| 메서드 | 이렇게 생각해 보세요 | 이럴 때 써요 |
|---|---|---|
zip |
양쪽 이를 하나씩 맞물리는 지퍼 | 짝을 이루는 목록이 두 개 있을 때 |
enumerate |
목록의 줄마다 번호 붙이기 | 항목과 함께 인덱스도 필요할 때 |
step_by(n) |
계단을 두 칸(또는 n칸)씩 오르기 | n번째 항목마다 하나씩 원할 때 |
take_while |
싫어하는 맛이 나올 때까지 사탕 먹기 | 목록의 앞부분을, 처음 “아니요”가 나오기 전까지 원할 때 |
skip_while |
본방송이 시작될 때까지 광고 건너뛰기 | 처음 “아니요”가 나온 곳부터 나머지를 원할 때 |
chain |
기차 객차 두 칸 연결하기 | 목록 하나 뒤에 다른 목록을 이어 붙일 때 |
fn main() { let names = ["지아", "민준", "서연"]; let scores = [88, 92, 75];
// zip은 두 목록을 짝지어 주고, enumerate는 0부터 세는 번호를 붙여요 for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() { println!("{}. {name}: {score}", i + 1); }
// step_by: 한 시간 중 15분마다 let quarters: Vec<u32> = (0..60).step_by(15).collect(); println!("15분 단위: {quarters:?}");
// 배열에 into_iter()를 쓰면 숫자 자체를 받아요 let queue = [3, 5, 8, 12, 4]; let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect(); let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect(); println!("처음 큰 수가 나오기 전: {small:?}, 그 뒤로: {rest:?}");
// chain: 한 목록 뒤에 다른 목록을 let morning = ["이메일", "스탠드업"]; let afternoon = ["코드 리뷰", "배포"]; let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect(); println!("{day:?}");}실행 결과:
1. 지아: 882. 민준: 923. 서연: 7515분 단위: [0, 15, 30, 45]처음 큰 수가 나오기 전: [3, 5, 8], 그 뒤로: [12, 4]["이메일", "스탠드업", "코드 리뷰", "배포"]take_while은 12에서 완전히 멈춘다는 점에 주목하세요. 그 뒤의 4는 작은 수지만, 아예 검사되지 않아요. 모든 항목을 하나하나 살펴보는 filter와 다른 점이 바로 이거예요.
질문에 답하기: fold, position, find_map, partition, flat_map
섹션 제목: “질문에 답하기: fold, position, find_map, partition, flat_map”| 메서드 | 이렇게 생각해 보세요 | 이럴 때 써요 |
|---|---|---|
sum |
계산기의 더하기 버튼 | 그냥 합계만 필요할 때 |
fold |
언덕을 굴러 내려가며 점점 커지는 눈덩이 | 결과 하나를 나만의 방식으로 만들 때 |
position |
“민준이는 몇 번 자리에 앉았지?” | 처음 조건에 맞는 항목의 인덱스가 필요할 때 |
find_map |
문이 열릴 때까지 열쇠를 하나씩 꽂아 보기 | 항목마다 무언가를 시도하고, 처음 성공한 결과를 원할 때 |
partition |
빨래를 바구니 두 개에 나눠 담기 | 예/아니요 질문으로 목록 하나를 둘로 나눌 때 |
flat_map |
봉지 여러 개를 탁자 하나에 쏟기 | 항목 하나가 여러 항목이 될 때 |
fn main() { let orders = [12, 40, 7, 55, 30];
// sum: 그냥 다 더해요 let total: i32 = orders.iter().sum();
// fold: 나만의 방식으로 더해요 (여기서는 주문마다 수수료 2가 붙어요) let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2); println!("합계 {total}, 수수료 포함 {with_fees}");
// position: 처음 조건에 맞는 항목의 인덱스 println!("50을 넘는 첫 주문의 인덱스: {:?}", orders.iter().position(|p| *p > 50));
// partition: "예"용 Vec 하나, "아니요"용 Vec 하나 let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30); println!("큰 주문 {big:?}, 작은 주문 {small:?}");
// find_map: 하나씩 숫자로 바꿔 보고, 처음 성공한 것을 써요 let inputs = ["abc", "", "42", "7"]; let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok()); println!("첫 번째 숫자: {first_number:?}");
// flat_map: 줄마다 여러 단어가 되고, 전부 목록 하나에 들어가요 let lines = ["우유 사기", "엄마한테 지금 전화하기"]; let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect(); println!("{words:?}");}실행 결과:
합계 144, 수수료 포함 15450을 넘는 첫 주문의 인덱스: Some(3)큰 주문 [40, 55, 30], 작은 주문 [12, 7]첫 번째 숫자: Some(42)["우유", "사기", "엄마한테", "지금", "전화하기"]fold는 시작값(0)과, “지금까지의 결과”와 다음 항목을 받는 클로저를 인자로 받아요. 단순한 합계만 필요하다면 sum이 의도를 더 분명하게 보여 줘요. 규칙을 직접 정해야 할 때 fold를 꺼내 쓰세요.
Vec을 그 자리에서 정리하기: dedup과 retain
섹션 제목: “Vec을 그 자리에서 정리하기: dedup과 retain”이 둘은 반복자 메서드가 아니라 Vec의 메서드지만, 같은 서랍에 넣어 둘 만해요. dedup은 같은 노래를 연달아 두 번 틀지 않는 DJ 같아요(나중에 다시 트는 건 괜찮아요). retain은 입구의 경비원이에요. 검사를 통과한 항목만 안에 남아요.
fn main() { // dedup은 바로 옆에 붙어 있는 중복을 없애요 let mut clicks = vec!["홈", "홈", "상점", "상점", "상점", "장바구니", "홈"]; clicks.dedup(); println!("{clicks:?}");
// retain은 조건을 통과한 항목만 남겨요 let mut cart = vec![("사과", 3), ("빵", 0), ("우유", 1), ("달걀", 0)]; cart.retain(|(_, qty)| *qty > 0); println!("{cart:?}");}실행 결과:
["홈", "상점", "장바구니", "홈"][("사과", 3), ("우유", 1)]마지막 "홈"이 살아남은 건, 다른 "홈"과 바로 붙어 있지 않았기 때문이에요. 중복을 모두 없애고 싶다면, 정렬 페이지에서 보여 주는 것처럼 먼저 정렬하세요.
이럴 때 써요: 새 Vec을 만들지 않고 지금 있는 Vec을 바꾸고 싶을 때.
Option과 Result의 편리한 도우미
섹션 제목: “Option과 Result의 편리한 도우미”Option은 도착했을 수도, 아직 안 왔을 수도 있는 택배 상자 같아요. 상자를 열 때마다 match를 제대로 쓸 수도 있지만, 대부분은 한 단어짜리 도우미로 똑같은 뜻을 표현할 수 있어요. Option 페이지에서 unwrap_or와 match를 다뤘으니, 여기서는 그다음 도우미들을 소개할게요.
fn main() { let nickname: Option<String> = None; let age: Option<u32> = Some(17); let width: Option<u32> = Some(3); let height: Option<u32> = Some(4);
// unwrap_or_default: 값, 없으면 타입의 기본값 ("", 0, 빈 Vec...) println!("별명: '{}'", nickname.unwrap_or_default());
// map_or: None이면 대체 값, 아니면 값을 변환해요 println!("{}", age.map_or("나이 모름".to_string(), |a| format!("{a}살")));
// filter: 조건을 통과할 때만 값을 남겨요 println!("성인 나이: {:?}", age.filter(|a| *a >= 18));
// zip: 둘 다 Some일 때만 Some println!("넓이: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then: 그 자체로 None이 될 수 있는 단계를 이어 붙여요 let good = Some("42").and_then(|s| s.parse::<u32>().ok()); let bad = Some("사십이").and_then(|s| s.parse::<u32>().ok()); println!("파싱 결과: {good:?}, {bad:?}");}실행 결과:
별명: ''17살성인 나이: None넓이: Some(12)파싱 결과: Some(42), None함수 안에서는 Option에도 ?를 쓸 수 있어요. 값이 None이면 함수가 곧바로 None을 반환해요. Result를 반환하는 함수라면, 먼저 ok_or(error)나 ok_or_else(|| error)로 Option을 Result로 바꿔 주세요. 에러를 만드는 데 일이 좀 든다면 ok_or_else를 쓰세요. 필요할 때만 실행되거든요.
struct Employee { name: String, manager: Option<String>,}
// Option에 `?`: None이면 곧바로 None을 반환해요.fn manager_initial(e: &Employee) -> Option<char> { let manager = e.manager.as_ref()?; manager.chars().next()}
// ok_or_else가 None을 에러로 바꿔 주니, Result 함수에서도 `?`를 쓸 수 있어요.fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting.ok_or_else(|| "PORT가 설정되지 않았어요".to_string())?; raw.parse::<u16>().map_err(|e| e.to_string())}
fn main() { let ana = Employee { name: "지아".to_string(), manager: Some("하은".to_string()) }; let grace = Employee { name: "하은".to_string(), manager: None }; println!("{}: {:?}", ana.name, manager_initial(&ana)); println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080"))); println!("{:?}", port_from(None)); println!("{:?}", port_from(Some("팔십")));
// transpose: Option<Result<..>>가 Result<Option<..>>로 바뀌어요 let retries_setting: Option<&str> = Some("3"); let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose(); println!("{retries:?}");}실행 결과:
지아: Some('하')하은: NoneOk(8080)Err("PORT가 설정되지 않았어요")Err("invalid digit found in string")Ok(Some(3))invalid digit found in string은 Rust가 직접 만드는 에러 메시지라서 영어로 나와요. “문자열에 잘못된 숫자가 있다”는 뜻이에요. 그리고 chars().next()는 한글 이름에서도 첫 글자('하')를 제대로 꺼내 줘요.
transpose는 자주 쓰이진 않지만, “설정값이 있을 수도 없을 수도 있고, 파싱이 실패할 수도 있다”는 상황에 딱 맞는 깔끔한 답이에요. 이걸 쓰면 결과에 ?를 붙이면서도 Option은 그대로 유지할 수 있어요.
이럴 때 써요: Option에 대한 match에서 의미 있는 갈래는 하나뿐이고, 나머지 하나는 뻔할 때.
HashMap의 entry API
섹션 제목: “HashMap의 entry API”외투 보관소를 생각해 보세요. 이름을 말하면, 직원이 그 이름의 옷걸이를 찾거나 새로 하나 걸어 두고 건네줘요. 두 번이 아니라 한 번에 끝나요. 이게 .entry(key)예요. 해시맵 페이지에서는 대표적인 카운터인 entry(key).or_insert(value)를 보여 줬어요. 알아 두면 좋은 변형이 두 가지 더 있어요.
or_insert_with(f)는 키가 없을 때만 기본값을 만들어요.or_default()는 같은 일을 타입의 기본값으로 해요.and_modify(f)는 키가 있으면 값을 바꿔요. 뒤에or_insert를 이어 붙이면 “새 키”인 경우도 처리할 수 있어요.
use std::collections::HashMap;
fn main() { let sales = [("사과", 3), ("배", 2), ("사과", 5), ("자두", 1), ("배", 4)];
// and_modify + or_insert: 있으면 갱신, 없으면 추가 let mut totals: HashMap<&str, i32> = HashMap::new(); for (fruit, qty) in sales { totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty); }
// or_insert_with: 빈 Vec은 필요할 때만 만들어요 let mut orders: HashMap<&str, Vec<i32>> = HashMap::new(); for (fruit, qty) in sales { orders.entry(fruit).or_insert_with(Vec::new).push(qty); }
// HashMap의 순서는 무작위라서, 출력하기 전에 키를 정렬해요 let mut fruits: Vec<&str> = totals.keys().copied().collect(); fruits.sort(); for fruit in fruits { println!("{fruit}: 합계 {}, 주문 {:?}", totals[fruit], orders[fruit]); }}실행 결과:
배: 합계 6, 주문 [2, 4]사과: 합계 8, 주문 [3, 5]자두: 합계 1, 주문 [1]한글 문자열은 가나다순으로 정렬돼요. 그래서 배, 사과, 자두 순서로 나와요.
이럴 때 써요: “키가 있으면 바꾸고, 없으면 넣기”를 직접 쓰려는 순간. 그렇게 쓰면 키를 두 번 찾게 되거든요.
never 타입 !: 절대 돌아오지 않는 코드
섹션 제목: “never 타입 !: 절대 돌아오지 않는 코드”일방통행 길처럼, 한번 들어가면 다시는 나오지 못하는 길이 있어요. Rust에서 끝나지 않는 표현식은 never 타입을 가지며, !로 써요. panic!, return, break, continue, 끝없는 loop, 그리고 std::process::exit가 모두 이 타입이에요.
! 값은 실제로 존재할 수 없기 때문에, 컴파일러는 이 값이 어떤 타입의 자리에든 들어갈 수 있게 해 줘요. 그래서 match의 한 갈래는 i32를 주고, 다른 갈래는 return을 해도 괜찮아요. 두 번째 갈래는 값을 만들어 내지 않으니, 타입을 깨뜨릴 일이 없거든요.
use std::process;
// `-> !`는 "이 함수는 절대 반환하지 않아요"라는 뜻fn quit(message: &str) -> ! { println!("{message}"); process::exit(0)}
fn level_points(level: &str) -> u32 { let points: u32 = match level { "쉬움" => 10, "어려움" => 30, _ => return 0, // `return`은 `!` 타입이라서, u32가 와야 할 자리에 들어갈 수 있어요 }; points * 2}
fn main() { println!("쉬움: {}, 어려움: {}, 이상한 값: {}", level_points("쉬움"), level_points("어려움"), level_points("???"));
let mut total = 0; for input in ["4", "앗", "6"] { let n: i32 = match input.parse() { Ok(n) => n, Err(_) => continue, // `continue`도 `!`예요 }; total += n; } println!("합계: {total}");
let config_file: Option<&str> = None; let config = match config_file { Some(path) => path, None => quit("설정 파일이 없어서 멈춰요"), }; println!("불러옴: {config}"); // 출력되지 않아요}실행 결과:
쉬움: 20, 어려움: 60, 이상한 값: 0합계: 10설정 파일이 없어서 멈춰요주의할 점이 하나 있어요. process::exit는 프로그램을 즉시 멈추고, 아직 살아 있는 값들의 정리 코드(소멸자, destructor)를 실행하지 않아요. 실제 프로그램에서는 보통 main에서 그냥 반환하는 쪽이 더 부드러운 방법이에요.
코드의 뼈대를 잡는 동안 !를 자리 표시자로 쓰는 매크로가 두 개 있어요. todo!()는 “나중에 쓸게요”, unimplemented!()는 “여기서는 지원하지 않아요”라는 뜻이에요. 둘 다 실제로 실행되면 패닉하지만, 나머지 프로그램은 컴파일되게 해 줘요.
fn export_pdf(report: &str) -> Vec<u8> { todo!("PDF 내보내기는 v2에서 지원해요")}이 함수를 호출하면 not yet implemented: PDF 내보내기는 v2에서 지원해요가 나와요. unimplemented!("PDF 내보내기")라면 메시지는 not implemented: PDF 내보내기예요. 앞쪽의 영어 부분은 Rust가 직접 붙이는 문구예요.
이럴 때 써요: 함수가 프로그램을 멈춰야 하거나, 어떤 갈래가 값을 주는 대신 일찍 빠져나가야 할 때.
작지만 쏠쏠한 도구들
섹션 제목: “작지만 쏠쏠한 도구들”주머니 속 공구 같은 것들이에요. 아주 작지만 매주 쓰게 될 거예요.
matches!(value, pattern)은 “이 값이 패턴에 맞나요?“에true나false로 답해요(자세한 건 패턴 매칭 페이지에서).std::cmp::min/max는 두 값 중 작은 쪽이나 큰 쪽을 골라 주고,.clamp(low, high)는 값을 범위 안에 묶어 둬요. 100에서 더 돌아가지 않는 볼륨 다이얼처럼요..abs_diff(other)는 두 수 사이의 거리를 줘요. 항상 양수이고, 부호 없는 타입에서도 절대 오버플로가 나지 않아요.dbg!(expr)는 파일, 줄, 표현식, 값을 출력한 다음 그 값을 그대로 돌려줘요. 그래서 어떤 표현식이든 감쌀 수 있어요.assert!(condition)은 조건이 거짓이면 패닉해요.debug_assert!도 같은 일을 하지만 디버그 빌드에서만 동작해서, 릴리스 빌드에서는 비용이 전혀 없어요.
use std::cmp::{max, min};
enum Order { Pending, Shipped, Delivered, Cancelled,}
fn main() { let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!: 예/아니요 패턴 검사, |는 "또는" let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count(); println!("진행 중인 주문: {open}");
// min, max, clamp println!("최솟값 {}, 최댓값 {}", min(7, 3), max(7, 3)); let volume = 140; let safe = volume.clamp(0, 100); println!("볼륨 {volume}, 제한 후 {safe}");
// abs_diff: 얼마나 떨어져 있는지, 절대 음수가 아니에요 let (guess, secret): (u32, u32) = (30, 42); println!("정답까지 {} 차이 나요", guess.abs_diff(secret));
// dbg!는 stderr에 출력하고 값을 그대로 돌려줘요 let doubled = dbg!(safe * 2);
// assert!는 이 조건이 거짓이 되면 프로그램을 멈춰요 assert!(doubled <= 200, "두 배 볼륨이 너무 커요: {doubled}"); debug_assert!(safe <= 100); // 디버그 빌드에서만 검사해요 println!("모든 검사 통과");}실행 결과:
진행 중인 주문: 3최솟값 3, 최댓값 7볼륨 140, 제한 후 100정답까지 12 차이 나요모든 검사 통과위의 실행 결과에 dbg! 줄이 없는 이유는, dbg!가 stdout이 아니라 stderr에 출력하기 때문이에요. Playground에서는 “Standard Error” 칸에 나타나요.
[src/main.rs:28:19] safe * 2 = 200그래서 디버깅하는 동안 dbg!를 여기저기 뿌려 둬도 괜찮아요. 프로그램의 진짜 출력에 섞이지 않으니까요. 다만 배포하기 전에는 꼭 지워 주세요.
정렬에도 작은 도구들이 있어요. 자세한 건 정렬 페이지에서 다루고, 여기서는 짧게만 볼게요. sort_unstable은 sort보다 조금 빠르고, 단순한 숫자라면 이걸로 충분해요. sort_by_key는 필드 하나를 기준으로 정렬해요. binary_search는 정렬된 데이터에서 항목을 아주 빨리 찾아요. 사전을 첫 페이지부터 읽는 대신 가운데쯤을 펼쳐서 찾는 것처럼요.
struct Player { name: &'static str, score: u32,}
fn main() { // sort_unstable: 조금 더 빠르지만, 같은 값끼리는 자리가 바뀔 수 있어요 let mut ids = vec![42, 7, 19, 3, 25]; ids.sort_unstable(); println!("{ids:?}");
// binary_search: 찾으면 Ok(인덱스), 없으면 들어갈 자리 Err(인덱스) println!("19 찾기: {:?}", ids.binary_search(&19)); println!("20 찾기: {:?}", ids.binary_search(&20));
// sort_by_key: 필드 하나를 기준으로 정렬 let mut players = vec![ Player { name: "지아", score: 120 }, Player { name: "민준", score: 95 }, Player { name: "서연", score: 150 }, ]; players.sort_by_key(|p| p.score); let names: Vec<&str> = players.iter().map(|p| p.name).collect(); println!("낮은 점수부터: {names:?}");}실행 결과:
[3, 7, 19, 25, 42]19 찾기: Ok(2)20 찾기: Err(3)낮은 점수부터: ["민준", "지아", "서연"]Err(3)은 걱정할 실패가 아니에요. 20이 목록에 없다는 것, 그리고 목록을 정렬된 상태로 유지하려면 인덱스 3에 넣으면 된다는 것을 알려 주는 거예요.
이럴 때 써요: 이 도구들이 이미 해 주는 일을 작은 if나 반복문으로 직접 쓰고 있는 자신을 발견했을 때.
자주 하는 실수
섹션 제목: “자주 하는 실수”Default가 없는 타입에 mem::take 쓰기. mem::take는 그 자리에 무언가를 남겨야 하니까, 그 타입에서 “비어 있다”는 게 무엇인지 알아야 해요.
use std::mem;
struct Connection { host: String,}
struct Pool { active: Connection,}
impl Pool { fn reset(&mut self) -> Connection { mem::take(&mut self.active) // ❌ "비어 있는" Connection이 뭘까요? }}error[E0277]: the trait bound `Connection: Default` is not satisfied --> src/main.rs:13:19 |13 | mem::take(&mut self.active) // ❌ "비어 있는" Connection이 뭘까요? | --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection` | | | required by a bound introduced by this call |help: consider annotating `Connection` with `#[derive(Default)]`컴파일러는 “Connection이 Default 트레이트를 구현하지 않았다”고 알려 주고, #[derive(Default)]를 붙여 보라고 제안해요. 고치는 방법은 mem::replace(&mut self.active, Connection { host: "db2".to_string() })로 대신 넣을 값을 직접 정하거나, 필드를 Option<Connection>으로 저장하고 self.active.take()를 쓰는 거예요. #[derive(Default)]는 그 타입에 “빈” 값이 정말 말이 될 때만 붙이세요.
Result를 반환하는 함수에서 Option에 ? 쓰기. 둘은 자동으로 섞이지 않아요.
fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting?; // ❌ None에는 돌려줄 에러 메시지가 없어요 raw.parse::<u16>().map_err(|e| e.to_string())}error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result` --> src/main.rs:2:22 |1 | fn port_from(setting: Option<&str>) -> Result<u16, String> { | ---------------------------------------------------------- this function returns a `Result`2 | let raw = setting?; // ❌ None에는 돌려줄 에러 메시지가 없어요 | ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`“Result를 반환하는 함수에서는 Option에 ?를 쓸 수 없다”는 뜻이에요. 고치는 방법도 컴파일러가 이미 알려 주고 있어요. setting.ok_or("PORT가 설정되지 않았어요".to_string())?라고 쓰거나, 위 예제처럼 ok_or_else를 쓰면 돼요. 이 에러의 다른 형태는 E0277에서 볼 수 있어요.
정렬되지 않은 데이터에 dedup이나 binary_search를 기대하기. dedup은 바로 옆에 붙은 중복만 없애고, 정렬되지 않은 데이터에 binary_search를 쓰면 의미 없는 답이 나와요. 둘 다 컴파일 에러가 아니라서 놓치기 쉬워요. 먼저 정렬하세요.
chunks_exact의 남은 항목 잊어버리기. chunks_exact(3)을 for 반복문으로 돌리면, 마지막 1~2개 항목은 조용히 건너뛰어요. 그 항목들이 중요하다면 .remainder()를 처리하거나, 대신 chunks를 쓰세요.
연습 문제
섹션 제목: “연습 문제”1. 하루하루의 가격 변화
섹션 제목: “1. 하루하루의 가격 변화”가격 [100, 104, 98, 110, 107]이 있어요. 하루에서 다음 날로 넘어갈 때의 변화를 담은 Vec을 만들어 출력하고, 가장 크게 오른 값을 출력해 보세요. 힌트: windows(2)는 두 값을 작은 슬라이스 w로 주니까, 변화는 w[1] - w[0]이에요. 가장 큰 값은 .iter().max()로 찾을 수 있어요.
정답 보기
fn main() { let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect(); println!("변화: {changes:?}");
let biggest_rise = changes.iter().max(); println!("가장 큰 상승: {biggest_rise:?}");}실행 결과:
변화: [4, -6, 12, -3]가장 큰 상승: Some(12)가격이 다섯 개면 변화는 네 개예요. windows(2)는 한 칸 움직일 때마다 짝을 하나씩 만들기 때문이에요. max()가 Option을 반환하는 건, 빈 목록에는 가장 큰 값이 없기 때문이에요.
2. 인쇄 대기열 비우기
섹션 제목: “2. 인쇄 대기열 비우기”queue: Vec<String> 필드를 가진 Printer 구조체를 만들어 보세요. add(&mut self, file: &str) 메서드와, 대기 중인 파일을 모두 넘겨주고 대기열을 비우는 flush(&mut self) -> Vec<String> 메서드를 만들어요. 파일 두 개를 추가하고 flush한 다음, 무엇을 인쇄했는지와 남은 파일이 몇 개인지 출력하세요. 힌트: flush에는 &mut self밖에 없으니, std::mem::take를 쓰세요.
정답 보기
use std::mem;
struct Printer { queue: Vec<String>,}
impl Printer { fn add(&mut self, file: &str) { self.queue.push(file.to_string()); }
fn flush(&mut self) -> Vec<String> { mem::take(&mut self.queue) }}
fn main() { let mut printer = Printer { queue: Vec::new() }; printer.add("report.pdf"); printer.add("photo.png");
let jobs = printer.flush(); println!("작업 {}개 인쇄 중: {jobs:?}", jobs.len()); println!("대기열에 남은 작업: {}", printer.queue.len());}실행 결과:
작업 2개 인쇄 중: ["report.pdf", "photo.png"]대기열에 남은 작업: 0flush에서 self.queue만 그대로 돌려주려고 하면 E0507 에러가 나요. mem::take는 Vec을 밖으로 옮기고 빈 Vec을 남겨 두니, 복사가 전혀 일어나지 않아요.
3. 첫 번째로 쓸 수 있는 포트
섹션 제목: “3. 첫 번째로 쓸 수 있는 포트”u16으로 파싱되는 첫 번째 문자열을 돌려주는 함수 first_port(candidates: &[&str]) -> Option<u16>을 만들어 보세요. ["", "abc", "70000", "8080", "9090"]과 ["none", "-1"]로 각각 호출하고, 찾은 포트를 출력하세요. 없으면 3000을 출력해요. 힌트: s.parse::<u16>().ok()와 함께 find_map을 쓰고, 그다음 unwrap_or(3000)을 붙이세요.
정답 보기
fn first_port(candidates: &[&str]) -> Option<u16> { candidates.iter().find_map(|s| s.parse::<u16>().ok())}
fn main() { let from_config = ["", "abc", "70000", "8080", "9090"]; let all_bad = ["none", "-1"];
println!("사용할 포트: {}", first_port(&from_config).unwrap_or(3000)); println!("사용할 포트: {}", first_port(&all_bad).unwrap_or(3000));}실행 결과:
사용할 포트: 8080사용할 포트: 3000"70000"은 숫자처럼 보이지만 u16에 들어가지 않아요(가장 큰 값이 65535예요). 그래서 파싱이 실패하고 find_map은 다음으로 넘어가요. "8080"에서 멈추고, "9090"은 아예 보지도 않아요.
mem::take,mem::replace,Option::take는 그 자리에 무언가를 남겨 두는 방식으로&mut에서 값을 꺼내요.mem::swap은 두 값을 맞바꿔요.- 슬라이스에서
windows는 겹치는 이웃을,chunks는 묶음을,split_first/split_last는 “항목 하나와 나머지”를 줘요. peekable로는 앞을 미리 볼 수 있고,zip,enumerate,chain,fold,find_map,partition은 손으로 쓰던 반복문 상당수를 대신해요.unwrap_or_default,map_or,filter,ok_or같은Option도우미 덕분에 작은match를 일일이 쓰지 않아도 되고,?는Option에서도 동작해요.entry()에or_insert_with나and_modify를 붙이면 한 번의 조회로HashMap을 갱신할 수 있어요.- never 타입
!는 끝나지 않는 코드(panic!,return,continue,process::exit)의 타입이라서, 값이 와야 할 어떤 자리에든 들어갈 수 있어요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. items가 Vec일 때, mem::take(&mut self.items)는 그 자리에 무엇을 남길까요?
2. [1, 2, 3, 4].windows(2)는 무엇을 줄까요?
3. let x: i32 = match s { "a" => 1, _ => return };가 컴파일되는 이유는 무엇일까요?
4. 함수는 Result를 반환하는데, 가진 건 Option이에요. ? 앞에 무엇을 붙여야 할까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”std::mem— 표준 라이브러리 문서 (영어):take,replace,swap과 그 밖의 메모리 관련 함수를 예제와 함께 설명해요.- 슬라이스 메서드 — 표준 라이브러리 문서 (영어):
windows,chunks,split_first,binary_search를 비롯한 슬라이스의 모든 메서드가 있어요. Iterator트레이트 — 표준 라이브러리 문서 (영어):zip,peekable부터partition,find_map까지, 반복자 메서드의 전체 목록이에요.Option— 표준 라이브러리 문서 (영어):Option의 모든 도우미 메서드를 용도별로 묶어 정리해 뒀어요.- 발산 함수(Diverging functions) — Rust by Example (영어):
!를 반환하는 함수의 짧은 예제와, 그게 왜 유용한지 보여 줘요.