러스트 기본 한다 한다 몇년째인지 모르겠는데 일단 클로드 코드를 이용해서 기본적인 excercise를 만들어서 내가 풀어보고 냅다 마크다운 만들어달라고 함.
Ch.01 – 변수와 가변성 (Variables & Mutability)
Rust에서 변수를 다루는 기본 규칙을 알아봅니다.
- 변수는 기본적으로 불변(immutable) 입니다.
mut키워드를 붙이면 가변 변수가 됩니다.let으로 같은 이름을 다시 선언하면 섀도잉(shadowing) 이 됩니다 — 타입까지 바꿀 수 있습니다.const는 컴파일 타임 상수이며, 반드시 타입을 명시해야 합니다.
연습 1: 불변 변수 선언
값이 42인 변수를 선언하고 반환합니다.
pub fn declare_variable() -> i32 {
let answer = 42;
answer
}
let으로 선언한 변수는 기본적으로 불변입니다. answer = 100;처럼 재할당하면 컴파일 에러가 발생합니다.
연습 2: 가변 변수 (mut)
가변 변수를 10으로 선언한 뒤, 20으로 재할당하고 반환합니다.
pub fn mutable_variable() -> i32 {
let mut answer = 10;
answer = 20;
answer
}
mut 키워드를 붙이면 같은 타입의 값으로 재할당할 수 있습니다. Rust는 의도적으로 변경이 필요한 변수만 mut으로 표시하게 합니다.
연습 3: 섀도잉 (Shadowing)
변수 x를 정수 5로 선언한 뒤, 같은 이름으로 문자열 "five"로 섀도잉합니다.
pub fn shadowing() -> &'static str {
let x = 5;
let x = "five";
x
}
섀도잉은 mut과 다릅니다. let을 다시 쓰면 새로운 변수가 만들어지기 때문에 타입 자체가 바뀔 수 있습니다. 여기서 x는 i32에서 &str로 바뀌었습니다.
연습 4: 상수와 타입 명시
상수 MAX_SCORE를 100으로, 상수 PI를 3.14로 선언하고 튜플로 반환합니다.
pub fn constants_and_types() -> (i32, f64) {
const MAX_SCORE: i32 = 100;
const PI: f64 = 3.14;
(MAX_SCORE, PI)
}
const는 let과 달리 반드시 타입을 명시해야 하고, 컴파일 타임에 값이 결정됩니다. 관례적으로 SCREAMING_SNAKE_CASE로 이름을 짓습니다.
Ch.02 – 기본 타입 (Primitive Types)
Rust가 제공하는 기본 타입들을 살펴봅니다.
- 정수:
i8~i128(부호 있음),u8~u128(부호 없음) - 부동소수점:
f32,f64(기본값은f64) - 불리언:
bool—true또는false - 문자:
char— 유니코드 스칼라 값 (4바이트) - 튜플: 서로 다른 타입을 묶을 수 있는 고정 크기 컬렉션
- 배열: 같은 타입의 고정 크기 컬렉션
[T; N]
연습 1: 정수 타입
i8, i32, u64 세 가지 정수 타입을 선언하고 튜플로 반환합니다.
pub fn integer_types() -> (i8, i32, u64) {
let a: i8 = -1;
let b: i32 = 1000;
let c: u64 = 42;
(a, b, c)
}
i8은 -128~127, u64는 0~18,446,744,073,709,551,615 범위입니다. 필요한 범위에 맞는 타입을 골라 쓰면 됩니다.
연습 2: 부동소수점과 불리언
f64 값 3.14와 bool 값 true를 반환합니다.
pub fn float_and_bool() -> (f64, bool) {
let a: f64 = 3.14;
let b = true;
(a, b)
}
Rust에서 소수점이 있는 숫자 리터럴은 기본적으로 f64로 추론됩니다.
연습 3: char와 문자열 슬라이스
한글 char와 문자열 슬라이스를 반환합니다.
pub fn char_and_string() -> (char, &'static str) {
let a: char = '가';
let b = "안녕";
(a, b)
}
Rust의 char는 유니코드 스칼라 값이라 한글, 이모지 등도 담을 수 있습니다. &str는 UTF-8로 인코딩된 문자열의 참조입니다.
연습 4: 튜플 접근
튜플 (10, 20, 30)에서 두 번째 요소를 반환합니다.
pub fn tuple_access() -> i32 {
let tuple = (10, 20, 30);
tuple.1
}
튜플의 각 요소는 .0, .1, .2 … 인덱스로 접근합니다. 구조 분해(destructuring)도 가능합니다: let (a, b, c) = tuple;
연습 5: 배열과 합계
배열 [1, 2, 3, 4, 5]의 모든 요소 합을 반환합니다.
pub fn array_sum() -> i32 {
let arr = [1, 2, 3, 4, 5];
arr.iter().sum()
}
.iter()로 이터레이터를 만들고 .sum()으로 합산합니다. 배열은 크기가 타입의 일부이므로 [i32; 5]와 [i32; 3]은 서로 다른 타입입니다.
Ch.03 – 함수 (Functions)
Rust 함수의 기본 규칙을 익힙니다.
fn키워드로 선언합니다.- 매개변수의 타입은 반드시 명시해야 합니다.
- 반환 타입은
->뒤에 씁니다. - 함수 본문의 마지막 표현식(세미콜론 없이)이 반환값이 됩니다.
- 함수 자체를 다른 함수의 인자로 전달할 수 있습니다 (함수 포인터
fn(T) -> U).
연습 1: 두 수의 합
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
세미콜론 없이 a + b를 쓰면 이 표현식의 결과가 그대로 반환됩니다. return a + b;와 동일하지만, Rust에서는 마지막 표현식 형태가 관용적입니다.
연습 2: 짝수 판별
pub fn is_even(n: i32) -> bool {
n % 2 == 0
}
% 연산자(나머지)를 사용해 2로 나눈 나머지가 0인지 확인합니다. 비교 표현식 자체가 bool을 반환하므로 if문 없이 바로 반환할 수 있습니다.
연습 3: 팩토리얼 (재귀)
n!을 재귀로 계산합니다. 0! = 1, 5! = 120.
pub fn factorial(n: u32) -> u32 {
if n < 2 {
return 1
}
n * factorial(n - 1)
}
재귀 호출로 n * (n-1) * ... * 1을 계산합니다. u32이므로 음수 걱정 없이 n - 1이 안전합니다. 다만 큰 수에서는 오버플로우에 주의해야 합니다.
Tip: 반복문 버전도 가능합니다:
(1..=n).product()
연습 4: 함수 포인터
함수 f를 x에 두 번 적용합니다 — f(f(x)).
pub fn apply_twice(f: fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn(i32) -> i32는 함수 포인터 타입입니다. 일반 함수를 값처럼 전달할 수 있습니다. 클로저를 받으려면 Fn 트레이트를 사용합니다(Ch.14에서 다룹니다).
Ch.04 – 제어 흐름 (Control Flow)
조건문과 반복문, 그리고 Rust의 강력한 match 표현식을 다룹니다.
if/else는 표현식이므로 값을 반환할 수 있습니다.loop,while,for세 가지 반복문이 있습니다.match는 패턴 매칭으로, 모든 경우를 빠짐없이 처리해야 합니다.for i in 0..n은 범위(Range) 기반 반복입니다.
연습 1: 절대값
if/else를 사용해 절대값을 반환합니다.
pub fn abs_value(x: i32) -> i32 {
if x < 0 {
-x
} else {
x
}
}
Rust에서 if는 표현식이므로 마지막 값이 반환됩니다. 별도의 return이 필요 없습니다.
연습 2: FizzBuzz
3과 5의 배수 여부에 따라 다른 문자열을 반환하는 고전적인 문제입니다.
pub fn fizzbuzz(n: u32) -> String {
if n % 15 == 0 {
String::from("FizzBuzz")
} else if n % 5 == 0 {
String::from("Buzz")
} else if n % 3 == 0 {
String::from("Fizz")
} else {
n.to_string()
}
}
15의 배수(= 3과 5의 공배수)를 먼저 검사해야 합니다. 순서가 바뀌면 "Fizz"나 "Buzz"가 먼저 매칭되어 버립니다.
연습 3: 범위 합계
start부터 end까지(양 끝 포함) 정수의 합을 반환합니다.
pub fn sum_range(start: i32, end: i32) -> i32 {
let mut sum = 0;
for i in start..=end {
sum += i
}
sum
}
start..=end는 양 끝을 포함하는 inclusive range입니다. start..end는 end를 포함하지 않습니다.
Tip: 이터레이터로 더 간결하게 쓸 수 있습니다:
(start..=end).sum()
연습 4: 모음 세기
문자열에서 영어 모음(a, e, i, o, u)의 개수를 셉니다. 대소문자를 모두 포함합니다.
pub fn count_vowels(s: &str) -> usize {
let vowels = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
let mut vowel_count: usize = 0;
for ch in s.chars() {
if vowels.contains(&ch) {
vowel_count += 1;
}
}
vowel_count
}
.chars()로 문자 단위 순회 후, 배열의 .contains()로 모음 여부를 판별합니다.
Tip: 이터레이터 체이닝으로도 작성할 수 있습니다:
s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()
연습 5: 콜라츠 추측
n이 1이 될 때까지의 단계 수를 반환합니다. 짝수이면 n/2, 홀수이면 3n+1.
pub fn collatz_steps(n: u32) -> u32 {
let mut number = n;
let mut count = 0;
while number != 1 {
if number % 2 == 0 {
number /= 2;
} else {
number = 3 * number + 1;
}
count += 1;
}
count
}
예시: 6 → 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1 = 8단계. 모든 자연수가 결국 1에 도달하는지는 아직 증명되지 않은 유명한 수학 문제입니다.
Ch.05 – 소유권 (Ownership)
Rust의 가장 핵심적인 개념입니다. 가비지 컬렉터 없이 메모리 안전성을 보장하는 비결이죠.
- 각 값은 하나의 소유자(owner) 만 가집니다.
- 소유자가 스코프를 벗어나면 값은 자동으로 해제(drop) 됩니다.
- 변수를 다른 변수에 대입하면 소유권이 이동(move) 됩니다.
.clone()으로 깊은 복사를 할 수 있습니다.- 스택에 저장되는 타입(
i32등)은Copy트레이트로 자동 복사됩니다.
연습 1: 소유권 이동과 반환
String의 소유권을 받아서 그대로 반환합니다.
pub fn take_and_return(s: String) -> String {
s
}
함수에 String을 넘기면 소유권이 이동합니다. 호출 후 원래 변수는 사용할 수 없고, 반환값을 다시 받아야 합니다.
let s = String::from("hello");
let s2 = take_and_return(s);
// s는 여기서 더 이상 사용 불가 — 소유권이 이동됨
// s2로 다시 받았으므로 s2를 사용
연습 2: 첫 번째 단어의 끝 위치
문자열에서 첫 공백의 위치를 찾습니다. 공백이 없으면 전체 길이를 반환합니다.
pub fn first_word_end(s: &String) -> usize {
let mut find = 0;
for ch in s.chars() {
if ch == ' ' {
break;
}
find += 1;
}
find
}
&String으로 빌려 받아 소유권을 가져가지 않습니다. .chars()로 문자 단위 순회하며 공백을 찾습니다.
Tip: 더 간결한 방법도 있습니다:
s.find(' ').unwrap_or(s.len())
연습 3: clone과 수정
원본을 복제한 뒤 " world"를 추가합니다. 원본은 변경되지 않습니다.
pub fn clone_and_modify(s: &String) -> String {
let mut result = String::from(s);
result += " world";
result
}
String::from(s)로 새로운 String을 만들어 수정합니다. &String(불변 참조)으로 받았으므로 원본에는 영향이 없습니다.
연습 4: Move semantics
Vec을 생성하고, 다른 변수로 이동한 뒤, 값을 추가하고 반환합니다.
pub fn move_semantics() -> Vec<i32> {
let mut result: Vec<i32> = vec![];
result.extend(vec![1, 2]);
let mut result2 = result;
// result는 여기서부터 사용 불가 — 소유권이 result2로 이동됨
result2.push(3);
result2
}
let mut result2 = result; 시점에서 소유권이 이동합니다. 이후 result를 사용하면 컴파일 에러가 발생합니다. 힙에 할당된 데이터(Vec, String 등)는 대입 시 복사되지 않고 이동합니다.
Ch.06 – 참조와 빌림 (References & Borrowing)
소유권을 넘기지 않고 값을 사용하는 방법입니다.
&T— 불변 참조(shared reference): 읽기만 가능, 동시에 여러 개 가능&mut T— 가변 참조(mutable reference): 수정 가능, 동시에 하나만 가능&[T],&str— 슬라이스: 컬렉션의 일부분을 참조하는 뷰- 참조는 항상 유효한 데이터를 가리켜야 합니다 (댕글링 참조 금지)
연습 1: 불변 참조
문자열 슬라이스의 길이를 반환합니다. 소유권을 가져가지 않습니다.
pub fn string_length(s: &str) -> usize {
s.len()
}
&str로 빌려 받으므로 호출 후에도 원본을 계속 사용할 수 있습니다. .len()은 바이트 수를 반환합니다 — 한글 "안녕"은 6바이트(UTF-8에서 한 글자당 3바이트)입니다.
연습 2: 가변 참조
가변 참조를 통해 벡터에 값을 추가합니다.
pub fn push_value(v: &mut Vec<i32>, val: i32) {
v.push(val)
}
&mut Vec<i32>로 받으면 벡터를 직접 수정할 수 있습니다. 가변 참조는 동시에 하나만 존재할 수 있으며, 불변 참조와도 동시에 존재할 수 없습니다.
let mut v = vec![1, 2, 3];
push_value(&mut v, 4); // &mut로 빌려줌
// v는 이제 [1, 2, 3, 4]
연습 3: 슬라이스에서 최대값
슬라이스를 순회하며 가장 큰 값을 찾습니다.
pub fn largest(list: &[i32]) -> i32 {
let mut max = list[0];
for item in list {
if *item > max {
max = *item;
}
}
max
}
&[i32]는 슬라이스 타입으로, Vec<i32>나 배열의 일부/전체를 참조할 수 있습니다. for item in list에서 item은 &i32 타입이므로 *item으로 역참조합니다.
연습 4: 값 교환 (swap)
두 가변 참조의 값을 서로 교환합니다.
pub fn swap_values(a: &mut i32, b: &mut i32) {
let temp = *a;
*a = *b;
*b = temp;
}
임시 변수를 사용한 고전적인 swap 패턴입니다. *a로 역참조하여 값을 읽고, *a = ...로 값을 씁니다.
Tip: 표준 라이브러리의
std::mem::swap(a, b)를 사용해도 됩니다.
Ch.07 – 구조체 (Structs)
관련된 데이터를 하나로 묶는 사용자 정의 타입입니다.
struct키워드로 정의합니다.impl블록으로 메서드를 추가합니다.&self— 불변 참조로 호출,&mut self— 가변 참조로 호출- 튜플 구조체
struct Color(u8, u8, u8)— 필드에 이름이 없는 간결한 형태
연습 1: 구조체 정의와 생성
Rectangle 구조체를 정의하고 생성 함수를 작성합니다.
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
pub fn create_rect(width: f64, height: f64) -> Rectangle {
Rectangle { width, height }
}
필드 이름과 매개변수 이름이 같으면 width: width 대신 width로 축약할 수 있습니다 (field init shorthand).
연습 2: 메서드 — 넓이 계산
impl 블록 안에 메서드를 정의합니다. &self로 구조체를 빌려 사용합니다.
impl Rectangle {
pub fn area(&self) -> f64 {
self.width * self.height
}
}
&self는 self: &Self의 축약형입니다. 소유권을 가져가지 않으므로 호출 후에도 구조체를 계속 사용할 수 있습니다.
연습 3: 정사각형 판별
impl Rectangle {
pub fn is_square(&self) -> bool {
self.width == self.height
}
}
연습 4: 포함 가능 여부
self가 other를 완전히 포함할 수 있는지 판별합니다.
impl Rectangle {
pub fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
메서드의 매개변수로 같은 타입의 참조를 받을 수 있습니다. other는 불변 참조로 빌려 받습니다.
연습 5: 튜플 구조체 — 색상 믹싱
두 Color의 각 RGB 채널 평균을 계산합니다.
pub struct Color(pub u8, pub u8, pub u8);
pub fn mix(c1: &Color, c2: &Color) -> Color {
let Color(r1, g1, b1) = c1;
let Color(r2, g2, b2) = c2;
fn arithmetic_mean(a: &u8, b: &u8) -> u8 {
((*a as u16 + *b as u16) / 2) as u8
}
Color(
arithmetic_mean(r1, r2),
arithmetic_mean(g1, g2),
arithmetic_mean(b1, b2),
)
}
u8(0~255)끼리 더하면 오버플로우가 발생할 수 있으므로, u16으로 캐스팅한 뒤 계산하고 다시 u8로 변환합니다. 튜플 구조체는 let Color(r, g, b) = c; 형태로 구조 분해가 가능합니다.
Ch.08 – 열거형과 패턴 매칭 (Enums & Pattern Matching)
열거형과 match는 Rust의 가장 강력한 기능 중 하나입니다.
enum— 여러 가지 변형(variant) 중 하나의 값을 가지는 타입- 각 변형은 데이터를 포함할 수 있습니다: Unit, Tuple, Struct 형태
match— 모든 변형을 빠짐없이 처리해야 하는 패턴 매칭Option<T>— 값이 있을 수도(Some), 없을 수도(None) 있는 내장 열거형
연습 1: 방향 판별
수평 방향(East, West)이면 true를 반환합니다.
pub enum Direction {
North,
South,
East,
West,
}
pub fn is_horizontal(d: &Direction) -> bool {
match d {
Direction::East | Direction::West => true,
_ => false
}
}
| 패턴으로 여러 변형을 하나의 분기에서 처리할 수 있습니다. _는 나머지 모든 경우를 잡는 와일드카드입니다.
Tip:
matches!매크로를 사용하면 더 간결합니다:matches!(d, Direction::East | Direction::West)
연습 2: 데이터 없는 열거형 — 동전 값
pub enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
pub fn coin_value(c: &Coin) -> u32 {
match c {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25
}
}
match는 모든 변형을 처리해야 합니다. 하나라도 빠지면 컴파일 에러가 발생합니다. 이것이 Rust가 버그를 컴파일 타임에 잡는 방법 중 하나입니다.
연습 3: 데이터를 포함하는 열거형 — 도형 넓이
pub enum Shape {
Circle(f64), // 반지름
Rect(f64, f64), // 가로, 세로
}
pub fn shape_area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect(width, height) => width * height
}
}
패턴 매칭에서 변형 안의 데이터를 바로 변수로 바인딩할 수 있습니다. Shape::Circle(r)에서 r은 반지름 값에 바인딩됩니다.
연습 4: match 가드 (guard)
숫자를 설명하는 문자열을 반환합니다.
pub fn describe_number(n: i32) -> &'static str {
match n {
x if x < 0 => "음수",
0 => "영",
_ => "양수",
}
}
if x < 0 부분이 match 가드입니다. 패턴 매칭 후 추가 조건을 검사할 수 있습니다. 0은 리터럴 패턴으로 직접 매칭하고, 나머지는 _로 처리합니다.
연습 5: Option 반환 — 안전한 나눗셈
b가 0이면 None, 아니면 Some(a / b)를 반환합니다.
pub fn safe_divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
Option<T>은 Rust에서 null 대신 사용하는 타입입니다. 값이 없을 수 있는 상황을 타입 시스템으로 명시하여, null pointer exception 같은 런타임 에러를 방지합니다.
Ch.09 – 컬렉션 (Collections)
Rust의 주요 컬렉션 타입을 다룹니다.
Vec<T>— 크기가 가변인 배열.push,pop,remove,len등String— UTF-8 문자열.+연산자,format!,push_str등HashMap<K, V>— 키-값 쌍의 해시 맵.insert,get,entry등HashSet<T>— 중복 없는 값의 집합
use std::collections::{HashMap, HashSet};
연습 1: Vec 기본 조작
빈 Vec에 1, 2, 3을 넣고, 두 번째 요소를 제거합니다.
pub fn vec_operations() -> Vec<i32> {
let mut result: Vec<i32> = vec![];
result.push(1);
result.push(2);
result.push(3);
result.remove(1); // 인덱스 1의 요소(2)를 제거
result // [1, 3]
}
.remove(index)는 해당 인덱스의 요소를 제거하고, 뒤의 요소들을 앞으로 당깁니다. 반환값은 제거된 요소입니다.
연습 2: String 연결
"Hello", " ", "World"를 하나의 String으로 연결합니다.
pub fn string_operations() -> String {
let hello = "Hello";
let space = " ";
let world = "World";
format!("{}{}{}", hello, space, world)
}
format! 매크로는 소유권을 가져가지 않아서 편리합니다. + 연산자를 쓰면 왼쪽의 String 소유권이 이동하므로 주의가 필요합니다.
연습 3: HashMap으로 단어 세기
텍스트에서 각 단어의 출현 횟수를 셉니다.
pub fn word_count(text: &str) -> HashMap<String, usize> {
let words = text.split_whitespace();
let mut count_map: HashMap<String, usize> = HashMap::new();
for word in words {
*count_map.entry(String::from(word)).or_insert(0) += 1;
}
count_map
}
핵심은 entry().or_insert() 패턴입니다:
entry(key)— 해당 키의 엔트리를 가져옵니다or_insert(0)— 키가 없으면 0을 삽입하고, 값의 가변 참조(&mut usize)를 반환합니다*참조 += 1— 역참조하여 직접 값을 증가시킵니다
다른 언어처럼 get → +1 → put할 필요 없이 한 번에 처리합니다.
연습 4: 중복 제거 (순서 유지)
슬라이스에서 중복을 제거하되 원래 순서를 유지합니다.
pub fn unique_elements(v: &[i32]) -> Vec<i32> {
let mut found: Vec<i32> = vec![];
for el in v {
if !found.contains(el) {
found.push(*el)
}
}
found
}
.contains()로 이미 추가된 값인지 확인합니다.
Tip:
HashSet을 사용하면 조회가 O(1)이 되어 성능이 좋아집니다:let mut seen = HashSet::new(); v.iter().filter(|x| seen.insert(**x)).copied().collect()
연습 5: 길이별 그룹화
단어들을 길이에 따라 HashMap으로 그룹화합니다.
pub fn group_by_length(words: &[&str]) -> HashMap<usize, Vec<String>> {
let mut result: HashMap<usize, Vec<String>> = HashMap::new();
for word in words {
let length = word.len();
result.entry(length)
.or_insert(vec![])
.push(word.to_string())
}
result
}
entry().or_insert(vec![])로 해당 길이의 벡터가 없으면 빈 벡터를 만들고, .push()로 바로 추가합니다. entry API의 강점이 잘 드러나는 패턴입니다.
Ch.10 – 에러 처리 (Error Handling)
Rust는 예외(exception) 대신 Option과 Result 타입으로 에러를 처리합니다.
Option<T>— 값이 있으면Some(T), 없으면NoneResult<T, E>— 성공이면Ok(T), 실패이면Err(E)?연산자 —Result/Option의 에러를 간결하게 상위로 전파unwrap_or,map,and_then등 조합 메서드로 우아하게 처리
연습 1: Option — 첫 번째 짝수 찾기
슬라이스에서 첫 번째 짝수를 찾아 Option으로 반환합니다.
pub fn find_first_even(v: &[i32]) -> Option<i32> {
for n in v {
if n % 2 == 0 {
return Some(*n);
}
}
None
}
찾으면 Some(값), 못 찾으면 None을 반환합니다. 호출하는 쪽에서 match나 if let으로 처리합니다.
Tip: 이터레이터로도 가능합니다:
v.iter().find(|n| *n % 2 == 0).copied()
연습 2: Result — 문자열 파싱
문자열을 i32로 파싱하고, 실패 시 에러 메시지를 포함한 Err를 반환합니다.
pub fn parse_number(s: &str) -> Result<i32, String> {
str::parse::<i32>(s).map_err(|_| format!("파싱 실패: {}", s))
}
.map_err()는 Err 값을 변환합니다. 원래 parse()가 반환하는 ParseIntError를 사람이 읽기 좋은 String으로 바꿉니다.
parse_number("42") // Ok(42)
parse_number("abc") // Err("파싱 실패: abc")
연습 3: Result — 리스트 나눗셈
슬라이스의 모든 요소를 divisor로 나눈 결과를 반환합니다.
pub fn divide_list(nums: &[f64], divisor: f64) -> Result<Vec<f64>, String> {
if divisor == 0.0 {
Err(String::from("0으로 나눌 수 없습니다"))
} else {
Ok(nums.iter().map(|n| n / divisor).collect())
}
}
0으로 나누기를 먼저 검사하고, 성공 시 Ok(변환된_벡터)를 반환합니다. 에러 체크를 앞에서 하는 early return 패턴입니다.
연습 4: unwrap_or — 기본값 처리
두 Option 값의 합을 반환합니다. None은 0으로 취급합니다.
pub fn unwrap_or_default_chain(a: Option<i32>, b: Option<i32>) -> i32 {
a.unwrap_or(0) + b.unwrap_or(0)
}
unwrap_or(기본값)은 Some이면 안의 값을, None이면 기본값을 반환합니다. unwrap()과 달리 panic하지 않아 안전합니다.
연습 5: ? 연산자 — 에러 전파 체인
문자열을 파싱 → 2를 곱함 → 다시 String으로 변환합니다.
pub fn safe_chain(input: &str) -> Result<String, String> {
let n = input.parse::<i32>().map_err(|e| e.to_string())?;
Ok((n * 2).to_string())
}
? 연산자는 Ok면 값을 꺼내고, Err면 즉시 함수를 종료하며 에러를 반환합니다. match로 일일이 처리하는 것보다 훨씬 간결합니다.
// ? 없이 작성하면:
match input.parse::<i32>() {
Ok(v) => Ok((2 * v).to_string()),
Err(e) => Err(e.to_string())
}
Ch.11 – 트레이트 (Traits)
트레이트는 타입이 구현해야 하는 동작을 정의합니다. 다른 언어의 인터페이스와 유사합니다.
trait— 메서드 시그니처(또는 기본 구현)를 정의impl Trait for Type— 특정 타입에 트레이트를 구현- 기본 구현 — 트레이트에서 기본 동작을 제공, 타입이 오버라이드 가능
- 트레이트 바운드 —
fn foo<T: MyTrait>(x: T)형태로 제네릭에 제약
연습 1: 커스텀 트레이트 구현
Greet 트레이트를 정의하고 Korean 구조체에 구현합니다.
pub trait Greet {
fn greet(&self) -> String;
}
pub struct Korean {
pub name: String,
}
impl Greet for Korean {
fn greet(&self) -> String {
format!("안녕하세요, {}님!", self.name)
}
}
트레이트를 구현하면 해당 타입에서 트레이트의 메서드를 호출할 수 있습니다:
let k = Korean { name: "철수".to_string() };
println!("{}", k.greet()); // "안녕하세요, 철수님!"
연습 2: 표준 트레이트 구현 — Display
fmt::Display를 구현하면 println!("{}", point) 형태로 출력할 수 있습니다.
use std::fmt;
pub struct Point {
pub x: f64,
pub y: f64,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
Display는 사용자에게 보여줄 형태, Debug({:?})는 개발자용 디버그 형태입니다. Debug는 #[derive(Debug)]로 자동 구현할 수 있지만, Display는 직접 구현해야 합니다.
연습 3: 기본 구현과 오버라이드
Summary 트레이트에 기본 구현을 제공하고, 타입별로 오버라이드합니다.
pub trait Summary {
fn summarize(&self) -> String {
String::from("(자세히 보기...)") // 기본 구현
}
}
pub struct Article {
pub title: String,
pub content: String,
}
pub struct Tweet {
pub username: String,
pub text: String,
}
// Article은 기본 구현을 그대로 사용
impl Summary for Article {}
// Tweet은 오버라이드
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.text)
}
}
article.summarize() // "(자세히 보기...)"
tweet.summarize() // "@rustlang: Rust is great!"
impl Summary for Article {}처럼 빈 블록을 쓰면 기본 구현이 사용됩니다.
연습 4: 트레이트 바운드
PartialOrd 바운드를 사용해 슬라이스에서 가장 큰 요소를 찾습니다.
pub fn largest_with_trait<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list.iter().skip(1) {
if item > largest {
largest = item
}
}
largest
}
T: PartialOrd는 "T는 비교 가능해야 한다"는 제약입니다. 이 바운드가 없으면 item > largest에서 컴파일 에러가 발생합니다.
참조(&T)를 반환하므로 Copy나 Clone 바운드가 필요 없습니다.
Ch.12 – 제네릭 (Generics)
코드 중복 없이 여러 타입에 대해 동작하는 코드를 작성합니다.
fn foo<T>(x: T)—T는 타입 매개변수struct Pair<T>— 제네릭 구조체impl<T: Trait> Pair<T>— 특정 트레이트를 만족하는T에 대해서만 메서드 구현- Rust의 제네릭은 단형화(monomorphization) 로 컴파일 타임에 구체 타입으로 변환되어 런타임 비용이 없습니다
연습 1: 제네릭 함수
슬라이스의 첫 번째 요소를 Option으로 반환합니다.
pub fn first<T>(list: &[T]) -> Option<&T> {
if list.is_empty() {
None
} else {
Some(&list[0])
}
}
<T>는 어떤 타입이든 받을 수 있다는 뜻입니다. i32, String, 커스텀 구조체 등 모든 타입에 대해 동작합니다.
Tip: 표준 라이브러리의
list.first()와 동일한 기능입니다.
연습 2: 제네릭 구조체
같은 타입의 두 값을 가지는 Pair를 정의합니다.
pub struct Pair<T> {
pub first: T,
pub second: T,
}
impl<T> Pair<T> {
pub fn new(first: T, second: T) -> Pair<T> {
Pair { first, second }
}
}
impl<T>의 <T>는 "이 impl 블록이 제네릭이다"라는 선언입니다. 모든 T에 대해 new를 사용할 수 있습니다.
연습 3: 조건부 메서드 구현
PartialOrd + Copy를 만족하는 T에 대해서만 max 메서드를 제공합니다.
impl<T: PartialOrd + Copy> Pair<T> {
pub fn max(&self) -> T {
if self.first > self.second {
self.first
} else {
self.second
}
}
}
T: PartialOrd + Copy는 두 가지 제약입니다:
PartialOrd— 비교 가능 (>,<연산)Copy— 값을 복사하여 반환 가능
Pair<String>은 Copy가 아니므로 .max()를 호출할 수 없습니다. 컴파일 타임에 잡아줍니다.
연습 4: 정렬된 슬라이스 병합
두 정렬된 슬라이스를 하나의 정렬된 Vec으로 병합합니다 (Merge Sort의 merge 단계).
pub fn merge_sorted<T: Ord + Clone>(a: &[T], b: &[T]) -> Vec<T> {
let mut a_idx = 0;
let mut b_idx = 0;
let mut result: Vec<T> = vec![];
while a_idx < a.len() && b_idx < b.len() {
if a[a_idx] < b[b_idx] {
result.push(a[a_idx].clone());
a_idx += 1;
} else {
result.push(b[b_idx].clone());
b_idx += 1;
}
}
// 남은 요소 추가
while a_idx < a.len() {
result.push(a[a_idx].clone());
a_idx += 1;
}
while b_idx < b.len() {
result.push(b[b_idx].clone());
b_idx += 1;
}
result
}
두 인덱스로 양쪽을 동시에 순회하며 작은 값부터 추가합니다. Ord는 전순서(total order) 비교, Clone은 슬라이스에서 값을 복제하기 위해 필요합니다.
Ch.13 – 라이프타임 (Lifetimes)
라이프타임은 참조가 유효한 범위를 나타냅니다. Rust가 컴파일 타임에 메모리 안전성을 보장하는 핵심 장치입니다.
'a— 라이프타임 매개변수. 여러 참조의 유효 범위를 연결합니다- 컴파일러가 자동으로 추론할 수 없을 때 명시적으로 지정해야 합니다
- 핵심 규칙: 출력 참조의 라이프타임은 반드시 입력 참조 중 하나와 연결되어야 합니다
- 구조체가 참조를 필드로 가지면 라이프타임 매개변수가 필요합니다
연습 1: 더 긴 문자열 반환
두 문자열 슬라이스 중 더 긴 것을 반환합니다. 길이가 같으면 첫 번째를 반환합니다.
pub fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
'a가 필요한 이유: 반환값이 s1일 수도, s2일 수도 있기 때문에 컴파일러가 어떤 참조의 수명을 따라야 하는지 알 수 없습니다. 'a로 "두 입력 중 더 짧은 수명만큼 유효하다"고 명시합니다.
// 라이프타임이 없으면 컴파일 에러:
// fn longer(s1: &str, s2: &str) -> &str // ← 어느 참조를 따르지?
연습 2: 첫 번째 단어 슬라이스
문자열에서 첫 공백 전까지의 슬라이스를 반환합니다.
pub fn first_word_slice<'a>(s: &'a str) -> &'a str {
let first_space_idx = s.find(' ');
match first_space_idx {
None => s,
Some(idx) => &s[..idx]
}
}
입력이 하나뿐이면 Rust의 라이프타임 생략 규칙(elision rules) 에 의해 'a를 생략할 수 있습니다:
// 이것도 동일하게 동작합니다:
pub fn first_word_slice(s: &str) -> &str { ... }
하지만 학습 목적으로 명시하는 것이 라이프타임 이해에 도움됩니다.
연습 3: 참조를 포함하는 구조체
구조체가 참조를 필드로 가지면 라이프타임을 명시해야 합니다.
pub struct Excerpt<'a> {
pub text: &'a str,
}
pub fn make_excerpt<'a>(s: &'a str) -> Excerpt<'a> {
let first_dot_idx = s.find('.');
Excerpt {
text: match first_dot_idx {
None => s,
Some(idx) => &s[..idx]
}
}
}
Excerpt<'a>는 "text 필드가 라이프타임 'a만큼 유효하다"는 의미입니다. Excerpt가 원본 문자열보다 오래 살 수 없다는 것을 컴파일러가 보장합니다.
let text = String::from("Rust is great. It is fast.");
let excerpt = make_excerpt(&text);
assert_eq!(excerpt.text, "Rust is great");
// text가 drop되면 excerpt.text도 사용 불가
연습 4: 가장 긴 공통 접두사
두 문자열의 공통 접두사를 슬라이스로 반환합니다.
pub fn longest_common_prefix<'a>(s1: &'a str, s2: &'a str) -> &'a str {
let mut idx = 0;
let mut s1_chars = s1.chars();
let mut s2_chars = s2.chars();
loop {
let (left, right) = (s1_chars.next(), s2_chars.next());
match (left, right) {
(Some(x), Some(y)) if x == y => {
idx += x.len_utf8(); // 바이트 단위로 계산
},
_ => break
}
}
&s1[..idx]
}
두 이터레이터를 동시에 순회하며 문자가 같은 동안 바이트 인덱스를 누적합니다. &s1[..idx]로 원본의 슬라이스를 반환하므로 새로운 String을 만들 필요가 없습니다.
longest_common_prefix("interstellar", "internet") // "inter"
longest_common_prefix("abc", "xyz") // ""
longest_common_prefix("rust", "rusty") // "rust"
주의: 바이트 슬라이싱(
&s[..idx])에 문자 개수 대신 바이트 수를 사용해야 합니다. ASCII만 다룬다면 문자 수 = 바이트 수이지만, 멀티바이트 문자(한글 등)에서는char::len_utf8()으로 바이트 수를 정확히 계산해야 합니다.
Ch.14 – 클로저와 이터레이터 (Closures & Iterators)
Rust의 함수형 프로그래밍 기능을 다룹니다.
- 클로저 —
|x| x + 1형태의 익명 함수. 환경의 변수를 캡처할 수 있습니다Fn— 불변 캡처FnMut— 가변 캡처FnOnce— 소유권 캡처 (한 번만 호출)
- 이터레이터 —
.iter(),.into_iter(),.iter_mut()로 생성 map,filter,fold,collect,sum등의 어댑터를 체이닝Iterator트레이트를 직접 구현하여 커스텀 이터레이터 생성 가능
연습 1: 클로저를 인자로 받기
클로저 f를 x에 적용한 결과를 반환합니다.
pub fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
F: Fn(i32) -> i32는 "i32를 받아 i32를 반환하는 클로저"라는 트레이트 바운드입니다. Ch.03의 함수 포인터(fn(i32) -> i32)와 달리, Fn 트레이트는 환경을 캡처하는 클로저도 받을 수 있습니다.
let offset = 10;
apply(|x| x + offset, 5) // 15 — offset을 캡처
연습 2: map으로 변환
슬라이스의 모든 요소를 2배로 만듭니다.
pub fn double_all(v: &[i32]) -> Vec<i32> {
v.iter().map(|x| 2 * x).collect()
}
이터레이터 체이닝의 기본 패턴:
.iter()— 이터레이터 생성 (각 요소의 참조&i32).map(|x| ...)— 각 요소를 변환.collect()— 결과를Vec으로 수집
이터레이터는 지연 평가(lazy) 입니다. .collect()를 호출할 때까지 실제 연산이 수행되지 않습니다.
연습 3: map + sum
각 요소를 제곱한 뒤 합을 구합니다.
pub fn sum_of_squares(v: &[i32]) -> i32 {
v.iter().map(|x| x * x).sum()
}
[1, 2, 3] → [1, 4, 9] → 14
.sum()은 Iterator 트레이트의 소비 어댑터(consuming adapter) 로, 이터레이터를 소모하며 합계를 반환합니다.
연습 4: filter + map 체이닝
짝수만 골라내고 문자열로 변환합니다.
pub fn filter_and_transform(v: &[i32]) -> Vec<String> {
v.iter()
.filter(|x| **x % 2 == 0)
.map(|x| x.to_string())
.collect()
}
[1, 2, 3, 4] → filter → [2, 4] → map → ["2", "4"]
.filter()의 클로저는 &&i32를 받습니다 (이터레이터가 &i32를 주고, filter가 다시 참조를 전달). 그래서 **x로 두 번 역참조합니다.
연습 5: 커스텀 이터레이터
Iterator 트레이트를 직접 구현하여 1부터 5까지 세는 카운터를 만듭니다.
pub struct Counter {
count: u32,
}
impl Counter {
pub fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count > 5 {
None
} else {
Some(self.count)
}
}
}
pub fn custom_counter() -> Vec<u32> {
Counter::new().collect() // [1, 2, 3, 4, 5]
}
Iterator 트레이트는 next() 하나만 구현하면 됩니다. map, filter, sum, collect 등 수십 개의 메서드가 자동으로 제공됩니다.
// 커스텀 이터레이터를 다른 어댑터와 조합
let sum: u32 = Counter::new().sum(); // 15
let doubled: Vec<u32> = Counter::new()
.map(|x| x * 2)
.collect(); // [2, 4, 6, 8, 10]
let evens: Vec<u32> = Counter::new()
.filter(|x| x % 2 == 0)
.collect(); // [2, 4]
대충 보니 빨간색으로 한 애들이 좀 생소한 애들이고 열거형/패턴매칭은 그거보단 쉬운데 많이 쓰는 애 인거 같음
이건 내가 대충 짜본 UP&DOWN 게임. 내가 15년전에 C를 처음 배웠을 때 맨처음으로 한 게 이거였음.
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use rand::RngExt;
pub fn updown_game() {
let answer = generate_answer();
loop {
match get_user_guess() {
Ok(num) if (1..=100).contains(&num) => {
if num == answer {
println!("Congratulations! You guessed the correct number.");
break;
} else if num < answer {
println!("Too low! Try again.");
} else {
println!("Too high! Try again.");
}
}
Ok(_) => println!("Invalid input. Please enter a number between 1 and 100."),
Err(err) => eprintln!("{}", err),
}
}
}
enum GetUserGuessError {
IoError(std::io::Error),
ParseIntError(std::num::ParseIntError),
}
impl Error for GetUserGuessError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
GetUserGuessError::IoError(err) => Some(err),
GetUserGuessError::ParseIntError(err) => Some(err),
}
}
}
impl From<std::io::Error> for GetUserGuessError {
fn from(err: std::io::Error) -> Self {
GetUserGuessError::IoError(err)
}
}
impl From<std::num::ParseIntError> for GetUserGuessError {
fn from(err: std::num::ParseIntError) -> Self {
GetUserGuessError::ParseIntError(err)
}
}
impl Debug for GetUserGuessError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
GetUserGuessError::IoError(err) => write!(f, "IoError: {}", err),
GetUserGuessError::ParseIntError(err) => write!(f, "ParseIntError: {}", err),
}
}
}
impl Display for GetUserGuessError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
(self as &dyn Debug).fmt(f)
}
}
fn get_user_guess() -> Result<u32, GetUserGuessError> {
println!("Enter your guess: ");
let mut guess = String::new();
std::io::stdin().read_line(&mut guess).map_err(GetUserGuessError::IoError)?;
let result = guess.trim().parse::<u32>().map_err(GetUserGuessError::ParseIntError)?;
Ok(result)
}
fn generate_answer() -> u32 {
rand::rng().random_range(1..=100)
}
#[cfg(test)]
mod updown_test {
use super::*;
#[test]
fn test_generate_answer() {
let answer = generate_answer();
assert!((1..=100).contains(&answer));
}
}
그래도 트레이트나 패턴매칭 같은건 잘 들어가있음.
그리고 생소한게 ?연산자인데 Result<T, E>나 Option<T>에 붙여서 쓸 수 있다.
반환형이 Result나 Option일 때 같은 Result나 Option의 뒤에 붙어서 Err나 None일때 그 즉시 반환을 해버린다.
섞어서 쓰려면 서로 변환 후 사용해야하는데 Option -> Result는 ok_or(Err), Result -> Option은 ok()를 사용한다고 한다.
답글 남기기