기본
변수 & 가변성
Rust 변수는 기본적으로 불변입니다 — 이것은 우발적 변경을 방지하는 핵심 안전 기능입니다. 값을 실제로 변경해야 할 때만 'mut'를 사용하세요. 'const'는 명시적 타입이 필요하고 컴파일 타임에 인라인되며, 'static'은 'static 라이프타임과 함께 고정된 메모리 주소를 가집니다.
let x = 5; // immutable by default
let mut y = 10; // mutable with 'mut'
y += 1; // OK, y is mutable
const MAX: u32 = 100; // compile-time constant
static GREETING: &str = "Hi"; // global static
let z: i32 = -5; // explicit type annotation섀도잉
섀도잉은 타입이나 값을 변경하면서 변수 이름을 재사용하게 합니다. 'mut'와 달리 섀도잉은 새 바인딩을 만듭니다 — 새 이름을 만들지 않고 데이터를 변환(예: 문자열을 정수로 파싱)하는 데 유용합니다. 이전 값은 새 바인딩 후에 삭제됩니다.
let x = 5;
let x = x * 2; // shadow previous x, now 10
let x = "text"; // can even change type!
println!("{}", x); // "text"
// shadowing vs mut: shadowing creates a NEW variable
// with the same name, allowing type changes주석 & 출력
항목 문서('cargo doc'으로 표시)에는 ///를, 모듈/크레이트 문서에는 //!를 사용하세요. println!은 stdout에, eprintln!은 stderr에 씁니다. {} 자리표시자는 형식 명세를 지원합니다: > 우측 정렬, < 좌측 정렬, ^ 중앙 정렬, .N 정밀도, #b/#o/#x 2진/8진/16진.
// Line comment
/* Block comment */
/// Doc comment (renders in rustdoc)
//! Module-level doc comment
let name = "Alice";
println!("Hello, {}!", name); // format string
println!("{0} {1} {0}", "a", "b"); // positional args
println!("{:>5}", 42); // right-align, width 5
println!("{:.2}", 3.14159); // 2 decimal places: 3.14
println!("{:#b}", 0b1010); // binary: 0b1010
eprintln!("Error to stderr"); // error output데이터 타입 개요
Rust에는 암시적 타입 변환이 없습니다 — 캐스트에 'as'를 사용하세요. i32가 기본 정수 타입이고, f64가 기본 float입니다. usize는 인덱싱과 크기에 사용됩니다. char는 완전한 Unicode 스칼라 값(바이트가 아님)이므로, '🦀'는 단일 char입니다.
// Scalar types
let a: i32 = 42; // signed 32-bit integer
let b: u64 = 100; // unsigned 64-bit
let c: f64 = 3.14; // 64-bit float (default)
let d: bool = true;
let e: char = '🦀'; // 4-byte Unicode scalar
// isize/usize: pointer-sized (32 or 64 bit depending on arch)
let len: usize = vec![1,2,3].len();
// Compound types
let pair: (i32, &str) = (1, "hello");
let arr: [i32; 3] = [0; 3]; // [0, 0, 0]타입 캐스팅 & 별칭
'as' 캐스트는 검사되지 않고 데이터를 잃을 수 있습니다(예: 300u16 as u8은 44로 래핑). 안전한 변환을 위해 무손실 캐스트에 구현된 From/Into 트레이트를 사용하세요. 타입 별칭은 새 타입을 만들지 않고 가독성을 향상합니다 — 별개의 newtype 패턴을 위해 'struct NewType(i32)'를 사용하세요.
// 'as' keyword for primitive casts (may truncate)
let x: i32 = 42;
let y: f64 = x as f64; // 42.0
let z: u8 = 300u16 as u8; // truncated: 44
// From/Into traits for safe conversion
let s = String::from("hi");
let owned: String = "hi".into();
// Type aliases
type Kilometers = i32;
let distance: Kilometers = 5;문자열
String vs &str
String(소유, 힙)과 &str(빌린 슬라이스)의 구분이 Rust에서 근 본적입니다. 텍스트를 소유/수정/확장해야 할 때 String을 사용하고, 읽기만 할 때 &str을 사용하세요. &str은 String의 버퍼나 바이너리의 문자열 리터럴을 가리킬 수 있습니다. 유연성을 위해 함수 매개변수에서 &str을 선호하세요.
// &str: immutable string slice (borrowed)
let s1: &str = "literal"; // stored in binary
let s2: &str = &s3[..2]; // slice of a String
// String: owned, growable, heap-allocated
let mut s3: String = String::from("hello");
s3.push_str(", world"); // append
s3.push('!'); // append char
s3.insert(0, '>'); // insert at index
println!("{}", s3); // >hello, world!
// Convert: &str -> String
let owned = "literal".to_string();
let owned2 = String::from("literal");String 메서드
String 메서드는 제자리에서 수정하는 대신 새 소유 String을 반환합니다(가변 String의 push_*/insert 메서드 제외). split()은 이터레이터를 반환하므로, 구체화하려면 .collect()를 사용하세요. UTF-8 경계가 바이트 인덱스와 정렬되지 않아 문자열에서 s[0] 인덱싱은 허용되지 않습니다 — 대신 s.chars().nth(0)을 사용하세요.
let s = String::from("Hello, World");
// inspection
println!("len: {}", s.len()); // 12
println!("is_empty: {}", s.is_empty()); // false
println!("contains 'World': {}", s.contains("World")); // true
println!("starts_with 'Hello': {}", s.starts_with("Hello")); // true
// transformation
let upper = s.to_uppercase(); // HELLO, WORLD
let replaced = s.replace("o", "0"); // Hell0, W0rld
let trimmed = " hi ".trim().to_string(); // hi
// splitting
for word in s.split(", ") {
println!("{}", word); // Hello / World
}
let parts: Vec<&str> = s.split_whitespace().collect();형식화 & 연결
+ 연산자는 왼쪽 String의 소유권을 가져오고 오른쪽(&str)을 빌립니다. 이것이 s1 + &s2 후에 s1이 유효하지 않게 되는 이유입니다. 여러 문자열을 체이닝하려면, 더 읽기 쉽고 어떤 피연산자도 이동하지 않는 format!를 선호하세요. concat!는 리터럴에만 작동하고 &'static str을 생성합니다.
// format! macro creates a new String
let name = "Alice";
let msg = format!("Hi {}, you have {} messages", name, 5);
// concatenation
let s1 = String::from("Hello");
let s2 = String::from(" World");
let s3 = s1 + &s2; // s1 moved, s2 borrowed
// s1 is now invalid!
let s4 = format!("{}{}", s2, s3); // neither moved
// concat! macro for literals
let s5 = concat!("foo", "bar"); // "foobar" at compile time문자 & 바이트 순회
Rust 문자열은 UTF-8 인코딩이므로, 바이트 인덱스 != 문자 인덱스입니다. chars()는 Unicode 스칼라 값을 순회하고(디코딩에 O(n)), bytes()는 원시 바이트를 순회합니다. [n..m] 슬라이싱은 n이나 m이 다중 바이트 문자 중간에 있으면 패닉합니다. 바이트 수준 접근을 위해, as_bytes()로 Vec<u8>로 변환하세요.
let s = "héllo"; // é is 2 bytes in UTF-8
// iterate over chars (Unicode scalar values)
for c in s.chars() {
print!("{} ", c); // h é l l o
}
// iterate over bytes
for b in s.bytes() {
print!("{} ", b); // 104 195 169 108 108 111
}
// get char at index (O(n) — must walk UTF-8)
let third = s.chars().nth(2); // Some('l')
// string slices must be on char boundaries
let slice = &s[0..1]; // "h" — OK
// let bad = &s[0..2]; // PANIC if mid-é!파싱 & 변환
parse()는 문자열이 유효한 숫자가 아닐 수 있으므로 Result를 반환합니다 — 항상 에러를 처리하세요. turbofish 구문 parse::<T>()는 타입을 인라인으로 지정하게 합니다. String을 &str로 변환하는 것은 무료(빌린만)이지만, &str을 String으로 변환하는 것은 할당합니다. collect()는 char 이터레이터에서 String을 구축할 수 있습니다.
// String/str -> number
let n: i32 = "42".parse().unwrap();
let n2 = "42".parse::<i32>().unwrap(); // turbofish syntax
let f: f64 = "3.14".parse().unwrap();
// number -> String
let s = 42.to_string();
let s2 = format!("{}", 42);
// String -> &str (free, just dereference)
let owned = String::from("hi");
let borrowed: &str = &owned;
// collect chars into String
let upper: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();데이터 구조
배열 & 슬라이스
배열 [T; N]은 컴파일 타임에 알려진 고정 크기를 가지며 스택에 있습니다. 슬라이스 &[T]는 연속 시퀀스를 빌리는 팻 포인터(포인터 + 길이)입니다 — 함수가 크기와 상관없이 모든 배열이나 벡터를 받을 수 있게 합니다. 일반성을 위해 함수 시그니처에서 슬라이스를 사용하세요.
// Fixed-size array (stack allocated)
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
println!("first: {}", arr[0]);
println!("len: {}", arr.len());
// Slice: a view into an array/vector
let slice: &[i32] = &arr[1..3]; // [2, 3]
let full: &[i32] = &arr; // whole array
let first = &arr[..1]; // [1]
// iterating
for n in &arr {
println!("{}", n);
}벡터 (Vec<T>)
Vec<T>는 Rust의 성장 가능한 배열로, 용량이 두 배가 되는 힙 메모리로 백업됩니다. push/pop은 O(1) 분할 상환; insert/remove는 요소 이동으로 O(n)입니다. 안전한 접근(Option 반환)을 원할 때 v[i] 대신 .get(i)를 사용하세요. into_iter()는 벡터를 소비하여 소유 값을 반환합니다.
// growable heap array
let mut v: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3]; // macro shorthand
v.push(4); // append
v.pop(); // remove last -> Option
v.insert(0, 0); // insert at index (O(n))
v.remove(0); // remove at index (O(n))
v.extend([5, 6]); // append multiple
// access
println!("{}", v[0]); // panics if out of bounds
println!("{:?}", v.get(0)); // Some(&4) — safe
// iterate by value, ref, or mut ref
for n in &v { print!("{}", n); }
for n in &mut v { *n *= 2; } // double each
let owned: Vec<i32> = v.into_iter().collect();HashMap & BTreeMap
HashMap은 O(1) 평균 접근을 위해 해싱을 사용하지만 순서가 없습니다. BTreeMap은 O(log n) 접근을 위해 B-트리를 사용하지만 키를 정렬된 상태로 유지합니다. 빠른 조회가 필요할 때 HashMap을; 정렬된 순회나 범위 쿼리가 필요할 때 BTreeMap을 사용하세요. entry().or_insert()는 'upsert'의 관용적 방법입니다 — 값에 대한 가변 참조를 반환하고, 없으면 기본값을 삽입합니다.
use std::collections::HashMap;
use std::collections::BTreeMap;
// HashMap: O(1) average lookup, unordered
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.entry("Bob".into()).or_insert(0); // insert if absent
let alice = scores.get("Alice"); // Some(&10)
// iterate (unordered)
for (name, score) in &scores {
println!("{}: {}", name, score);
}
// BTreeMap: O(log n) lookup, sorted by key
let mut bt: BTreeMap<String, i32> = BTreeMap::new();
bt.insert("zebra".into(), 1);
bt.insert("apple".into(), 2);
// iterates in sorted order: apple, zebraHashSet & BTreeSet
HashSet은 O(1) 멤버십 검사로 고유한 값을 저장합니다. 집합 연산(union, intersection, difference, symmetric_difference)은 이터레이터를 반환합니다. 중복 제거, 멤버십 테스트, 수학적 집합 연산에 사용하세요. BTreeSet은 BTreeMap으로 백업된 정렬된 동등물입니다.
use std::collections::HashSet;
let mut a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5].into_iter().collect();
a.insert(4);
a.remove(&1);
println!("contains 2: {}", a.contains(&2));
// set operations
let union: HashSet<_> = a.union(&b).copied().collect();
let inter: HashSet<_> = a.intersection(&b).copied().collect();
let diff: HashSet<_> = a.difference(&b).copied().collect();
let sym: HashSet<_> = a.symmetric_difference(&b).copied().collect();튜플 & 구조 분해
튜플은 잠재적으로 다른 타입의 고정된 수의 값을 그룹화합니다. .0, .1 등으로 필드에 접근하세요. let 패턴으로 구조 분해가 관용적입니다. 단위 타입 ()은 하나의 값 ()을 가지며 다른 언어의 void처럼 사용됩니다. 튜플은 함수에서 여러 값을 반환하는 데 일반적으로 사용됩니다.
// tuples can hold different types
let tup: (i32, f64, &str) = (42, 3.14, "hi");
// access by index
println!("{} {} {}", tup.0, tup.1, tup.2);
// destructuring
let (n, pi, s) = tup;
println!("{} {} {}", n, pi, s);
// nested
let ((a, b), c) = ((1, 2), 3);
// unit tuple (empty)
let unit: () = ();
// function returning multiple values
fn divmod(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
let (q, r) = divmod(17, 5); // (3, 2)제어 흐름
If / Else If / Else
많은 언어와 달리, Rust에서 'if'는 값을 반환하는 표 현식입니다. 이것이 삼항 연산자(condition ? a : b)의 필요를 없앱니다 — 그냥 if/else를 사용하세요. 두 브랜치는 같은 타입을 반환해야 합니다. 조건은 괄호가 필요 없지만, 본문은 블록 { }이어야 합니다.
let score = 85;
// if is an expression — returns a value
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else if score >= 70 {
"C"
} else {
"F"
};
println!("Grade: {}", grade);
// arms must return same type
// let bad = if true { 1 } else { "two" }; // ERROR!루프: loop, while, for
loop는 무한 루프를 만듭니다 — break가 빠져나가며 값을 반환할 수 있습니다(break value). while은 각 반복 전에 조건을 검사합니다. for는 가장 일반적인 루프로, 범위, 배열, 벡터, 이터레이터를 순회합니다. 다음 반복으로 건너뛰려면 'continue'를 사용하세요. 범위: a..b는 배타적, a..=b는 포함.
// loop: infinite, use break to exit
let mut count = 0;
let result = loop {
count += 1;
if count == 10 {
break count * 2; // break with value
}
};
println!("{}", result); // 20
// while: condition-checked loop
let mut n = 5;
while n > 0 {
println!("{}", n);
n -= 1;
}
// for: iterate over anything with IntoIterator
for i in 0..5 { print!("{}", i); } // 01234
for i in (1..=3).rev() { print!("{}", i); } // 321
for ch in "hello".chars() { print!("{}", ch); }Match (패턴 매칭)
match는 Rust의 강력한 패턴 매칭 구문입니다. 철저해야 합니다(모든 가능성 포함) — 캐치올로 _를 사용하세요. 패턴은 리터럴, 범위(..=), or-패턴(|), 바인딩, 가드(if)를 지원합니다. match는 표현식이며 값을 반환합니다. Option과 Result 같은 열거형을 처리하는 관용적 방법입니다.
let coin = 25;
match coin {
25 => println!("quarter"),
10 => println!("dime"),
5 => println!("nickel"),
1 => println!("penny"),
_ => println!("unknown"), // catch-all (required)
}
// matching with bindings
let x = 3;
match x {
1 | 2 => println!("one or two"), // or-pattern
3..=9 => println!("single digit"), // range
n if n % 2 == 0 => println!("even: {}", n), // guard
_ => println!("other"),
}
// match is exhaustive — all cases must be coveredIf Let & While Let
if let은 하나의 변형만 신경 쓸 때 match의 구문적 설탕입니다. match보다 덜 장황하지만 덜 철저합니다 — 다른 케이스가 중요하지 않을 때 사용하세요. while let은 패턴이 일치하는 동안 루프하며, 이터레이터와 함께 일반적으로 사용됩니다(next()는 Option 반환). else 브랜치는 선택적입니다.
// if let: shorthand for matching one pattern
let maybe: Option<i32> = Some(5);
// verbose: match
match maybe {
Some(x) => println!("got {}", x),
None => println!("nothing"),
}
// concise: if let
if let Some(x) = maybe {
println!("got {}", x);
} else {
println!("nothing");
}
// while let: loop while pattern matches
let mut iter = vec![1, 2, 3].into_iter();
while let Some(n) = iter.next() {
println!("{}", n);
}Break, Continue & 레이블
레이블(단일 따옴표 + 이름)은 중첩 루프 내에서 외부 루프를 break하거나 continue할 수 있게 합니다. 여러 루프 수준을 한 번에 빠져나가야 할 때 필수적입니다. 레이블 없이, break/continue는 가장 안쪽 루프에만 영향을 줍니다. 레이블은 플래그 변수의 깔끔한 대안입니다.
// continue: skip to next iteration
for i in 0..10 {
if i % 2 == 0 { continue; }
println!("{}", i); // prints odd numbers
}
// break: exit loop
for i in 0..10 {
if i == 5 { break; }
println!("{}", i); // prints 0..4
}
// labeled loops (for nested break/continue)
'outer: for i in 0..3 {
for j in 0..3 {
if i == 1 && j == 1 {
break 'outer; // breaks the outer loop
}
println!("{} {}", i, j);
}
}함수 & 클로저
함수 정의
함수는 'fn' 키워드를 사용합니다. 세미콜론 없는 마지막 표현식이 반환 값입니다(표현식). 세미콜론을 추가하면 ()를 반환하는 문이 됩니다. 조기 종료에만 명시적 'return'을 사용하세요. ! 반환 타입은 절대 반환하지 않는 발산 함수(무한 루프, 패닉, 프로세스 종료)를 표시합니다.
// basic function with return type
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = expression = return value
}
// statements (with semicolon) return ()
fn greet(name: &str) {
println!("Hi, {}", name);
// implicit return ()
}
// explicit return
fn abs(x: i32) -> i32 {
if x < 0 {
return -x; // early return needs 'return'
}
x // tail expression
}
// diverging function (never returns)
fn forever() -> ! {
loop {}
}매개변수 & 인수
Rust에는 함수 오버로딩이나 선택적 매개변수가 없습니다(대신 제네릭이나 빌더를 사용). 매개변수 타입을 신중히 선택하세요: 읽기 접근에는 &T, 쓰기 접근에는 &mut T, 소유권 이전에는 T. 슬라이스(&[T])는 가변 길이 시퀀스를 받는 관용적 방법입니다. 기본 인수는 지원되지 않습니다 — 빌더 패턴이나 Option<T>를 사용하세요.
// immutable borrow
fn len(s: &String) -> usize { s.len() }
// mutable borrow
fn push(v: &mut Vec<i32>) { v.push(42); }
// take ownership
fn consume(s: String) { println!("{}", s); }
// multiple return via tuple
fn swap(a: i32, b: i32) -> (i32, i32) { (b, a) }
// variadic-ish via slices
fn sum(nums: &[i32]) -> i32 {
nums.iter().sum()
}
println!("{}", sum(&[1, 2, 3, 4])); // 10클로저
클로저는 환경을 캡처할 수 있는 익명 함수입니다. 사용에 의해 추론됩니다. 클로저는 기본적으로 참조로 캡처합니다; 'move'는 소유권 이전을 강제합니다(스레드에 필수). 클로저는 Fn(빌린), FnMut(가변 빌린), 또는 FnOnce(소비) 트레이트를 구현하여 함수 매개변수로 전달될 수 있습니다.
// closure syntax: |params| body
let add = |a, b| a + b;
println!("{}", add(1, 2)); // 3
// type annotations (rarely needed)
let square = |x: i32| -> i32 { x * x };
// capturing environment
let multiplier = 3;
let multiply = |x| x * multiplier; // borrows multiplier
println!("{}", multiply(5)); // 15
// move closure: takes ownership of captured vars
let name = String::from("Alice");
let greet = move || println!("Hi {}", name);
// name is now moved into greet
greet();고차 함수 & 이터레이터
Rust 이터레이터는 지연입니다 — .collect()나 다른 소비 메서드가 호출될 때까지 작업이 실행되지 않습니다. 이는 제로 비용 추상을 가능하게 합니다: 컴파일러가 체인된 이터레이터 메서드를 효율적인 루프로 최적화할 수 있습니다. 일반 메서드: map(변환), filter(선택), fold(누적), take(제한), skip, enumerate, zip, flat_map.
let nums = vec![1, 2, 3, 4, 5];
// map: transform each element
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
// filter: keep elements matching predicate
let evens: Vec<&i32> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// fold/reduce: accumulate
let sum: i32 = nums.iter().sum(); // 15
let product: i32 = nums.iter().product(); // 120
let combined = nums.iter().fold(0, |acc, x| acc + x);
// chain multiple operations (lazy!)
let result: Vec<i32> = nums.iter()
.filter(|&&x| x > 1)
.map(|&x| x * x)
.collect(); // [4, 9, 16, 25]함수 포인터 & 트레이트
fn(소문자)은 함수 포인터 타입입니다 — 제로 비용이지만 환경을 캡처할 수 없습니다. 캡처하는 클로저를 위해, 제네릭 <F: Fn(...)> 바운드를 사용하세요. Fn은 빌리고, FnMut는 가변으로 빌리고, FnOnce는 소비합니다. 함수 포인터는 구조체에 함수를 저장하거나 C에 전달하는 데 유용합니다. Fn 바운드가 있는 제네릭은 더 유연하며 모노모프화되면 여전히 제로 비용입니다.
// function pointer type
type MathFn = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn mul(a: i32, b: i32) -> i32 { a * b }
fn apply(f: MathFn, a: i32, b: i32) -> i32 {
f(a, b)
}
println!("{}", apply(add, 3, 4)); // 7
println!("{}", apply(mul, 3, 4)); // 12
// generic over Fn trait (accepts closures too)
fn apply_fn<F: Fn(i32, i32) -> i32>(f: F, a: i32, b: i32) -> i32 {
f(a, b)
}
let closure = |a, b| a - b;
println!("{}", apply_fn(closure, 10, 3)); // 7소유권 & 빌림
소유권 규칙
소유권은 Rust의 핵심 메모리 관리 시스템입니다 — 가비지 컬렉터가 필요 없습니다. 힙 값(String, Vec)을 할당하면 소유권이 이동하고 이전 변수는 유효하지 않게 됩니다. 스택 타입(i32, f64, bool, char, Copy 타입의 튜플)은 Copy를 구현하여 복제됩니다. 이는 컴파일 타임에 use-after-free와 double-free 버그를 제거합니다.
// Rule 1: Each value has ONE owner
let s1 = String::from("hello");
let s2 = s1; // s1's ownership MOVED to s2
// println!("{}", s1); // ERROR: s1 is invalid after move
// Rule 2: When owner goes out of scope, value is dropped
{
let s = String::from("temp");
// s is valid here
} // s is automatically dropped (memory freed)
// Rule 3: Copy types (i32, bool, char, etc.) are copied, not moved
let a = 5;
let b = a; // a is copied, both valid
println!("{} {}", a, b); // OK빌림 & 참조
빌림은 소유권을 가져오지 않고 값을 사용하게 합니다. &T는 불변 참조를 만듭니다 — 동시에 여러 개를 가질 수 있습니다. &mut T는 가변 참조를 만듭니다 — 하지만 오직 하나의 가변 참조 OR 임의 수의 불변 참조, 둘 다는 안 됩니다. 이것이 컴파일 타임에 데이터 레이스를 방지합니다. 참조는 항상 유효한 데이터를 가리켜야 합니다(댕글링 포인터 없음).
// &T: immutable borrow (read-only, multiple allowed)
fn calc_len(s: &String) -> usize {
s.len()
// s goes out of scope but is NOT dropped (we don't own it)
}
let s = String::from("hello");
let len = calc_len(&s); // borrow s, don't move it
println!("'{}' has length {}", s, len); // s still valid
// &mut T: mutable borrow (exclusive, only ONE at a time)
fn push_world(s: &mut String) {
s.push_str(", world");
}
let mut s2 = String::from("hello");
push_world(&mut s2);
println!("{}", s2); // hello, world슬라이스 참조
슬라이스는 컬렉션의 연속 부분에 대한 참조입니다. 포인터와 길이를 포함하는 '팻 포인터'입니다. 문자열 슬라이스(&str)는 함수가 String과 문자열 리터럴 모두를 받을 수 있게 합니다. 배열 슬라이스(&[T])는 임의의 연속 시퀀스와 작동합니다. 슬라이스는 기본 데이터를 빌려서, 슬라이스가 존재하는 동안 수정되거나 삭제되는 것을 방지합니다.
// string slice: &str
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello"
let world: &str = &s[6..]; // "world"
let full: &str = &s[..]; // "hello world"
// array slice: &[T]
let arr = [1, 2, 3, 4, 5];
let mid: &[i32] = &arr[1..4]; // [2, 3, 4]
// function accepting slices (idiomatic)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[0..i];
}
}
&s[..]
}라이프타임
라이프타임은 컴파일러에게 참조가 얼마나 오래 유효한지 알립니다. 'a 어노테이션은 라이프타임을 변경하지 않습니다 — 관계를 설명합니다. 'static은 프로그램 전체 지속되는 특수 라이프타임입니다(문자열 리터럴이 가짐). 대부분의 코드는 라이프타임 생략(컴파일러 추론)을 사용합니다. 다음 경우에 명시적 라이프타임이 필요합니다: 함수가 참조를 반환하거나, 구조체가 참조를 가질 때.
// explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y's lifetimes
let s1 = String::from("long string");
let s2 = String::from("hi");
let result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result);
// struct holding references needs lifetime
struct Excerpt<'a> {
part: &'a str,
}
let novel = String::from("call me Ishmael. years ago...");
let first_sentence = novel.split('.').next().unwrap();
let ex = Excerpt { part: first_sentence };스마트 포인터
Box<T>는 데이터를 힙으로 이동합니다(단일 소유자). Rc<T>는 참조 카운팅을 통해 공유 소유권을 가능하게 합니다(단일 스레드만). Arc<T>는 원자를 사용하는 스레드 안전 버전입니다. RefCell<T>는 빌림 검사를 런타임으로 이동하여, 공유 참조를 통한 변경(내부 가변성)을 허용합니다. 재귀 타입에는 Box, 그래프 같은 구조에는 Rc/Arc, 공유 데이터를 변경해야 할 때 RefCell을 사용하세요.
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
// Box<T>: heap allocation, single owner
let b = Box::new(5); // 5 lives on heap
println!("{}", b); // dereferenced automatically
// Rc<T>: reference counting, multiple owners (single-threaded)
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments ref count
println!("count: {}", Rc::strong_count(&a)); // 2
// Arc<T>: atomic Rc, thread-safe
let arc = Arc::new(vec![1, 2, 3]);
// RefCell<T>: interior mutability (runtime borrow check)
let cell = RefCell::new(5);
*cell.borrow_mut() += 1; // mutable borrow checked at runtime구조체, 열거형 & 트레이트
구조체 정의
구조체는 관련 필드를 그룹화합니다. 명명 필드 구조체가 가장 일반적입니다. 튜플 구조체는 필드 이름이 의미 없을 때 유용합니다(Color, Point). 단위 구조체는 데이터가 없고 트레이트를 구현하는 데 사용됩니다. .. 구문은 다른 인스턴스에서 미지정 필드를 복사합니다. 구조체는 힙 타입(String, Vec, Box)을 포함하지 않는 한 스택 할당됩니다.
// named-field struct
struct User {
name: String,
age: u32,
active: bool,
}
let u = User {
name: String::from("Alice"),
age: 30,
active: true,
};
// field init shorthand
let name = String::from("Bob");
let u2 = User { name, age: 25, active: true };
// update syntax (copy remaining fields from another)
let u3 = User { age: 40, ..u2 };
// tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
// unit struct (no fields, useful for traits)
struct AlwaysEqual;impl로 메서드
메서드는 impl 블록에 있습니다. &self는 불변으로 빌리고, &mut self는 가변으로 빌리고, self는 소유권을 가져옵니다(소비). 연관 함수(self 매개변수 없음)는 정적 메서드와 같습니다 — Type::function()으로 호출. Self는 타입의 별칭입니다. 여러 impl 블록이 허용되며, 관심사별로 메서드를 분할하거나 조건부 컴파일에 유용합니다.
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// associated function (constructor, no &self)
fn new(w: f64, h: f64) -> Self {
Rectangle { width: w, height: h }
}
// method (borrows self)
fn area(&self) -> f64 {
self.width * self.height
}
// mutable method
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
// consuming method (takes ownership)
fn into_square(self) -> Rectangle {
let side = (self.width + self.height) / 2.0;
Rectangle { width: side, height: side }
}
}
let mut r = Rectangle::new(10.0, 5.0);
println!("Area: {}", r.area()); // 50
r.scale(2.0);
let sq = r.into_square(); // r consumed열거형 & 패턴 매칭
Rust의 열거형은 대수적 데이터 타입입니다 — 각 변형이 다른 데이터를 가질 수 있습니다. 이것이 C 열거형보다 훨씬 강력하게 만듭니다. match로 패턴 매칭은 변형을 분해하고 데이터를 추출합니다. 값이 여러 구별된 형태 중 하나일 때 열거형을 사용하세요. matches! 매크로는 불리언을 반환하는 단일 패턴 매칭의 약식입니다.
// enum with data (algebraic data type)
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields
Write(String), // tuple variant
ChangeColor(i32, i32, i32), // tuple variant
}
// pattern matching with destructuring
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("RGB({}, {}, {})", r, g, b),
}
}
process(Message::Move { x: 10, y: 20 });
process(Message::Write(String::from("hello")));
// enums can have methods too
impl Message {
fn is_quit(&self) -> bool {
matches!(self, Message::Quit)
}
}Option & Result
Option<T>는 null을 대체합니다 — None 케이스를 명시적으로 처리해야 하여 NullPointerException 스타일 버그를 제거합니다. Result<T, E>는 실패할 수 있는 작업을 위한 것입니다. 둘 다 풍부한 메서드를 가집니다: map(변환), and_then(체인), unwrap_or(기본값), is_some/is_ok(검사). Result의 ? 연산자는 에러를 자동으로 전파합니다. 이 두 타입이 Rust 에러 처리의 중추입니다.
// Option<T>: Some(value) or None (replaces null)
fn find_user(id: i32) -> Option<String> {
if id == 1 { Some(String::from("Alice")) }
else { None }
}
let user = find_user(1);
match user {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
// convenient methods
let name = find_user(1).unwrap_or("Anonymous".into());
let upper = find_user(1).map(|n| n.to_uppercase());
let len = find_user(1).and_then(|n| Some(n.len()));
// Result<T, E>: Ok(value) or Err(error)
fn parse_num(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
match parse_num("42") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Error: {}", e),
}트레이트 & 트레이트 바운드
트레이트는 공유 동작을 정의합니다(다른 언어의 인터페이스처럼). 타입은 'impl Trait for Type'으로 트레이트를 구현합니다. 트레이트는 기본 메서드 구현을 가질 수 있습니다. 트레이트 바운드(<T: Trait>)는 제네릭을 특정 트레이트를 구현하는 타입으로 제한합니다. 'where' 절은 복잡한 바운드의 가독성을 향상합니다. 트레이트는 정적 디스패치(제네릭)와 동적 디스패치(트레이트 객체 &dyn Trait) 모두로 다형성을 가능하게 합니다.
// define a trait (interface)
trait Summary {
fn summarize(&self) -> String;
// default method
fn author(&self) -> String {
String::from("Unknown")
}
}
struct Article { title: String, content: String }
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.content)
}
// author() uses default implementation
}
let a = Article {
title: "Rust".into(),
content: "Great".into(),
};
println!("{}", a.summarize());
println!("Author: {}", a.author()); // Unknown
// generic with trait bound
fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// multiple bounds with +, or where clause
fn display<T: Summary + std::fmt::Display>(item: &T) {}
// or: fn display<T>(item: &T) where T: Summary + std::fmt::Display {}Derive 매크로 & 트레이트 객체
#[derive(...)]는 일반 트레이트를 자동 구현합니다: Debug(디버그 출력), Clone(깊은 복사), PartialEq/Eq(== 비교), Hash(HashMap 키용), Copy(이동 대신 스택 복사). 트레이트 객체(&dyn Trait 또는 Box<dyn Trait>)는 vtable을 통해 런타임 다형성을 가능하게 하며, 약간의 성능 비용이 있습니다. 가능하면 정적 디스패치(제로 비용)를 위해 제네릭을, 이질적 컬렉션이 필요할 때 트레이트 객체를 사용하세요.
// derive common traits automatically
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Clone
println!("{:?}", p1); // Debug
println!("{}", p1 == p2); // PartialEq -> true
// trait object: dynamic dispatch
trait Animal {
fn sound(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog { fn sound(&self) -> String { "Woof".into() } }
impl Animal for Cat { fn sound(&self) -> String { "Meow".into() } }
// Vec of trait objects (dynamic dispatch via vtable)
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in &animals {
println!("{}", a.sound());
}에러 처리
Result & ? 연산자
? 연산자는 에러를 전파하는 관용적 방법입니다. Ok(v)에서는 v로 언랩합니다. Err(e)에서는 함수에서 즉시 Err(e)를 반환합니다. ?는 From 트레이트를 통해 에러 타입을 변환하므로, Box<dyn Error>를 반환하는 함수는 모든 에러 타입에 ?를 사용할 수 있습니다. Option에서 ?는 일찍 None을 반환합니다. 이것이 안전성을 희생하지 않고 에러 처리를 간결하게 만듭니다.
use std::fs;
use std::io;
use std::num::ParseIntError;
// ? propagates errors: if Err, return early; if Ok, unwrap
fn read_config(path: &str) -> Result<i32, io::Error> {
let content = fs::read_to_string(path)?; // ? on io::Result
Ok(content.len() as i32)
}
// ? converts error types via From
fn parse_and_read(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // io::Error -> Box
let n: i32 = content.trim().parse()?; // ParseIntError -> Box
Ok(n)
}
// ? on Option too
fn first_char(s: &str) -> Option<char> {
s.lines().next()?.chars().next()
}Panic vs Result
복구 불가능한 에러(버그, 위반된 불변성)에는 panic!을 사용하세요 — 프로그래밍 에러를 나타냅니다. 예상되고 복구 가능한 실패(사용자 입력, 파일 I/O, 네트워크)에는 Result를 사용하세요. unwrap()/expect()는 에러 시 패닉합니다 — 테스트, 프로토타입, 또는 값이 유효함을 증명할 수 있을 때 허용됩니다. 프로덕션 코드에서는 ?와 match로 적절한 에러 처리를 선호하세요.
// panic: unrecoverable error, crashes the program
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero!"); // unwinds stack
}
a / b
}
// Result: recoverable error, caller decides
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
// unwrap/expect: panic on Err (use sparingly)
let n: i32 = "42".parse().unwrap(); // panics on error
let n2: i32 = "42".parse().expect("valid number"); // panic with msg
// when to panic vs Result:
// panic: bugs, invariant violations, impossible states
// Result: expected failures (file not found, parse error)커스텀 에러 타입
커스텀 에러 타입은 타입 안전하고 구조화된 에러 처리를 제공합니다. Display(사람이 읽기 쉬운)와 Error(소스 체이닝용)를 구현하세요. 각 기본 에러에 대해 From을 구현하여 ?가 자동 변환하게 하세요. thiserror(derive 매크로)나 anyhow(동적 에러 박스) 같은 라이브러리가 보일러플레이트를 줄입니다. 라이브러리에는 thiserror를, 애플리케이션에는 anyhow를 사용하세요.
use std::fmt;
use std::error::Error;
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
}
// implement Display (required by Error)
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::NotFound(name) => write!(f, "Not found: {}", name),
}
}
}
// implement Error
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
AppError::Parse(e) => Some(e),
_ => None,
}
}
}
// From impls for ? to work automatically
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}Result 매칭 & 결합
Result 컴비네이터는 match 없이 함수형 스타일 에러 처리를 허용합니다. map은 Ok 값을 변환하고, map_err는 에러를 변환하고, and_then은 실패 가능한 작업을 체인합니다(flatMap). unwrap_or는 에러 시 기본값을 제공합니다. is_ok/is_err는 소비 없이 검사합니다. 이 메서드들은 에러 처리 체인을 읽기 쉽게 만들고 깊은 중첩을 방지합니다.
// match on Result
let result: Result<i32, &str> = Ok(42);
match result {
Ok(n) => println!("Got: {}", n),
Err(e) => println!("Error: {}", e),
}
// combinators
let r1: Result<i32, &str> = Ok(5);
let doubled = r1.map(|n| n * 2); // Ok(10)
let mapped_err = r1.map_err(|e| format!("{}", e));
// and_then: chain fallible operations
let r2 = r1.and_then(|n| if n > 0 { Ok(n * 2) } else { Err("negative") });
// unwrap_or family
let val = r1.unwrap_or(0); // 5 or 0
let val2 = r1.unwrap_or_default(); // 5 or T::default()
let val3 = r1.unwrap_or_else(|_| 0); // lazy default
// check variants
println!("{}", r1.is_ok()); // true
println!("{}", r1.is_err()); // falseFrom으로 에러 변환
? 연산자는 From을 사용하여 에러를 변환합니다. Box<dyn Error>는 모든 표준 에러에 대해 From을 구현하여, 편리한 캐치올로 만듭니다. anyhow 크레이트는 컨텍스트를 추가하는 anyhow::Result를 제공합니다(예: .context("failed to read config")?). 라이브러리의 경우, thiserror로 특정 에러 열거형을 정의하세요; 애플리케이션의 경우, 단순성을 위해 anyhow를 사용하세요.
use std::fs;
use std::num::ParseIntError;
// Without From: manual conversion needed
fn manual(path: &str) -> Result<i32, String> {
let content = fs::read_to_string(path)
.map_err(|e| e.to_string())?; // manual convert
let n: i32 = content.trim().parse()
.map_err(|e| e.to_string())?; // manual convert
Ok(n)
}
// With Box<dyn Error>: auto-converts via From
fn boxed(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // auto-converts
let n: i32 = content.trim().parse()?; // auto-converts
Ok(n)
}
// anyhow::Result (from anyhow crate) is ergonomic for apps
// fn anyhow_fn() -> anyhow::Result<i32> {
// let n: i32 = something()?; // any error auto-converted
// Ok(n)
// }모듈 & 크레이트
모듈 시스템
Rust의 모듈 시스템은 코드를 조직합니다. 'mod'는 모듈을 선언합니다(인라인 또는 파일로). 'pub'는 항목을 공개로 만듭니다(기본은 private). 'use'는 경로로의 단축키를 만듭니다. 'pub use'는 항목을 재내보내기합니다(API 설계에 유용). 파일 시스템은 모듈 트리를 반영합니다: mod network는 src/network.rs 또는 src/network/mod.rs가 될 수 있습니다. 크레이트 루트는 lib.rs(라이브러리) 또는 main.rs(바이너리)입니다.
// mod.rs or mod declaration in lib.rs/main.rs
// File: src/lib.rs
mod network {
pub mod connection {
pub fn connect() -> bool { true }
fn disconnect() {} // private
}
}
// use: bring paths into scope
use network::connection::connect;
// calling with full path
fn main() {
network::connection::connect();
connect(); // after 'use'
}
// re-export with pub use
pub use network::connection::connect as open_connection;
// nested file: src/network/connection.rs
// mod network; in lib.rs loads src/network/mod.rs
// which can have: pub mod connection;프라이버시 & 가시성
Rust의 프라이버시는 모듈 범위입니다. 항목은 기본적으로 private입니다 — 정의 모듈과 자손 내에서만 접근 가능합니다. 'pub'는 공개로 만듭니다. 'pub(crate)'는 현재 크레이트로 제한합니다(라이브러리 내부에 유용). 'pub(super)'는 부모 모듈로 제한합니다. 구조체 필드는 개별 가시성을 가집니다 — pub 구조체가 private 필드를 가질 수 있어, 생성자가 필요합니다.
mod my_module {
// private by default
fn internal_helper() {}
// public: accessible from outside
pub fn public_api() {
internal_helper(); // can call private within module
}
// pub(crate): visible within this crate only
pub(crate) fn crate_wide() {}
// pub(super): visible to parent module
pub(super) fn parent_visible() {}
// struct fields are private even if struct is pub
pub struct Config {
pub name: String, // public field
secret: String, // private field
}
// enum variants inherit the enum's visibility
pub enum Status {
Active, // public because Status is public
Inactive,
}
}Use 문 & 별칭
use 문은 항목을 범위로 가져와, 경로 장황성을 줄입니다. 그룹화된 임포트(use std::io::{self, Read})는 여러 줄보다 깔끔합니다. 트레이트는 메서드를 사용하려면 범위에 있어야 합니다 — 이것이 이름으로 Read를 참조하지 않더라도 'use std::io::Read'가 필요한 이유입니다. 글로브 임포트(*)는 프렐루드를 제외하고 권장하지 않습니다.
// basic use
use std::collections::HashMap;
use std::fs::read_to_string;
// grouped use
use std::io::{self, Read, Write, BufRead};
// equivalent to:
// use std::io;
// use std::io::Read;
// use std::io::Write;
// use std::io::BufRead;
// aliasing with 'as'
use std::collections::HashMap as Map;
// glob import (use sparingly)
use std::prelude::v1::*;
// bringing trait methods into scope
use std::io::Read; // now .read() method is available
let mut f = std::fs::File::open("x")?;
let mut buf = String::new();
f.read_to_string(&mut buf)?; // Read trait methodCargo & 외부 크레이트
Cargo는 Rust의 패키지 매니저이자 빌드 시스템입니다. 종속성은 Cargo.toml의 [dependencies] 아래에 있습니다. 기능은 선택적 기능을 활성화합니다(컴파일 시간/바이너리 크기 감소). 'cargo add'는 Cargo.toml을 자동 편집합니다. 크레이트는 crates.io에 게시됩니다. 에디션(2021)은 언어 기능을 제어합니다. Cargo는 컴파일, 테스트, 문서화, 게시를 처리합니다.
# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"
# in Rust code
use serde::{Serialize, Deserialize};
use rand::Rng;
#[derive(Serialize, Deserialize)]
struct User { name: String, age: u32 }
fn main() {
let mut rng = rand::thread_rng();
let n: i32 = rng.gen_range(1..=100);
println!("{}", n);
}
# CLI commands:
# cargo new my_app # create new project
# cargo build # compile
# cargo run # compile + run
# cargo test # run tests
# cargo add serde # add dependency
# cargo update # update dependencies테스팅
테스트는 #[test] 속성을 사용합니다. assert_eq!/assert_ne!가 값을 비교합니다. #[should_panic]은 패닉 발생을 검증합니다. 테스트는 에러 기반 어설션을 위해 Result를 반환할 수 있습니다. #[cfg(test)]는 테스트 모듈이 테스트 중에만 컴파일됨을 보장합니다. 단위 테스트는 코드와 함께; 통합 테스트는 tests/ 디렉토리에 있습니다. 'cargo test'로 실행하세요. 불안정한 테스트를 건너뛰려면 #[ignore]를 사용하세요.
// Unit tests in same file (convention: tests module)
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_ne!(add(2, 3), 6);
}
#[test]
fn test_with_message() {
let result = add(1, 1);
assert_eq!(result, 2, "Expected 2, got {}", result);
}
#[test]
#[should_panic(expected = "division by zero")]
fn test_panic() {
panic!("division by zero");
}
#[test]
fn test_result() -> Result<(), String> {
if add(2, 2) == 4 { Ok(()) }
else { Err(String::from("math is broken")) }
}
}
// Integration tests: tests/integration_test.rs
// Run: cargo test동시성 & 파일 I/O
스레드
std::thread::spawn은 OS 스레드를 만듭니다. 클로저는 변수를 캡처하면 'move'여야 하며, 스레드로 소유권을 이전합니다(use-after-free 방지). join()은 스레드가 완료될 때까지 차단하여 Result를 반환합니다. Rust의 소유권 시스템은 컴파일 타임에 데이터 레이스를 방지합니다 — 동기화 없이 스레드 간에 가변 데이터를 공유할 수 없습니다(Arc<Mutex<T>>).
use std::thread;
use std::time::Duration;
// spawn a thread
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread: {}", i);
thread::sleep(Duration::from_millis(10));
}
});
// main thread continues
for i in 0..3 {
println!("main thread: {}", i);
}
// wait for spawned thread to finish
handle.join().unwrap();
// move closure: transfer ownership to thread
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
println!("got data: {:?}", data); // data moved here
});
h.join().unwrap();채널 (메시지 전달)
채널은 메시지 전달을 통한 스레드 통신을 가능하게 합니다(Go의 모토: '통신으로 메모리 공유'). mpsc는 여러 송신자(tx.clone())를 허용하지만 하나의 수신자를 가집니다. send()는 Result를 반환합니다(수신자가 삭제되면 Err). 수신자는 Iterator를 구현하여, for 루프가 자연스럽게 작동합니다. 여러 수신자를 위해, crossbeam-channel이나 비동기 채널을 사용하세요. 메시지 전달은 공유 가변 상태의 복잡성을 피합니다.
use std::sync::mpsc;
use std::thread;
// mpsc: multiple producer, single consumer
let (tx, rx) = mpsc::channel();
// clone transmitter for multiple producers
let tx2 = tx.clone();
thread::spawn(move || {
let vals = vec!["a", "b", "c"];
for v in vals {
tx.send(v).unwrap();
}
});
thread::spawn(move || {
tx2.send("from tx2").unwrap();
});
// receiver is an iterator
for received in rx {
println!("Got: {}", received);
}Mutex & Arc (공유 상태)
Arc(원자 참조 카운트)는 스레드 간 공유 소유권을 가능하게 합니다(스레드 안전 Rc). Mutex는 배타적 접근을 제공합니다 — lock()은 획득할 때까지 차단하고, drop 시 잠금을 해제하는 MutexGuard를 반환합니다. RwLock은 여러 읽기 또는 하나의 쓰기를 허용합니다. Arc<Mutex<T>> 조합이 공유 가변 상태의 표준 패턴입니다. lock()의 unwrap()은 포이즌(잠금을 보유한 동안 스레드가 패닉)을 처리합니다.
use std::sync::{Arc, Mutex};
use std::thread;
// Arc: thread-safe reference counting
// Mutex: mutual exclusion lock
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// lock() returns MutexGuard, auto-unlocks when dropped
let mut num = counter.lock().unwrap();
*num += 1;
}); // lock released here
handles.push(handle);
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 10
// RwLock: multiple readers OR one writer
use std::sync::RwLock;
let data = RwLock::new(5);
let r1 = data.read().unwrap(); // shared read
let r2 = data.read().unwrap();
// let w = data.write().unwrap(); // would block until r1, r2 dropped파일 I/O
fs::read_to_string는 작은 파일에 편리합니다. 대용량 파일의 경우, 시스템 호출을 줄이기 위해 BufReader/BufWriter를 사용하세요. Read/Write 트레이트는 저수준 바이트 작업을 제공합니다. BufRead는 텍스트를 위한 lines()와 read_line()을 추가합니다. 항상 ?로 에러를 처리하세요(파일이 없음, 권한 거부, 디스크 가득 참). flush()는 버퍼링된 데이터가 OS에 도달함을 보장합니다(디스크에는 아닐 수 있음).
use std::fs;
use std::io::{Read, Write, BufReader, BufWriter};
use std::fs::File;
// read entire file
let content = fs::read_to_string("file.txt")?;
fs::write("output.txt", "Hello")?;
// buffered read (efficient for large files)
let file = File::open("file.txt")?;
let mut reader = BufReader::new(file);
let mut buf = String::new();
reader.read_to_string(&mut buf)?;
// line by line
use std::io::BufRead;
let file = File::open("file.txt")?;
for line in BufReader::new(file).lines() {
println!("{}", line?);
}
// buffered write
let file = File::create("out.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "Line 1")?;
writeln!(writer, "Line 2")?;
writer.flush()?; // ensure written to diskAsync/Await (Tokio)
Async/await는 OS 스레드 없이 효율적인 동시성을 가능하게 합니다 — 태스크는 스레드 풀에서 실행되고 .await 지점에서 양보합니다. tokio는 가장 인기 있는 비동기 런타임입니다. async fn은 반드시 .await되어야 하는 Future를 반환합니다. tokio::join!는 future를 동시에 실행하고 모두를 대기합니다. tokio::select!는 future를 경쟁시킵니다. 비동기는 I/O 바운드 작업(네트워크, 파일)에 이상적; CPU 바운드 작업에는 스레드를 사용하세요. .await는 스레드를 차단하지 않습니다 — 런타임으로 제어를 양보합니다.
use tokio::{fs, task, time};
use std::time::Duration;
// async fn returns a Future
async fn fetch_data() -> String {
time::sleep(Duration::from_secs(1)).await;
String::from("data")
}
// spawn concurrent tasks
#[tokio::main]
async fn main() {
// run tasks concurrently
let (a, b, c) = tokio::join!(
fetch_data(),
fetch_data(),
fetch_data()
);
println!("{} {} {}", a, b, c);
// spawn a background task
let handle = tokio::spawn(async {
let data = fs::read_to_string("file.txt").await.unwrap();
println!("Read {} bytes", data.len());
});
handle.await.unwrap();
// select: first to complete wins
tokio::select! {
val = fetch_data() => println!("Got: {}", val),
_ = time::sleep(Duration::from_millis(500)) => {
println!("Timeout!");
}
}
}라이프타임 심층
함수의 명명된 라이프타임
라이프타임은 컴파일러가 참조 유효성을 추적하는 방법입니다. 'a는 제네릭 라이프타임 매개변수입니다 — 런타임 동작이 아닌 컴파일 타임 검사만 변경합니다. 함수가 여러 참조를 받고 하나를 반환할 때, 반환된 참조가 입력보다 오래 살지 않음을 컴파일러가 알 수 있도록 라이프타임을 어노테이션해야 합니다. 이는 컴파일 타임에 댕글링 포인터를 방지합니다.
// Lifetimes tell the compiler how long references live
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y
let s1 = String::from("long string");
let result;
{
let s2 = String::from("hi");
result = longest(s1.as_str(), s2.as_str());
// result valid here
println!("{}", result);
}
// result NOT valid here — s2 is dropped라이프타임 생략 규칙
라이프타임 생략 규칙은 일반적인 경우에 명시적 라이프타임 어노테이션을 생략할 수 있게 합니다. 규칙 1은 각 참조 매개변수에 별개의 라이프타임을 할당합니다. 규칙 2는 입력 참조가 정확히 하나일 때 출력에 그 라이프타임을 할당합니다. 규칙 3은 메서드에 적용 — 출력이 &self의 라이프타임을 얻습니다. 이것들이 모든 참조를 해결하지 못할 때, 명시적 라이프타임을 작성해야 합니다. 대부분의 관용적 Rust 코드는 명시적 라이프타임 어노테이션이 거의 필요 없습니다.
// The compiler applies 3 elision rules automatically:
// 1. Each input reference gets its own lifetime
// 2. If one input lifetime, output gets that lifetime
// 3. If &self/&mut self, output gets self's lifetime
// These DON'T need explicit annotations (rule 2):
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' { return &s[0..i]; }
}
&s[..]
}
// This NEEDS annotation (multiple inputs, rule doesn't apply):
// fn longest<'a>(x: &'a str, y: &'a str) -> &'a str'static 라이프타임
'static은 가장 긴 라이프타임입니다 — 전체 프로그램 기간 지속합니다. 모든 문자열 리터럴은 바이너리에 내장되어 있으므로 이 라이프타임을 가집니다. T: 'static을 바운드로 볼 때, T가 영원히 살아야 함이 아니라 — T가 'static보다 짧은 참조를 포함하지 않아야 함을 의미합니다(즉, String, Vec, i64 같은 소유 타입은 항상 만족). 스레드는 호출 함수보다 오래 살 수 있으므로 'static이 필요합니다.
// 'static means the reference lives for the entire program
let s: &'static str = "I live forever";
// All string literals are &'static str
// Static variables (global, program-wide)
static COUNTER: AtomicUsize = AtomicUsize::new(0);
COUNTER.fetch_add(1, Ordering::SeqCst);
// 'static in bounds: T: 'static means T contains no
// non-static references (owned data always satisfies this)
fn spawn_thread<T: Send + 'static>(t: T) {
std::thread::spawn(move || {
drop(t);
});
}구조체의 라이프타임
구조체가 참조를 가질 때(소유 타입이 아닌), 그 참조가 유효한 기간을 선언하기 위해 라이프타임 매개변수가 필요합니다. 구조체 인스턴스는 빌린 데이터보다 오래 살 수 없습니다. 이는 제로 카피 파서, 빌린 데이터에 대한 이터레이터, 뷰에 일반적입니다. 라이프타임은 구조체와 impl 블록 모두에 선언되어야 합니다. 빌려야 할 특별한 이유가 없는 한 데이터를 소유하세요(String, Vec).
// Structs holding references need lifetime annotations
struct Parser<'a> {
text: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(text: &'a str) -> Self {
Parser { text, pos: 0 }
}
fn peek(&self) -> Option<char> {
self.text[self.pos..].chars().next()
}
fn advance(&mut self) {
self.pos += 1;
}
}
// The struct cannot outlive the text it borrows
let text = String::from("hello");
let mut p = Parser::new(&text);
p.advance();다중 라이프타임 & 서브타이핑
다르게 상호작용하는 여러 참조가 있는 함수는 여러 라이프타임 매개변수가 필요합니다. 라이프타임 서브타이핑은 더 긴 라이프타임이 더 짧은 것을 대체할 수 있음을 의미합니다(공변성) — &'static은 &'a가 예상되는 곳 어디나 사용 가능. 이것이 'static이 모든 라이프타임의 서브타입인 이유입니다. 출력의 라이프타임이 일부 입력에만 의존할 때 여러 라이프타임을 사용하여, 컴파일러에 더 많은 유연성과 호출자에게 더 적은 제약을 줍니다.
// Multiple lifetime parameters
fn parse<'src, 'ctx>(src: &'src str, ctx: &'ctx Context) -> &'src str {
// returns something tied to src, not ctx
src
}
// 'static: 'a is always true (static outlives everything)
fn static_ref<'a>(_: &'a str) -> &'static str {
"constant" // 'static coerces to 'a
}
// Variance: &'long can be used where &'short is expected
// (covariance) — longer lifetime is a subtype
fn use_ref<'short>(r: &'short str) {}
let long: &'static str = "hi";
use_ref(long); // OK: 'static coerces to any 'short스마트 포인터 (Box, Rc, Arc, RefCell)
Box<T> — 힙 할당
Box<T>는 Rust의 가장 단순한 스마트 포인터입니다 — 단일 소유권으로 값을 힙 할당합니다. 재귀 타입(컴파일 타임에 크기를 알 수 없는), 스택에서 이동하지 않을 대형 값, 동적 디스패치를 위한 트레이트 객체(Box<dyn Trait>)에 사용하세요. Box는 T로 역참조되어 내부 값처럼 동작합니다. 힙 할당 자체 외에는 본질적으로 제로 오버헤드입니다.
// Box moves data to the heap (single owner)
let b = Box::new(5);
println!("{}", b); // derefs automatically
// Recursive types NEED Box (size unknown at compile time)
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
// Trait objects (dynamic dispatch)
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }
// Box has zero runtime overhead when dereferencingRc<T> — 참조 카운팅 (단일 스레드)
Rc<T>(참조 카운트)는 단일 스레드 시나리오를 위한 다중 소유권을 가능하게 합니다. Rc::clone은 데이터를 복사하는 대신 참조 카운트를 증가시킵니다 — 저렴한 포인터 복사. 마지막 Rc가 삭제되면 값이 삭제됩니다. 공유 그래프 노드, 부모-자식 트리, 또는 여러 부분이 같은 데이터를 소유해야 하는 구조에 Rc를 사용하세요. Rc는 스레드 안전하지 않습니다 — 다중 스레딩에는 Arc를 사용하세요. Rc는 불변입니다 — 직접 변경할 수 없습니다.
use std::rc::Rc;
// Rc allows multiple owners via reference counting
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments count, doesn't copy
let c = Rc::clone(&a);
println!("count = {}", Rc::strong_count(&a)); // 3
// Data dropped when count reaches 0
// Shared graph/tree structures
struct Node {
children: Vec<Rc<Node>>,
value: i32,
}
let leaf = Rc::new(Node { children: vec![], value: 1 });
let branch = Rc::new(Node {
children: vec![Rc::clone(&leaf)],
value: 2,
});Arc<T> — 원자 참조 카운팅 (스레드 안전)
Arc<T>(원자 참조 카운트)는 Rc의 스레드 안전 대응물입니다. 참조 카운팅에 원자 연산을 사용하여, 스레드 간 공유가 안전합니다. 트레이드오프는 Rc보다 약간 더 많은 오버헤드입니다. 여러 스레드 간 공유 소유권이 필요할 때마다 Arc를 사용하세요. 공유 데이터를 변경하려면, Arc를 Mutex(배타적 접근용) 또는 RwLock(읽기 많은 접근용)과 결합하세요. Arc::clone은 저렴합니다 — 원자 카운터를 증가시킬 뿐.
use std::sync::Arc;
use std::thread;
// Arc: thread-safe version of Rc (atomic ref counting)
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let handles: Vec<_> = (0..3).map(|i| {
let data = Arc::clone(&data); // atomic increment
thread::spawn(move || {
println!("Thread {} sees: {:?}", i, *data);
})
}).collect();
for h in handles { h.join().unwrap(); }
// Arc is slightly slower than Rc due to atomic operations
// Only use Arc when actually sharing across threadsRefCell<T> — 내부 가변성
RefCell<T>는 내부 가변성을 제공합니다 — 공유 참조를 통해 변경할 수 있으며, 빌림 규칙이 컴파일 타임 대신 런타임에 강제됩니다. borrow()는 불변 참조를, borrow_mut()는 가변 참조를 반환합니다. 규칙 위반(예: 두 개의 가변 빌림)은 런타임에 패닉을 일으킵니다. 컴파일러가 빌림 안전성을 증명할 수 없을 때(예: 그래프 구조, 테스트의 모의 객체) RefCell을 사용하세요. Rc<RefCell<T>>가 가변 공유 단일 스레드 데이터의 고전적 패턴입니다.
use std::cell::RefCell;
// RefCell moves borrow checking to RUNTIME
let cell = RefCell::new(vec![1, 2, 3]);
// Multiple immutable borrows OR one mutable borrow
{
let mut borrowed = cell.borrow_mut();
borrowed.push(4);
} // borrow released here
{
let r1 = cell.borrow(); // OK
let r2 = cell.borrow(); // OK — multiple immutable
println!("{:?} {:?}", r1, r2);
}
// cell.borrow_mut() while r1 alive → PANIC at runtime
// Combine with Rc: Rc<RefCell<T>> for mutable shared graphsWeak<T> — 참조 순환 끊기
Weak<T>는 강한 참조 카운트에 영향을 주지 않는 비소유 참조입니다. 이것이 참조 순환을 끊는 데 필수적입니다: 부모가 자식을 소유(Rc)하고 자식이 부모를 소유(Rc)하면, 어느 쪽도 해제되지 않습니다 — 메모리 누수. 해결책은 역참조를 Weak로 만드는 것입니다. upgrade()는 Option<Rc<T>>를 반환합니다 — 값이 이미 삭제되었으면 None. 자식→부모 링크, 캐시, 데이터를 살려두고 싶지 않은 관찰자 패턴에 Weak를 사용하세요.
use std::rc::{Rc, Weak, RefCell};
// Weak references don't contribute to the strong count
// — prevents memory leaks in cycles
struct Node {
parent: RefCell<Weak<Node>>, // weak: child → parent
children: RefCell<Vec<Rc<Node>>>, // strong: parent → children
}
let leaf = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
// Upgrade returns Option — parent may already be dropped
if let Some(p) = leaf.parent.borrow().upgrade() {
println!("parent exists");
}트레이트 객체 & 동적 디스패치
dyn Trait — 동적 디스패치
dyn Trait은 동적 디스패치를 가능하게 합니다 — 구체 타입이 컴파일 타임에 지워지고 메서드 호출이 런타임에 vtable을 통과합니다. 이는 단일 컬렉션에 이질적 타입을 저장할 수 있게 합니다(Vec<Box<dyn Animal>>). 트레이드오프: 약간의 런타임 비용(vtable 간접, 인라인 없음)과 컴파일 타임에 타입을 알 수 없음. 구체 타입의 집합이 컴파일 타임에 알려지지 않거나 다른 타입을 함께 그룹화해야 할 때 트레이트 객체를 사용하세요.
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> String;
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> String { "Woof".into() }
}
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> String { "Meow".into() }
}
// dyn Trait = erased type, runtime dispatch via vtable
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: "Rex".into() }),
Box::new(Cat { name: "Whiskers".into() }),
];
for a in &animals {
println!("{} says {}", a.name(), a.sound());
}객체 안전 규칙
트레이트는 컴파일러가 vtable을 구축할 수 있을 때만 객체 안전입니다. 두 규칙: (1) 메서드는 Self를 반환하지 않아야 함(구체 타입이 지워지므로 알 수 없음), (2) 메서드는 제네릭 타입 매개변수를 가져서는 안 됨(vtable이 모든 가능한 타입에 대한 항목이 필요). Sized를 슈퍼트레이트로 가지는 트레이트도 객체 안전하지 않습니다. 객체 안전성이 필요하면, Self 반환 메서드를 Box<dyn Trait> 반환으로 리팩터링하거나 별도의 팩토리 함수를 사용하세요.
// Object-safe traits CAN be used as dyn Trait:
trait Draw {
fn draw(&self); // OK: &self, no generics
}
// NOT object-safe:
trait Bad {
fn create() -> Self; // returns Self — needs known type
fn process<T>(&self, x: T); // generic method — vtable can't cover all T
const SIZE: usize; // associated const (sometimes OK)
}
// Workaround for Self returns — use a factory or Box<Self>
trait GoodFactory {
fn new_boxed() -> Box<dyn GoodFactory>;
}
// Sized bound makes trait non-object-safe
// trait Foo: Sized {} // NOT object-safe트레이트 객체 vs 제네릭
제네릭은 모노모피제이션을 사용합니다 — 컴파일러가 각 구체 타입에 대해 함수의 별도 복사본을 생성하여, 정적 디스패치와 완전한 최적화(인라인)를 가능하게 합니다. 이는 런타임 비용이 없지만 바이너리 크기를 증가시킵니다. 트레이트 객체(dyn)는 vtable 조회가 있는 단일 함수를 사용합니다 — 더 작은 바이너리지만 호출당 약간의 런타임 비용. 성능이 중요하고 타입 집합이 작거나 알려진 경우 제네릭을; 이질적 컬렉션이 필요하거나 모든 타입을 미리 모를 때 dyn을 선택하세요.
// Generics: monomorphization, static dispatch, zero overhead
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Each type instantiation creates a separate function:
// max_generic::<i32>, max_generic::<f64>, etc.
// Trait objects: dynamic dispatch, single function
fn max_dyn(a: &dyn PartialOrd, b: &dyn PartialOrd) -> bool {
// can't return — don't know size at compile time
a.partial_cmp(b) == Some(std::cmp::Ordering::Less)
}
// Rule of thumb:
// - Few types, performance-critical → generics (static dispatch)
// - Many/unknown types, flexibility needed → dyn (dynamic dispatch)
// - Heterogeneous collections → must use dynAny 트레이트 & 다운캐스팅
Any 트레이트는 모든 타입의 값을 저장하고 다운캐스팅으로 런타임에 구체 타입을 복구할 수 있게 합니다. downcast_ref::<T>()는 Option<&T>를 반환하고, downcast::<T>()는 Result<Box<T>, Box<dyn Any>>를 반환합니다. 이것이 컴파일 타임에 타입을 진정으로 모를 때(플러그인 시스템, 동적 설정)를 위한 Rust의 탈출구입니다. 하지만 가능한 타입의 집합이 알려진 경우 열거형을 선호하세요 — 더 안전하고, 빠르며, 더 관용적입니다. Any는 모든 'static 타입에 구현되는 TypeId에 의존합니다.
use std::any::Any;
// Any enables runtime type checking & downcasting
let x: Box<dyn Any> = Box::new(42i32);
// downcast_ref returns Option<&T>
if let Some(n) = x.downcast_ref::<i32>() {
println!("It's an i32: {}", n);
}
// downcast returns Option<T> (for Box)
let boxed: Box<dyn Any> = Box::new("hello");
if let Ok(s) = boxed.downcast::<&str>() {
println!("Recovered: {}", s);
}
// Useful for: plugin systems, error types, heterogeneous storage
// Avoid overusing — prefer enums when the type set is known기본 트레이트 메서드 & 슈퍼트레이트
트레이트는 구현자가 재정의하거나 그대로 사용할 수 있는 기본 메서드 구현을 제공할 수 있습니다. 슈퍼트레이트(trait Named: Shape)는 구현 타입이 슈퍼트레이트도 구현해야 함을 요구합니다 — 이것이 Named 타입이 area()와 describe()를 가짐이 보장되는 계층을 만듭니다. 기본 메서드는 보일러플레이트를 줄이고, 트레이트에 메서드를 추가하면 기존의 모든 구현자를 깨지 않고 자동으로 이익을 주는 '확장 메서드' 패턴을 가능하게 합니다.
trait Shape {
fn area(&self) -> f64;
// Default method — can be overridden
fn describe(&self) -> String {
format!("Shape with area {:.2}", self.area())
}
}
// Supertrait: trait that requires another trait
trait Named: Shape {
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius.powi(2) }
}
impl Named for Circle {
fn name(&self) -> &str { "Circle" }
}
let c = Circle { radius: 2.0 };
println!("{}", c.describe()); // uses default method매크로 (선언적 & 절차적)
선언적 매크로 (macro_rules!)
macro_rules!는 패턴 매칭을 통해 컴파일 타임에 확장되는 선언적 매크로를 만듭니다. $(...),* 구문은 쉼표로 구분된 표현식을 0개 이상 매칭하는 반복 매처입니다. $x:expr는 '임의의 표현식을 매칭하고 x에 바인딩'을 의미합니다. 매크로는 타입 검사 전에 확장되므로, 모든 타입과 작동하는 코드를 생성할 수 있습니다. 제네릭이 처리할 수 없는 보일러플레이트(예: 가변 인수, 구문 확장)를 줄이기 위해 매크로를 사용하세요.
// macro_rules! defines pattern-matching macros
macro_rules! vec_of {
($($x:expr),*) => {{
let mut v = Vec::new();
$( v.push($x); )*
v
}};
}
let nums = vec_of!(1, 2, 3, 4);
// Recursive macro: build a HashMap
macro_rules! hashmap {
($($k:expr => $v:expr),*) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($k, $v); )*
m
}};
}
let config = hashmap!("host" => "localhost", "port" => 8080);매크로 프래그먼트 타입
프래그먼트 지정자는 매크로 인수가 어떤 종류의 구문을 매칭하는지 결정합니다. :ident는 식별자(이름)를, :expr는 표현식(값)을, :ty는 타입을, :block은 중괄호 블록을, :stmt는 문을, :literal은 리터럴을 매칭합니다. 올바른 지정자 선택이 중요합니다 — :expr가 가장 일반적이지만 함수/변수 이름을 만 들고 싶을 때 :ident가 필요합니다. 매크로 시스템은 위생적입니다: 매크로가 도입한 식별자는 주변 코드와 충돌하지 않습니다.
// Common fragment specifiers:
macro_rules! build_fn {
// $name:ident — identifier (function/variable name)
// $body:block — a block { ... }
// $ty:ty — a type
// $expr:expr — an expression
// $stmt:stmt — a statement
// $lit:literal — a literal (string, number)
($name:ident, $ret:ty, $body:block) => {
fn $name() -> $ret $body
};
}
build_fn!(get_answer, i32, { 42 });
println!("{}", get_answer());
// :pat — pattern, :path — module path
// :meta — attribute meta-item, :vis — visibility내장 표준 매크로
Rust는 많은 내장 매크로와 함께 제공됩니다. println!/eprintln!는 stdout/stderr에 출력합니다. dbg!는 표현식의 값을 파일/라인 정보와 함께 출력합니다 — 디버깅에 훌륭합니다(값도 반환합니다). assert!/assert_eq!/assert_ne!는 테스트와 불변성을 위한 것입니다. todo!/unimplemented!는 패닉과 함께 미완성 코드를 표시합니다. file!/line!/module!는 컴파일 타임 위치 정보를 제공합니다. env!/option_env!는 컴파일 타임에 환경 변수를 읽습니다 — 버전 정보 임베딩에 유용.
// Formatting & printing
println!("x = {}, y = {:?}", 1, "two");
eprintln!("Error: {}", "oops"); // stderr
format!("{}-{}", "a", "b"); // returns String
// Debug helpers
dbg!(2 + 2); // prints [src.rs:1] 2 + 2 = 4
assert!(1 + 1 == 2); // panic if false
assert_eq!(2 + 2, 4); // panic if not equal
assert_ne!(1, 2); // panic if equal
// Code generation
todo!("not implemented yet"); // unimplemented!()
unimplemented!();
panic!("fatal: {}", "reason");
// Environment & file info
println!("{}:{}", file!(), line!()); // src.rs:1
println!("{}", env!("CARGO_PKG_NAME"));절차적 매크로 개요
절차적 매크로(proc macros)는 TokenStream을 입력으로 받아 TokenStream을 출력으로 생성하는 Rust 함수입니다 — 완전한 코드-대-코드 변환. 선언적 매크로와 달리, 임의의 계산을 수행할 수 있습니다. 세 가지 타입: derive 매크로(#[derive]로 트레이트 구현 추가), 속성 매크로(항목에 어노테이션), 함수 유사 매크로(sqlx::query! 같은 커스텀 구문). proc-macro = true인 별도의 크레이트에 있어야 합니다. syn 크레이트가 Rust 구문을 파싱하고, quote!가 코드를 생성합니다. 인기 있는 예: serde, tokio, thiserror.
// Procedural macros are Rust functions that transform code
// Three kinds (must be in a separate crate with proc-macro=true):
// 1. Derive macros — #[derive(MyTrait)]
#[derive(Debug, Clone, MyTrait)]
struct Point { x: f64, y: f64 }
// 2. Attribute macros — #[my_attr]
#[my_attr]
fn function() {}
// 3. Function-like macros — my_macro!(...)
sqlx::query!("SELECT * FROM users");
// Cargo.toml for a proc-macro crate:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "2.0" # parse Rust code
// quote = "1.0" # generate code
// proc-macro2 = "1.0"매크로 위생 & 일반 패턴
Rust 매크로는 위생적입니다 — 매크로 내부에서 생성된 식별자는 별도의 '구문 컨텍스트'에 존재하며 호출 범위의 변수를 실수로 캡처하거나 섀도잉하지 않습니다. 이것이 매크로의 내부 변수 이름이 호출자의 것과 충돌하는 미묘한 버그를 방지합니다. stringify!는 임의의 토큰 스트림을 컴파일 타임에 문자열 리터럴로 변환합니다(에러 메시지에 유용). cfg_debug!는 일반적인 패턴을 보여줍니다: cfg! 시스템을 사용하여 빌드 설정을 기반으로 코드를 조건부 컴파일.
// Hygiene: macro-introduced identifiers don't leak
macro_rules! using_temp {
($e:expr) => {
let temp = $e; // this 'temp' is distinct from caller's
println!("{}", temp);
};
}
let temp = 10;
using_temp!(temp + 5); // no conflict — hygienic
// Conditional compilation macro
macro_rules! cfg_debug {
($($e:tt)*) => {
#[cfg(debug_assertions)]
{ $($e)* }
};
}
cfg_debug! {
println!("Debug mode on");
}
// stringify! converts tokens to a string literal
let s = stringify!(a + b * c); // "a + b * c"Unsafe Rust
원시 포인터
원시 포인터(*const T, *mut T)는 빌림 검사기로부터의 Rust의 탈출구입니다. 참조와 달리, null일 수 있고, 별칭(같은 데이터에 대한 여러 포인터)을 가질 수 있으며, 라이프타임을 추적하지 않습니다. 생성은 안전하지만, 역참조는 컴파일러가 유효성을 보장할 수 없으므로 unsafe가 필요합니다. FFI(C와 인터페이스), 저수준 데이터 구조(연결 리스트, 벡터) 구현, 수동으로 안전성을 보장하는 성능 중심 코드를 위해 원시 포인터를 사용하세요. 항상 unsafe가 건전한 이유를 문서화하세요.
// Raw pointers: *const T (immutable) and *mut T (mutable)
let x = 42;
let r1: *const i32 = &x; // coerce from reference
let r2: *mut i32 = x as *mut i32; // cast
// Can be null, can alias, no borrow checking
let null: *const i32 = std::ptr::null();
// Creating raw pointers is safe, but DEREFERENCING is unsafe
unsafe {
println!("r1 = {}", *r1);
}
// Convert between types (transmute-like)
let bytes: [u8; 4] = [0x78, 0x56, 0x34, 0x12];
let ptr = bytes.as_ptr() as *const u32;
unsafe { println!("0x{:x}", *ptr); } // little-endian intUnsafe 블록 & 함수
unsafe는 빌림 검사기를 끄는 것이 아니라 — 5가지 특정 작업을 할 수 있게 합니다: (1) 원시 포인터 역참조, (2) unsafe 함수 호출, (3) unsafe 트레이트 구현, (4) static mut 접근/변경, (5) union 필드 접근. unsafe 블록은 unsafe 작업을 명시적이고 국소적으로 만듭니다. unsafe fn은 함수 호출이 컴파일러가 검사할 수 없는 불변성을 유지해야 함을 선언합니다. get_unchecked는 성능을 위해 경계 검사를 건너뜁니다 — 인덱스를 검증한 경우에만 안전. unsafe 영역을 최소화하고 안전한 API 뒤에 캡슐화하세요.
// unsafe block: a localized unsafe region
let ptr: *const i32 = &42;
let val = unsafe { *ptr };
// unsafe fn: the ENTIRE function body is unsafe
unsafe fn dangerous(ptr: *const u8) -> u8 {
*ptr
}
// Callers must use unsafe block:
let b = 5u8;
unsafe { dangerous(&b) };
// Splitting borrows safely (compiler is conservative)
let mut v = vec![1, 2, 3, 4];
let len = v.len();
unsafe {
let first = v.get_unchecked(0); // no bounds check
let last = v.get_unchecked(len - 1);
println!("{} {}", first, last);
}FFI — C 함수 호출
FFI(외부 함수 인터페이스)는 Rust가 C 함수를 호출하고 그 반대도 가능하게 합니다. extern "C" 블록은 외부 C 함수를 선언합니다 — 호출은 unsafe인데, 컴파일러가 동작을 검증할 수 없기 때문입니다. #[no_mangle]은 Rust가 함수 이름을 바꾸는 것을 방지하여 C가 이름으로 찾을 수 있게 합니다. #[repr(C)]는 구조체 레이아웃이 C의 메모리 레이아웃과 일치함을 보장합니다(Rust는 효율성을 위해 기본적으로 필드를 재정렬할 수 있음). 시스템 호출, 레거시 라이브러리, 성능 중심 바인딩을 위해 FFI를 사용하세요. bindgen 크레이트가 C 헤더에서 FFI 선언을 자동 생성합니다.
// extern "C" declares foreign functions
extern "C" {
fn abs(x: i32) -> i32;
}
fn main() {
let x = -5;
let positive = unsafe { abs(x) };
println!("{}", positive); // 5
}
// Exporting Rust functions to C
#[no_mangle] // prevent name mangling
pub extern "C" fn add(a: i64, b: i64) -> i64 {
a + b
}
// C-compatible struct
#[repr(C)]
struct Point { x: f64, y: f64 }Unsafe 트레이트 구현
Unsafe 트레이트(Send, Sync 같은)는 구현자가 컴파일러가 검증할 수 없는 불변성을 유지해야 합니다. Send는 타입이 스레드 간 안전하게 이동할 수 있음을; Sync는 &T가 스레드 간 공유될 수 있음을 의미합니다. 컴파일러는 대부분 타입에 대해 이를 자동 파생하지만, 원시 포인터는 기본적으로 Send/Sync가 아닙니다. 수동으로 구 현할 때, 스레드 안전성에 대한 책임을 집니다. static mut는 여러 스레드가 경쟁할 수 있으므로 unsafe 접근이 필요 — 대신 원자(AtomicU64)나 Mutex를 선호하세요. 항상 SAFETY 주석으로 안전성 근거를 문서화하세요.
// Some traits are unsafe to implement — the compiler can't verify invariants
use std::marker::Send;
// Send/Sync are auto-implemented, but sometimes you must manually impl
struct RawPointer<T>(*mut T);
// SAFETY: We guarantee the pointer is only used on one thread
unsafe impl<T> Send for RawPointer<T> where T: Send {}
// Splitting a slice into disjoint mutable parts
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = v.split_at_mut(3);
// left = [1,2,3], right = [4,5,6] — disjoint, but compiler
// couldn't prove this without unsafe internally
// Global mutable state
static mut COUNTER: u64 = 0;
unsafe { COUNTER += 1; } // unsafe: data races possibleUnion & 인라인 어셈블리
Union은 다른 타입이 같은 메모리 위치를 공유하게 합니다 — 마지막으로 쓰여지지 않은 필드를 읽으면 정의되지 않은 동작이므로 unsafe입니다. 주로 C와의 FFI를 위한 것입니다. transmute는 한 타입의 비트 패턴을 같은 크기의 다른 타입으로 재해석 캐스트합니다 — 크기가 다르거나 타입이 호환되지 않으면 극히 위험합니다. 인라인 어셈블리(asm!)는 CPU 명령을 직접 임베드할 수 있게 하여, 커널 개발과 극한 최적화에 유용합니다. 이 모든 것은 날카로운 도구입니다: 안전한 대안이 없을 때만 사용하고, 안전한 추상 뒤에 캡슐화하세요.
// Unions: multiple fields share the same memory (like C unions)
#[repr(C)]
union IntOrFloat {
i: i32,
f: f32,
}
let mut u = IntOrFloat { i: 42 };
unsafe { println!("as int: {}", u.i); }
u.f = 3.14;
unsafe { println!("as float: {}", u.f); }
// Reading the WRONG field is undefined behavior!
// Inline assembly (nightly / asm!)
#[cfg(feature = "asm")]
unsafe fn halt() {
std::arch::asm!("hlt", options(nostack));
}
// Transmute: reinterpret bits as another type (same size)
let bits: u32 = 0x40490FDB; // ~3.14159 in IEEE 754
let pi: f32 = unsafe { std::mem::transmute(bits) };이터레이터 심층
Iterator 트레이트 & 생성
Iterator 트레이트는 Option<Item>을 반환하는 next() 메서드만 필요합니다 — None은 소진을 알립니다. 나머지 모든 것(map, filter, collect)은 그 위에 구축됩니다. iter()는 요소를 빌리 고(&T), into_iter()는 컬렉션을 소비합니다(소유 T 반환), iter_mut()는 &mut T를 반환합니다. 범위(1..5, 1..=5)는 직접 이터레이터입니다. 문자열은 char(Unicode 스칼라 값) 또는 바이트로 순회합니다. 이터레이터는 지연입니다 — 소비할 때까지 아무것도 실행되지 않습니다.
// The Iterator trait: one required method
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ... many provided methods (map, filter, etc.)
}
// Creating iterators
let v = vec![1, 2, 3];
let iter = v.iter(); // borrows: &i32
let into_iter = v.into_iter(); // owns: i32 (consumes v)
let mut_iter = v.iter_mut(); // mutably borrows: &mut i32
// Range is an iterator
for i in 1..=5 { print!("{} ", i); } // 1 2 3 4 5
// String iteration
for c in "héllo".chars() { print!("{} ", c); } // h é l l o
for b in "hi".bytes() { print!("{} ", b); } // 104 105어댑터 메서드 (지연)
어댑터 메서드는 이터레이터를 변환하여 새 이터레이터를 반환합니다 — 지연이므로, map().filter().map() 체이닝은 중간 컬렉션을 0개 생성합니다. map은 각 요소에 함수를 적용합니다. filter는 술어가 true를 반환하는 요소를 유지합니다. take(n)는 n개 요소 후 중지합니다(무한 이터레이터에 유용). skip(n)은 처음 n개를 버립니다. flat_map은 매핑하고 중첩된 이터레이터를 평탄화합니다. enumerate는 각 요소에 인덱스를 짝지음. 소비자(collect, sum, for 루프)가 이터레이터를 구동할 때까지 아무것도 실행되지 않습니다.
let nums = vec![1, 2, 3, 4, 5, 6];
// map: transform each element
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();
// filter: keep elements matching predicate
let evens: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// take / skip: limit or skip elements
let first3: Vec<_> = nums.iter().take(3).collect(); // [1,2,3]
let after2: Vec<_> = nums.iter().skip(2).collect(); // [3,4,5,6]
// flat_map: map then flatten one level
let words: Vec<_> = ["a b", "c"].iter()
.flat_map(|s| s.split(' '))
.collect(); // ["a","b","c"]
// enumerate: add index
for (i, v) in nums.iter().enumerate() {
println!("{}: {}", i, v);
}소비자 메서드
소비자 메서드는 지연 이터레이터 체인을 실제로 실행되게 구동합니다. collect()는 FromIterator를 구현하는 임의의 컬렉션(Vec, HashMap, String 등)으로 결과를 모읍니다. sum/product/count/fold는 이터레이터를 단일 값으로 줄입니다. find/any/all은 단락 — 답을 아는 즉시 중지하므로 무한 이터레이터에서 효율적입니다. min/max는 Option 반환(빈 이터레이터의 경우 None). 지연 어댑터 + 최종 소비자의 조합은 최적화 후 이터레이터 체인이 손으로 쓴 루프만큼 효율적임을 의미합니다.
let nums = vec![1, 2, 3, 4, 5];
// collect: gather into a collection
let v: Vec<i32> = nums.iter().copied().collect();
let s: std::collections::HashSet<i32> = nums.iter().copied().collect();
// sum, product, count
let total: i32 = nums.iter().sum(); // 15
let prod: i32 = nums.iter().product(); // 120
let count = nums.iter().count(); // 5
// reduce / fold: accumulate into a single value
let max = nums.iter().copied().reduce(i32::max); // Some(5)
let sum = nums.iter().fold(0, |acc, &x| acc + x); // 15
// find / any / all: short-circuiting
let first_even = nums.iter().find(|&&x| x % 2 == 0); // Some(2)
let has_neg = nums.iter().any(|&x| *x < 0); // false
let all_pos = nums.iter().all(|&x| *x > 0); // true
// min / max
nums.iter().copied().min(); // Some(1)
nums.iter().copied().max(); // Some(5)커스텀 이터레이터
커스텀 이터레이터를 만들려면, next() 메서드로 Iterator 트레이트를 구현하세요. 그러면 70개 이상의 어댑터와 소비자 메서드를 무료로 받습니다. 이터레이터는 자체 상태(현재 위치 등)를 추적하고 소진 시 None을 반환해야 합니다. 컬렉션 타입에 대해 IntoIterator를 구현하면 for-루프 구문이 가능합니다. 양방향이나 임의 접근을 위해, DoubleEndedIterator나 ExactSizeIterator도 구현하세요. 이것이 Vec, HashMap, Range 및 모든 표준 컬렉션이 순회를 제공하는 방식입니다.
// Build a custom iterator for a counter
struct Counter {
current: usize,
max: usize,
}
impl Counter {
fn new(max: usize) -> Self {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let val = self.current;
self.current += 1;
Some(val)
} else {
None
}
}
}
// Now all iterator methods work!
let sum: usize = Counter::new(5).sum(); // 0+1+2+3+4 = 10
let doubled: Vec<_> = Counter::new(3).map(|x| x * 10).collect();무한 & 체인 이터레이터
Rust 이터레이터는 무한할 수 있습니다 — (1..)은 영원히 자연수를 생성하고, repeat(x)는 끝없이 반복합니다. 어댑터가 지연이므로 이것은 안전합니다: take(n)이 소비를 제한합니다. cycle()은 유한 이터레이터를 무한히 반복합니다. chain()은 이터레이터를 순차적으로 연결합니다. zip()은 요소를 위치별로 짝지음(짧은 쪽에서 중지). peekable()은 소비하지 않고 다음 요소를 살펴보게 합니다 — 파서에 유용. 지연 설계는 무한 이터레이터가 소비 전까지 비용이 없음을 의미하며, 컴파일러는 체인을 타이트한 루프로 최적화합니다.
use std::iter;
// Infinite iterators (use with take!)
let naturals = (1..).take(10); // 1..10
let zeros = iter::repeat(0).take(5); // [0,0,0,0,0]
let alternating = iter::repeat_with(|| rand::random::<u8>());
// cycle: repeat a finite iterator infinitely
let pattern = [1, 2, 3].iter().cycle().take(7);
// [1,2,3,1,2,3,1]
// chain: concatenate two iterators
let combined = [1, 2].iter().chain([3, 4].iter());
// [1,2,3,4]
// zip: pair elements from two iterators
let pairs: Vec<_> = [1, 2, 3].iter().zip(['a', 'b', 'c']).collect();
// [(1,'a'), (2,'b'), (3,'c')]
// peekable: look ahead without consuming
let mut iter = [1, 2, 3].iter().peekable();
if let Some(&&first) = iter.peek() { println!("{}", first); }에러 처리 심층
Result & Option 컴비네이터
컴비네이터는 중첩된 match 표현식 없이 실패 가능한 작업을 체인하게 합니다. map은 Ok 값을 변환하고, map_err는 에러를 변환합니다. and_then은 자체가 Result를 반환하는 작업을 체인합니다(에러용 flatmap). ok_or는 Option→Result로 변환합니다. unwrap_or/unwrap_or_else/unwrap_or_default는 대체 값을 제공합니다. 이들은 우아하게 조합됩니다: parse().map().and_then().map_err()는 각 단계가 실패할 수 있는 파이프라인을 만들고, 첫 번째 실패가 단락됩니다. 프로덕션 코드에서 unwrap() 대신 컴비네이터를 선호하세요.
// Result combinators chain operations without match
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>().map(|n| n * 2)
}
// and_then: chain fallible operations
fn validate(n: i32) -> Result<i32, String> {
if n > 0 { Ok(n) } else { Err("must be positive".into()) }
}
let result = "5".parse::<i32>().and_then(validate); // Ok(5)
// map_err: transform the error type
let r = "abc".parse::<i32>()
.map_err(|e| format!("Parse failed: {}", e));
// ok_or / ok_or_else: convert Option to Result
let opt: Option<i32> = None;
let val = opt.ok_or("missing value"); // Err("missing value")
// unwrap_or / unwrap_or_else / unwrap_or_default
let x: i32 = "abc".parse().unwrap_or(0);커스텀 에러 타입
커스텀 에러 타입은 도메인별 실패를 표현하게 합니다. 핵심 패턴: 각 기본 에러 타입에 대해 From을 구현하여 ? 연산자가 자동 변환하게 합니다. 이는 명시적 map_err 없이 std::io::Error, ParseIntError 등과 ?를 사용할 수 있음을 의미합니다. Display 구현은 에러를 사용자 친화적으로 만들고; Debug는 개발자용입니다. 열거형 기반 에러가 Rust에서 관용적입니다 — 철저하고(컴파일러가 누락된 케이스 경고) 제로 비용(힙 할당 없음)입니다. thiserror 사용 전의 기반입니다.
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
Unauthorized,
}
// Implement From for automatic conversion with ?
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::NotFound(s) => write!(f, "Not found: {}", s),
AppError::Unauthorized => write!(f, "Unauthorized"),
}
}
}
// Now ? works for io::Error and ParseIntError automatically
fn read_config() -> Result<i32, AppError> {
let s = std::fs::read_to_string("config.txt")?; // io → AppError
let n: i32 = s.trim().parse()?; // parse → AppError
Ok(n)
}Error 트레이트 & Box<dyn Error>
std::error::Error는 에러 타입을 위한 표준 라이브러리 트레이트입니다(Debug + Display 필요). Box<dyn Error>는 가장 단순한 에러 타입입니다 — ?로 모든 에러를 받아들이며, 특정 에러를 프로그래밍적으로 처리할 필요 없는 프로토타이핑이나 애플리케이션에 훌륭합니다. 단점: 구체 에러 타입을 잃으므로, 특정 변형 매칭은 downcast_ref가 필요합니다. 라이브러리의 경우, 구체 열거형 에러 타입을 선호하세요(thiserror와 함께). 애플리케이션의 경우, 백트레이스와 에러 체인을 보존하므로 Box<dyn Error>보다 anyhow가 더 나은 선택입니다.
use std::error::Error;
// std::error::Error is the trait for all errors
// Requires: Debug + Display
fn do_something() -> Result<(), Box<dyn Error>> {
let f = std::fs::read_to_string("file.txt")?; // io::Error
let n: i32 = f.parse()?; // ParseIntError
println!("{}", n);
Ok(())
}
// Box<dyn Error> is the quick-and-dirty error type
// — accepts any error via ?, but loses specific type info
// downcast to recover specific error type
match do_something() {
Err(e) => {
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
println!("IO: {}", io_err);
}
}
_ => {}
}thiserror 크레이트 (라이브러리 에러)
thiserror는 라이브러리 에러 타입을 위한 표준 크레이트입니다. #[derive(Error)] 매크로가 Display(#[error("..."]에서)와 From(#[from]에서) 구현을 자동 생성합니다. #[from]은 ?가 기본 에러를 열거형 변형으로 변환하게 만듭니다. 이것은 강력한 타입의 철저한 에러 열거형을 유지하면서 보일러플레이트를 제거합니다. 라이브러리(호출자가 특정 에러를 매칭해야 하는)에 thiserror를 사용하세요. {0} 자리표시자는 내부 에러의 Display를 삽입합니다; {id} 같은 명명된 필드는 구조체 필드를 삽입합니다.
use thiserror::Error;
// thiserror auto-generates Display and From impls
#[derive(Debug, Error)]
enum DataError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("parse failed: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("item {id} not found")]
NotFound { id: u32 },
#[error("invalid state: {msg}")]
Invalid { msg: String },
}
// #[from] auto-implements From, so ? just works:
fn load() -> Result<i32, DataError> {
let s = std::fs::read_to_string("data.txt")?; // auto-converts
let n: i32 = s.trim().parse()?;
Ok(n)
}anyhow 크레이트 (애플리케이션 에러)
anyhow는 애플리케이션/바이너리 에러 처리를 위한 표준 크레이트입니다. anyhow::Error는 std::error::Error를 구현하는 임의의 에러를 래핑하고 컨텍스트, 백트레이스, 에러 체이닝을 추가합니다. context()는 각 실패 가능한 단계에 사람이 읽기 쉬운 메시지를 첨부하여, 'Failed to read config: IO error: No such file' 같은 체인을 만듭니다. 이는 디버깅을 훨씬 쉽게 만듭니다 — 정확히 어느 단계가 실패했고 왜 실패했는지 봅니 다. main()과 에러를 보고하기만 하면 되는 애플리케이션 코드에 anyhow를 사용하세요. 호출자가 타입화된 에러가 필요한 라이브러리에는 thiserror를 사용하세요.
use anyhow::{Context, Result, anyhow};
// anyhow::Result = Result<T, anyhow::Error>
fn load_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")
.context("Failed to read config.toml")?; // add context
Ok(content)
}
fn main() -> Result<()> {
let config = load_config()?;
if config.is_empty() {
// bail! / anyhow! create errors with format! syntax
return Err(anyhow!("config is empty"));
}
println!("{}", config);
Ok(())
}
// Error chain: "Failed to read config.toml: No such file..."
// anyhow preserves the full chain and backtraceCargo & 크레이트 심층
Cargo.toml 구조
Cargo.toml은 Rust 프로젝트의 매니페스트입니다. [package]는 크레이트 메타데이터를 설명합니다. [dependencies]는 외부 크레이트를 나열합니다 — 버전 문자열은 semver를 사용합니다(^1.0은 >=1.0, <2.0 의미). features는 선택적 기능을 활성화합니다(serde의 derive 기능은 #[derive(Serialize)]를 켬). [dev-dependencies]는 테스트/벤치마크 전용입니다. [features]는 조건부 컴파일 플래그를 정의합니다. [profile.release]는 최적화 설정을 제어합니다. edition 필드(2015/2018/2021)는 언어 기능을 결정합니다 — 항상 최신을 사용하세요.
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
authors = ["You <[email protected]>"]
license = "MIT"
description = "A sample app"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", optional = true }
rand = "0.8"
[dev-dependencies]
criterion = "0.5" # only for tests/benches
[features]
default = ["tokio"]
async-mode = ["dep:tokio"]
[[bin]]
name = "myapp"
path = "src/main.rs"
[profile.release]
opt-level = 3
lto = true
strip = true종속성 & 기능 플래그
기능 플래그는 조건부 컴파일을 가능하게 합니다. 각 종속성은 기능을 노출할 수 있습니다(예: serde의 derive). default-features = false는 바이너리 크기를 줄이기 위해 기본 기능을 제거합니다. 선택적 종속성(optional = true)은 기능이 dep:name으로 활성화할 때만 컴파일됩니다. 기능은 가산적입니다 — 켜는 것이지 끄는 것이 아닙니다. 이는 기능 통합을 보장합니다: 두 종속성이 serde의 다른 기능을 활성화하면, Cargo가 모든 기능의 합집합으로 serde를 한 번 컴파일합니다. 코드를 조건부 컴파일하려면 #[cfg(feature = "x")]를 사용하세요.
# Cargo.toml
[dependencies]
# Version requirements: ^1.2 (compatible), =1.2.3 (exact), >=1.0,<2.0 (range)
serde = "1.0" # ^1.0 (default: compatible)
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", default-features = false } # disable defaults
# Optional dependencies (enabled by a feature)
tokio = { version = "1", optional = true }
[features]
# "async" feature enables the optional tokio dep
async = ["dep:tokio"]
# Features can enable other features
full = ["async", "serde/derive"]
# In code:
# #[cfg(feature = "async")]
# fn run_async() { ... }워크스페이스 (다중 크레이트 프로젝트)
워크스페이스는 Cargo.lock과 target 디렉토리를 공유하는 여러 관련 크레이트를 그룹화합니다. 이는 빌드 속도를 높이고(공유 컴파일 캐시) 모든 크레이트가 같은 종속성 버전을 사용함을 보장합니다. 멤버는 path = "../core"로 서로 종속될 수 있습니다. [workspace.dependencies]는 버전 관리를 중앙화합니다 — 멤버 크레이트는 { workspace = true }로 참조합니다. resolver = "2"(edition 2021의 기본값)는 타겟별 기능 통합을 사용하여, 일부 빌드 문제를 방지합니다. 모노레포, 여러 컴포넌트가 있는 라이브러리, 또는 core/CLI/server를 분할하는 프로젝트에 워크스페이스를 사용하세요.
# Root Cargo.toml — workspace manifest
[workspace]
members = [
"core",
"cli",
"server",
"utils",
]
resolver = "2"
# Shared dependencies across all members
[workspace.dependencies]
serde = "1.0"
tokio = "1"
# In member crates (e.g., cli/Cargo.toml):
# [dependencies]
# serde = { workspace = true }
# core = { path = "../core" } # local path dependency빌드 프로필 & 최적화
프로필은 cargo가 프로젝트를 빌드하는 방법을 제어합니다. dev(cargo build의 기본값)는 컴파일 속도를 우선합니다. release(cargo build --release)는 런타임 성능을 우선합니다. 주요 노브: opt-level(0-3, 크기용 's', 최소 크기용 'z'), lto(크레이트 경계 너머 링크 타임 최적화), codegen-units(1 = 최고 최적화지만 가장 느린 컴파일), strip(더 작은 바이너리를 위해 심볼 제거), panic = 'abort'(언와인딩 비활성화, 더 작은 바이너리). 프로덕션의 경우 lto = true, codegen-units = 1, strip = true를 사용하세요. 커스텀 프로필은 기존 것에서 상속됩니다.
# Cargo.toml
[profile.dev]
opt-level = 0 # no optimization (fast compile)
debug = true # include debug symbols
overflow-checks = true
[profile.release]
opt-level = 3 # max optimization
lto = "fat" # link-time optimization across crates
codegen-units = 1 # single codegen unit (better opt, slower compile)
strip = true # strip debug symbols from binary
panic = "abort" # smaller binary, no unwinding
[profile.release.package."*"]
opt-level = 2 # optimize dependencies less than your code
# Custom profile
[profile.bench]
inherits = "release"
debug = true # keep symbols for profiling
# Usage: cargo build --release --profile bench게시 & 문서화
crates.io에 게시는 영구적입니다 — 버전은 덮어쓰거나 삭제할 수 없습니다(yank만 가능, 새 종속자를 방지). name, version, description, license, repository가 설정되어 있는지 확인하세요. cargo package는 매니페스트를 검증하고 게시될 내용을 보여줍니다. cargo doc는 /// doc 주석에서 HTML 문서를 생성합니다 — doc 테스트(``` 블록의 코드)는 cargo test로 컴파일되고 실행됩니다. 예제가 있는 좋은 doc 주석은 문서이자 테스트입니다. #[doc(hidden)]로 내부 항목을 숨기세요.
# Before publishing:
# 1. Login (one-time)
# $ cargo login <token> (token from crates.io)
# 2. Check the package
# $ cargo package # creates .crate file, checks metadata
# $ cargo publish --dry-run
# 3. Publish
# $ cargo publish # uploads to crates.io (irreversible!)
# Required fields in Cargo.toml for publishing:
# name, version, description, license, repository
# Documentation:
# $ cargo doc # generate docs for your crate + deps
# $ cargo doc --open # generate and open in browser
# $ cargo doc --no-deps # only your crate
# Doc tests run automatically:
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }트레이트 객체 & 동적 디스패치
dyn Trait 기본
트레이트 객체(dyn Trait)는 런타임 다형성을 가능하게 합니다: 단일 변수가 같은 트레이트를 구현하는 다른 구체 타입을 가질 수 있습니다. 컴파일러는 타입별 vtable(가상 메서드 테이블)을 생성하고, 메서드 호출은 vtable을 통과합니다(동적 디스패치). 약간의 런타임 비용이 있지만 이질적 컬렉션을 가능하게 합니다. 구체 타입이 컴파일 타임에 알려지지 않거나 변할 때 트레이트 객체를 사용하세요.
trait Draw {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Draw for Circle {
fn draw(&self) { println!("Circle r={}", self.radius); }
}
impl Draw for Square {
fn draw(&self) { println!("Square s={}", self.side); }
}
// Trait object: type-erased, dynamic dispatch
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }
// Function taking trait object
fn render(shape: &dyn Draw) { shape.draw(); }객체 안전성
트레이트는 다음 경우에만 객체 안전(dyn Trait으로 사용 가능)합니다: Self를 반환하는 메서드가 없고, Self를 값으로 받는 메서드가 없고, 제네릭 메서드가 없으며, 모든 메서드가 디스패치 가능합니다. Clone, Default, From은 객체 안전하지 않습니다. 해결 방법으로 Box<Self> 반환(box_clone 패턴), 트레이트 분할, 또는 열거형으로 정적 디스패치 사 용이 있습니다. 컴파일러는 객체 안전성 위반을 명확히 보고합니다.
// Object-safe trait (can be made into dyn)
trait Animal {
fn sound(&self) -> String;
fn name(&self) -> &str;
}
// NOT object-safe: returns Self
trait Clone {
fn clone(&self) -> Self; // Self is unknown for dyn
}
// NOT object-safe: takes Self by value
trait Add {
fn add(&self, other: Self) -> Self;
}
// NOT object-safe: generic method
trait From {
fn from<T>(t: T) -> Self;
}
// Fix: use where clauses or separate traits
trait AnimalSafe {
fn sound(&self) -> String;
fn box_clone(&self) -> Box<dyn AnimalSafe>;
}정적 vs 동적 디스패치
정적 디스패치(트레이트 바운드가 있는 제네릭)는 모노모피화합니다: 컴파일러가 구체 타입별로 특화된 버전을 생성하여, 인라인과 바이너리 크기 비용으로 최대 성능을 가능하게 합니다. 동적 디스패치(dyn Trait)는 런타임에 vtable 조회를 사용하여, 더 작은 바이너리지만 더 느린 호출(인라인 방지). 성능 중심 코드에는 정적 디스패치를 선호하세요; 이질적 컬렉션과 플러그인 시스템에는 동적 디스패치를 사용하세요.
// Static dispatch (monomorphization)
fn max<T: Ord>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Compiler generates max_i32, max_f64, etc.
// Dynamic dispatch (vtable lookup)
fn max_dyn(a: &dyn Ord, b: &dyn Ord) -> bool {
// a > b // Cannot use operators on dyn
false
}
// Trait bound (static)
fn process<T: Display>(item: &T) {
println!("{}", item);
}
// impl Trait (static, syntactic sugar)
fn process2(item: &impl Display) {
println!("{}", item);
}
// dyn Trait (dynamic)
fn process3(item: &dyn Display) {
println!("{}", item);
}라이프타임이 있는 트레이트 객체
트레이트 객체는 라이프타임 바운드를 가질 수 있습니다: Box<dyn Trait + 'a>는 트레이트 객체(및 그 뒤의 구체 타입)가 최소 'a 동안 살아야 함을 의미합니다. 기본적으로 Box<dyn Trait>은 'static을 의미합니다. 참조를 가질 수 있는 트레이트 객체를 저장할 때, 라이프타임을 명시적으로 추가하세요. + 구문은 트레이트 바운드와 라이프타임을 결합합니다. 이것은 플러그인 시스 템과 이벤트 핸들러에서 흔합니다.
trait Parser {
fn parse(&self, input: &str) -> &str;
}
// Trait object with lifetime
fn make_parser() -> Box<dyn Parser> {
Box::new(MyParser)
}
// Trait object holding references
struct Runner<'a> {
parsers: Vec<Box<dyn Parser + 'a>>,
}
impl<'a> Runner<'a> {
fn add(&mut self, p: Box<dyn Parser + 'a>) {
self.parsers.push(p);
}
}
// dyn Trait defaults to 'static when no lifetime given
fn static_parser() -> Box<dyn Parser> {
Box::new(MyParser)
}다운캐스팅 & Any
Any 트레이트는 런타임 타입 검사와 다운캐스팅을 가능하게 합니다. Any는 모든 'static 타입에 자동으로 구현됩니다. downcast_ref와 downcast_mut는 Option을 반환하여 안전한 타입 복구를 허용합니다. 이것은 플러그인 시스템, 동적 설정, 이질적 컨테이너에 유용합니다. Any는 타입 시스템을 우회하므로 드물게 사용하세요. 알려진 대안에는 열거형을, 타입 안전 다형성에는 제네릭을 선호하세요.
use std::any::Any;
// Any enables runtime type identification
let x: Box<dyn Any> = Box::new(42_i32);
// Downcast to concrete type
if let Some(n) = x.downcast_ref::<i32>() {
println!("Got i32: {}", n);
}
// Store heterogeneous values
let mut bag: Vec<Box<dyn Any>> = vec![
Box::new(42_i32),
Box::new("hello".to_string()),
Box::new(3.14_f64),
];
for item in &bag {
if let Some(s) = item.downcast_ref::<String>() {
println!("String: {}", s);
} else if let Some(n) = item.downcast_ref::<i32>() {
println!("i32: {}", n);
}
}선언적 매크로
macro_rules! 기본
macro_rules!는 패턴을 매칭하고 코드로 확장되는 선언적 매크로를 정의합니다. $( $x:expr ),*는 쉼표로 구분된 표현식 목록을 0회 이상 반복 매칭합니다. $() ... * 블록은 각 매치마다 반복됩니다. 매크로는 타입 검사 전에 컴파일 타임에 확장됩니다. 위생적입니다: 매크로가 도입한 식별자는 주변 코드와 충돌하지 않습니다. 보일러플레이트를 줄이기 위해 매크로를 사용하세요(vec!, println!, format!).
macro_rules! vec_of {
( $( $x:expr ),* ) => {
{
let mut v = Vec::new();
$(
v.push($x);
)*
v
}
};
}
let nums = vec_of!(1, 2, 3, 4);
let strs = vec_of!("a", "b", "c");
// Multiple patterns
macro_rules! greet {
() => { println!("Hello!") };
($name:expr) => { println!("Hello, {}!", $name) };
($name:expr, $greeting:expr) => {
println!("{}, {}!", $greeting, $name)
};
}
greet!();
greet!("Alice");
greet!("Bob", "Hi");프래그먼트 타입
매크로 프래그먼트는 특정 타입을 가집니다: expr(표현식), stmt(문), ty(타입), pat(패턴), ident(식별자), tt(토큰 트리, 가장 유연), literal(리터럴) 등. 프래그먼트 타입은 매크로가 받아들이는 것과 파싱하는 방법을 결정합니다. tt가 가장 일반적입니다 — 임의의 유효한 토큰 시퀀스. 더 나은 에러 메시지를 위해 가능한 가장 구체적인 타입을 사용하세요. 파서는 Most-Recently-Added-Ambiguity 규칙을 따릅니다.
macro_rules! items {
// $x:expr - expression (1 + 2, foo())
// $x:stmt - statement (let x = 5;)
// $x:ty - type (Vec<i32>, &str)
// $x:pat - pattern (Some(x), (a, b))
// $x:path - path (std::vec::Vec, Module::Type)
// $x:ident - identifier (foo, Bar)
// $x:literal - literal (42, "hello")
// $x:tt - token tree (anything)
// $x:block - block ({ ... })
// $x:item - item (fn, struct)
($e:expr, $t:ty, $i:ident) => {
let $i: $t = $e;
};
}
items!(42, i32, my_num);
items!("hi", &str, greeting);반복 패턴
매크로의 반복: $(...)*는 0회 이상 매칭, $(...)+는 1회 이상 매칭, $(...)?는 0회나 1회 매칭. 쉼표 같은 구분자는 매치 사이에 옵니다. 중첩 반복은 다차원 데이터(행렬, 리스트의 리스트)를 처리합니다. $() ... * 확장 블록은 각 매치마다 반복됩니다. 같은 반복의 여러 변수는 같은 횟수만큼 매치해야 합니다. 누적기 패턴을 위해 @prefix 내부 규칙을 사용하세요.
macro_rules! sum {
// Zero or more: $(...)*
( $( $x:expr ),* ) => {
{
let mut total = 0;
$(
total += $x;
)*
total
}
};
// One or more: $(...)+
( first $(, $x:expr )+ ) => {
println!("First and more");
};
// With separator and count
( $( $x:expr ),+ $(; $sep:expr )? ) => {
println!("List with optional separator");
};
}
// Nested repetition
macro_rules! matrix {
( $( [ $( $x:expr ),* ] ),* ) => {
vec![ vec![ $( $x ),* ],* ]
};
}위생 & 내보내기
매크로 위생은 식별자 충돌을 방지합니다: 매크로가 도입한 변수는 자체 범위에 있으며 호출자 변수를 캡처하거나 섀도잉하지 않습니다. 매크로가 정의된 크레이트의 항목을 참조하려면 $crate를 사용하여, 재내보내기 후에도 작동하게 합니다. @prefix 관례는 사용자가 직접 호출해서는 안 되는 내부 헬퍼 규칙을 표시합니다. #[macro_export]는 크레이트 루트에 매크로를 게시합니다.
// Export at crate root
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {
// Hygienic: this T does not collide with caller's T
let _ts: std::time::Instant = std::time::Instant::now();
eprintln!("[{}] {}", "LOG", format!($($arg)*));
};
}
// Use $crate to refer to crate items (works after re-export)
#[macro_export]
macro_rules! make_error {
($msg:expr) => {
$crate::Error::new($msg)
};
}
// Internal helper rule (convention: @ prefix)
macro_rules! count {
(@count $x:expr, $($rest:expr),*) => { 1 + count!(@count $($rest),*) };
(@count $x:expr) => { 1 };
() => { 0 };
}일반 매크로 패턴
매크로는 DSL 구축과 보일러플레이트 감소에 탁월합니다. 일반 패턴: 빌더 DSL(html!, sql!), 테스트 어설션(assert_approx!), 설정(config!), 코드 생성(derive 유사 매크로). 매크로는 위생적이고 컴파일 타임이므로 런타임 비용이 없습니다. 제한: 64를 넘는 재귀 깊이 없음, 복잡한 에러 메시지, 사소하지 않은 파싱의 어려움. 복잡한 메타프로그래밍에는 절차적 매크로를 사용하세요.
// 1. Builder DSL
macro_rules! html {
($tag:ident { $($body:tt)* }) => {
format!("<{}>{}</{}>", stringify!($tag), html_inner!($($body)*), stringify!($tag))
};
}
// 2. Test assertion
macro_rules! assert_approx {
($a:expr, $b:expr, $eps:expr) => {
assert!(($a - $b).abs() < $eps, "{} != {} within {}", $a, $b, $eps);
};
}
// 3. Configuration
macro_rules! config {
( $( $key:ident : $val:expr ),* ) => {
{
let mut m = std::collections::HashMap::new();
$(
m.insert(stringify!($key).to_string(), $val.to_string());
)*
m
}
};
}
let cfg = config! { host: "localhost", port: 8080 };Cargo & 워크스페이스
워크스페이스 설정
워크스페이스는 종속성과 target 디렉토리를 공유하는 여러 크레이트를 그룹화합니다. 멤버는 명시적으로 또는 글로브로 나열됩니다. [workspace.package]는 공유 패키지 메타데이터를 정의하며, .workspace = true로 상속됩니다. [workspace.dependencies]는 종속성 버전을 중앙화하여, 모든 크레이트가 같은 버전을 사용하게 합니다. 이는 버전 충돌을 방지하고 빌드를 가속화합니다(단일 Cargo.lock). CLI + 라이브러리 + 서버 같은 다중 크레이트 프로젝트에 워크스페이스를 사용하세요.
# Root Cargo.toml
[workspace]
members = ["crates/*", "cli", "server"]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Team <[email protected]>"]
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
# Member crate: crates/mylib/Cargo.toml
[package]
name = "mylib"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true
tokio.workspace = true빌드 프로필
프로필은 컴파일 설정을 제어합니다. dev는 빠른 컴파일을 우선합니다(opt-level 0, 디버그 심볼). release는 런타임 성능을 최대화합니다(opt-level 3, LTO, 단일 codegen unit). LTO(링크 타임 최적화)는 크레이트 간 인라인을 가능하게 합니다. panic = "abort"는 더 작은 바이너리를 생성하지만 언와인딩을 비활성화합니다. 패키지별 재정의(profile.dev.package."*")는 dev에서도 종속성을 최적화합니다. 커스텀 프로필은 기존 것에서 상속됩니다.
# Cargo.toml
[profile.dev]
opt-level = 0 # No optimization (fast compile)
debug = true # Include debug symbols
overflow-checks = true
[profile.release]
opt-level = 3 # Max optimization
debug = false
lto = "fat" # Link-time optimization
codegen-units = 1 # Single unit (better opt, slower compile)
panic = "abort" # Smaller binary, no unwinding
strip = true # Strip symbols
[profile.dev.package."*"]
opt-level = 2 # Optimize dependencies in dev
# Custom profile
[profile.bench]
inherits = "release"
debug = true
# Usage: cargo build --release, cargo build --profile=bench기능 & 조건부 컴파일
기능은 조건부 컴파일을 가능하게 합니다. 선택적 종속성은 자동으로 기능이 됩니다. cfg(feature = "...")는 기능별로 코드를 게이트합니다. --no-default-features가 전달되지 않는 한 기본 기능 집합이 활성화됩니다. 기능은 가산적이어야 합니다(덜가 아닌 더 켜기). 바이너리 크기를 줄이거나, 여러 백엔드를 지원하거나, 실험적 코드를 게이트하기 위해 기능을 사용하세요. 조건부 derive 매크로를 위해 cfg_attr와 결합하세요. 상호 배타적인 기능을 피하세요.
# Cargo.toml
[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]
async-runtime = ["tokio"]
[dependencies]
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
tokio = { version = "1", optional = true, features = ["full"] }
# Code: conditional compilation
#[cfg(feature = "json")]
pub fn parse_json(s: &str) -> Result<Value, Error> {
serde_json::from_str(s)
}
#[cfg(not(feature = "json"))]
pub fn parse_json(_: &str) -> Result<Value, Error> {
Err(Error::FeatureNotEnabled)
}빌드 스크립트 (build.rs)
build.rs는 컴파일 전에 실행되어, 코드 생성, 환경 임베딩, C 라이브러리 링크를 가능하게 합니다. cargo:rerun-if-* 지시어는 스크립트 재실행 시점을 제어합니다. println! 매크로로 코드를 생성한 후, 크레이트에 include!하세요. 일반 용도: 버전 정보 임베딩, 바인딩 생성(bindgen), protobuf/SQL 스키마 컴파일, 시스템 라이브러리 링크. 빌드 스크립트를 빠르게 유지하세요 — 매 빌드마다 실행됩니다.
// build.rs: runs before compilation
use std::env;
use std::fs;
use std::path::Path;
fn main() {
// Tell Cargo to rerun if env changes
println!("cargo:rerun-if-env-changed=DATABASE_URL");
let out_dir = env::var("OUT_DIR").unwrap();
let dest = Path::new(&out_dir).join("config.rs");
let db_url = env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite://default.db".to_string());
// Generate Rust code at build time
fs::write(&dest, format!(
"pub const DATABASE_URL: &str = \"{}\";",
db_url
)).unwrap();
// Link a C library
println!("cargo:rustc-link-lib=static=mylib");
println!("cargo:rustc-link-search=native=/usr/local/lib");
}
// In code: include generated file
include!(concat!(env!("OUT_DIR"), "/config.rs"));게시 & 문서화
cargo publish는 크레이트를 crates.io에 업로드합니다. 게시 전에 --dry-run으로 검증하세요. cargo doc는 doc 주석(///)에서 HTML 문서를 생성합니다. doc 주석의 코드 블록은 cargo test --doc로 테스트됩니다. Examples, Panics, Errors 섹션을 포함하세요. 메타데이터(description, repository, keywords)는 발견 가능성을 향상합니다. 일단 게시되면, 버전은 재사용이나 삭제가 불가능합니다 — yank로 새 프로젝트가 종속하지 않게 하세요.
# Publish to crates.io
cargo login <token> # One-time authentication
cargo publish # Publish current crate
# Before publishing:
cargo publish --dry-run # Verify package contents
cargo package # Inspect the .crate file
# Documentation
cargo doc # Generate docs
cargo doc --open # Generate and open
cargo doc --no-deps # Only this crate
# In code: doc comments
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
# README and metadata in Cargo.toml
[package]
description = "A short description"
repository = "https://github.com/user/repo"
readme = "README.md"
keywords = ["parser", "cli"]
categories = ["command-line-utilities"]절차적 매크로
매크로 타입
절차적 매크로는 컴파일 타임에 Rust 코드를 생성하며, 토큰 스트림에 작동합니다. 세 가지 타입: 함수 유사(custom!()), derive(#[derive(Custom)]), 속성(#[custom]). proc-macro = true인 별도 크레이트가 필요합니다. syn 크레이트가 Rust 구문을 파싱하고, quote가 코드를 생성하며, proc-macro2가 테스트를 가능하게 합니다. proc-매크로는 강력하지만 복잡합니다 — derive 매크로, DSL, 선언적 매크로가 처리할 수 없는 코드 생성에 사용하세요.
// Three types of procedural macros:
// 1. Function-like: my_macro!(...)
// 2. Derive: #[derive(MyMacro)]
// 3. Attribute: #[my_macro]
// Cargo.toml for a proc-macro crate
// [lib]
// proc-macro = true
// [dependencies]
// syn = { version = "2", features = ["full"] }
// quote = "1"
// proc-macro2 = "1"
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> i32 { 42 }".parse().unwrap()
}
// Usage: make_answer!();
// Generates: fn answer() -> i32 { 42 }Derive 매크로
Derive 매크로는 #[derive(MyMacro)]로 어노테이션된 타입에 트레이트 구현을 추가합니다. syn은 입력을 DeriveInput AST로 파싱합니다. quote!가 변수에 대한 # 보간으로 코드를 생성합니다. 헬퍼 속성(attributes(hello))은 필드나 변형에 대한 커스터마이징을 허용합니다. 일반 derive 매크로: Debug, Clone, Serialize, Deserialize. 생성된 코드는 모듈에 추가되므로, 원래 타입을 수정할 수 없습니다.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let expanded = quote! {
impl HelloMacro for #name {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
};
expanded.into()
}
// Usage:
// #[derive(HelloMacro)]
// struct Pancakes;
// Pancakes::hello(); // "Hello from Pancakes!"
// Helper attributes
#[proc_macro_derive(HelloMacro, attributes(hello))]
pub fn hello_with_attr(input: TokenStream) -> TokenStream { /* ... */ }속성 매크로
속성 매크로(#[my_attr])는 어노테이션된 항목을 변환하며, 잠재적으로 완전히 대체합니다. 속성 인수와 어노테이션된 항목 모두를 받습니다. 일반 용도: 로깅, 캐싱, 비동기 래퍼(#[tokio::main]), 라우팅(#[get("/path")]). 속성 매크로는 항목의 시그니처를 변경하거나, 코드를 추가하거나, 추가 항목을 생성할 수 있습니다. derive 매크로보다 유연하지만 올바르게 사용하기는 더 어렵습니다.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn log_calls(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_args = syn::parse_macro_input!(attr as syn::AttributeArgs);
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;
let expanded = quote! {
fn #fn_name() {
println!("Calling {}", stringify!(#fn_name));
let __result = (|| #fn_block)();
println!("Finished {}", stringify!(#fn_name));
__result
}
};
expanded.into()
}
// Usage:
// #[log_calls]
// fn my_function() -> i32 { 42 }함수 유사 매크로
함수 유사 절차적 매크로(my_macro!())는 임의의 토큰 스트림을 받아 커스텀 DSL을 가능하게 합니다. Parse를 구현하여 허용되는 구문을 정의하세요. 매크로는 입력을 기반으로 검증, 변환, 또는 코드 생성을 할 수 있습니다. 일반 용도: SQL 쿼리(sqlx), HTML 템플릿(maud), 설정 DSL. 선언적 매크로와 달리, proc-매크로는 복잡한 구문을 파싱하고 컴파일 타임에 임의 계산을 수행할 수 있습니다. 느린 빌드를 방지하기 위해 빠르게 유지하세요.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, parse::ParseStream, parse_macro_input};
// Custom syntax: sql!(SELECT * FROM users WHERE id = $1)
struct SqlQuery {
query: String,
}
impl Parse for SqlQuery {
fn parse(input: ParseStream) -> syn::Result<Self> {
let query = input.to_string();
Ok(SqlQuery { query })
}
}
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let SqlQuery { query } = parse_macro_input!(input as SqlQuery);
let expanded = quote! {
{
static QUERY: &str = #query;
// Compile-time SQL validation could go here
QUERY
}
};
expanded.into()
}테스팅 & 디버깅
proc-매크로 테스팅: trybuild는 컴파일러 출력(성공 또는 에러 메시지)을 예상 파일과 비교하는 UI 테스트를 실행합니다. derive 매크로의 경우, 생성된 코드가 컴파일되고 올바르게 동작하는지 테스트하세요. eprintln!(컴파일 중 출력) 또는 cargo expand(확장된 매크로 출력 표시)로 디버그하세요. 매크로 개발은 반복적입니다: 매크로 작성, cargo expand로 출력 검사, 문제 수정. 매크로의 구문과 지원 기능을 명확히 문서화하세요.
// Cargo.toml
// [dev-dependencies]
// trybuild = "1"
// tests/ui/my_macro.rs - test file
// #[derive(MyMacro)]
// struct Foo;
// fn main() { Foo::hello(); }
// tests/ui/my_macro.stderr - expected error
// error: ...
// Test runner
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/pass_*.rs");
t.compile_fail("tests/ui/fail_*.rs");
}
// Debugging with eprintln
#[proc_macro_derive(Debug)]
pub fn debug_derive(input: TokenStream) -> TokenStream {
eprintln!("Input tokens: {}", input);
let ast = syn::parse2(input.clone().into()).unwrap();
eprintln!("Parsed AST: {:#?}", ast);
TokenStream::new()
}매크로
macro_rules!
macro_rules!는 선언적 매크로를 정의합니다. $x는 캡처, expr는 표현식 매칭. $(...)*는 반복. 매크로는 컴파일 타임에 확장됩니다. 보일러플레이트 감소에 유용. 표준 vec! 매크로가 유사하게 작동합니다.
macro_rules! vec_of {
($($x:expr),*) => {{
let mut v = Vec::new();
$(v.push($x);)*
v
}};
}
let nums = vec_of!(1, 2, 3);절차적 매크로
절차적 매크로는 컴파일 타임에 코드를 생성합니다. 세 가지 타입: derive(#[derive(Debug)]), 속성(#[my_attr]), 함수 유사(my_macro!). macro_rules!보다 강력하지만 별도 크레이트가 필요. serde, tokio, diesel에서 사용.
// In a separate crate with proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Generate impl HelloMacro for the type
// Returns new TokenStream
}일반 매크로
내장 매크로: 출력용 println!/format!, 벡터용 vec!, 테스트용 assert!/assert_eq!, 디버깅용 dbg!, 제어 흐름용 todo!/unreachable!. 모두 macro_rules! 기반. dbg!는 체이닝을 위해 값을 반환합니다.
println!("Hello, {}!", "world");
format!("x = {}", 42);
vec![1, 2, 3];
assert!(1 + 1 == 2);
assert_eq!(2 + 2, 4);
dbg!(some_variable); // Debug print
todo!("Not implemented");
unreachable!();매크로 위생
Rust 매크로는 위생적입니다: 매크로가 도입한 식별자는 호출 범위의 식별자와 충돌하지 않습니다. 이는 미묘한 버그를 방지합니다. 매크로 내부의 temp는 외부 temp와 다릅니다. 선언적 매크로는 항상 위생적입니다.
macro_rules! swap {
($a:expr, $b:expr) => {
let temp = $a;
$a = $b;
$b = temp;
};
}
// temp is hygienic: does not conflict with outer temp
let mut temp = 1;
let mut x = 2;
swap!(temp, x); // Works correctly반복
매크로의 반복: $(...)*는 0회 이상, $(...)+는 1회 이상 매칭. 구분자(쉼표)를 지정할 수 있습니다. $x는 각 값을 캡처. 가변 인수 유사 함수에 유용. 표준 println!이 여러 인수를 위해 이것을 사용합니다.
macro_rules! sum {
($($x:expr),*) => {
0 $(+ $x)*
};
}
let total = sum!(1, 2, 3, 4); // 10
// $(...)* zero or more, $(...)+ one or more
// $(...),? optional trailing comma비동기 심층
async/await
async fn은 Future를 반환합니다. .await는 future가 준비될 때까지 일시 정지. Future는 지연입니다: await되기 전까지 아무것도 실행되지 않습니다. 컴파일러가 async fn을 상태 머신으로 변환. 실행하려면 tokio 또는 async-std 런타임을 사용하세요.
async fn fetch_data() -> String {
// Simulate async work
String::from("data")
}
async fn process() {
let data = fetch_data().await;
println!("{}", data);
}Tokio 런타임
tokio::main은 async main을 가능하게. spawn은 태스크 생성(그린 스레드처럼). join!는 여러 future를 동시에 대기. Tokio는 I/O, 타이머, 스케줄링을 제공. Rust에서 가장 인기 있는 비동기 런타임.
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async { work1().await });
let task2 = tokio::spawn(async { work2().await });
let (r1, r2) = tokio::join!(task1, task2);
}채널 (비동기)
mpsc(다중 생산자, 단일 소비자) 채널은 비동기 통신을 가능하게. send/recv는 비동기. 채널은 버퍼를 가집니다(32개 메시지). 모든 송신자가 drop되면 recv는 None 반환. 생산자-소비자 패턴에 유용.
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("hello").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("{}", msg);
}
}Select
select!는 여러 future 중 첫 번째가 완료되기를 대기. 다른 future는 drop됩니다. 타임아웃과 경쟁 작업에 유용. 네트워크 서버에서 흔한 패턴. 각 브랜치는 가드 패턴을 가질 수 있습니다.
tokio::select! {
result = task1 => {
println!("Task1 done: {:?}", result);
}
result = task2 => {
println!("Task2 done: {:?}", result);
}
_ = tokio::time::sleep(Duration::from_secs(5)) => {
println!("Timeout");
}
}Stream
Stream은 Iterator의 비동기 동등물. next().await로 다음 항목을 가져옵니다. StreamExt는 map, filter, for_each 제공. 네트워크나 파일에서 데이터 청크 처리에 유용. async-stream 크레이트가 stream 생성을 단순화.
use tokio_stream::{self as stream, StreamExt};
let mut stream = stream::iter(vec![1, 2, 3]);
while let Some(item) = stream.next().await {
println!("{}", item);
}
// Map, filter like iterators but async
stream.map(|x| x * 2).filter(|x| *x > 2).for_each(|x| async move {
println!("{}", x);
}).await;Cargo & 크레이트
Cargo.toml
Cargo.toml은 매니페스트 파일. [package]는 메타데이터 정의. [dependencies]는 외부 크레이트 나열. 기능은 선택적 기능 활성화. [dev-dependencies]는 테스트 전용. Edition 2021이 최신 안정. 버전은 semver 사용.
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1"Cargo 명령
cargo new는 바이너리 프로젝트 생성(라이브러리는 --lib). build는 target/로 컴파일. --release는 최적화 활성화. check는 build보다 빠름(코드 생성 없음). clippy는 일반적인 실수를 잡음. fmt는 코드 형식화. doc는 HTML 문서 생성. add는 종속성 삽입.
cargo new myapp # Create new project
cargo build # Compile
cargo build --release # Optimized build
cargo run # Build and run
cargo test # Run tests
cargo check # Fast type-check
cargo fmt # Format code
cargo clippy # Lint
cargo doc --open # Generate docs
cargo add serde # Add dependency워크스페이스
워크스페이스는 target 디렉토리와 Cargo.lock을 공유하는 여러 크레이트를 그룹화. 멤버는 개별 크레이트. [workspace.dependencies]는 종속성 버전을 중앙화. 각 크레이트는 workspace = true로 참조. 공유 컴파일로 빠른 빌드. rust-analyzer 같은 대형 프로젝트에서 사용.
# Root Cargo.toml
[workspace]
members = ["crate-a", "crate-b"]
# Shared dependencies
[workspace.dependencies]
serde = "1.0"
# In crate-a/Cargo.toml
[dependencies]
serde = { workspace = true }기능
기능은 조건부 컴파일을 가능하게. --no-default-features가 없으면 기본 기능 활성화. cfg(feature = ...)로 코드 게이트. 종속성의 dep: 구문은 기능 통합을 회피. 선택적 기능과 플랫폼별 코드에 유용. 크레이트는 소비자에게 기능을 노출할 수 있습니다.
# Cargo.toml
[features]
default = ["csv"]
csv = ["dep:csv-parse"]
json = ["dep:serde_json"]
# Conditional compilation
#[cfg(feature = "csv")]
pub fn parse_csv() { /* ... */ }게시
crates.io는 Rust 패키지 레지스트리. cargo login으로 인증. --dry-run으로 문제 잡기. 일단 게시되면 버전 재게시 불가(yank는 검색에서 숨길 뿐). semver 따르기: 수정은 수정용, 마이너는 기능용, 메이저는 호환성 깨는 변경. README와 라이선스 필요.
# Login (one time)
cargo login <token>
# Check before publishing
cargo publish --dry-run
# Publish to crates.io
cargo publish
# Version bumping
cargo bump patch # 0.1.0 -> 0.1.1
cargo bump minor # 0.1.0 -> 0.2.0
cargo bump major # 0.1.0 -> 1.0.0Rust 테스팅
단위 테스트
테스트는 #[cfg(test)] 모듈에 있습니다. use super::*는 부모를 임포트. #[test]가 테스트 함수 표시. assert_eq!가 동등성 검사. #[should_panic]은 패닉 예상. 테스트는 cargo test로 실행. 단위 테스트는 코드와 함께. cfg(test) 속성은 release 빌드에서 테스트가 컴파일되지 않음을 보장.
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic]
fn test_panic() {
panic!("expected");
}
}통합 테스트
통합 테스트는 tests/ 디렉토리에 있습니다. 각 파일은 별도 크레이트로 컴파일. 공개 API만 테스트 가능. 엔드투엔드 테스팅에 유용. --test <name>으로 특정 테스트 실행. 통합 테스트는 컴파일이 느리지만 실제 인터페이스를 테스트.
// tests/integration_test.rs
use myapp::add;
#[test]
fn test_add_integration() {
assert_eq!(add(2, 3), 5);
}
// Run: cargo test --test integration_test
// Each file in tests/ is a separate crate테스트 조직
#[ignore]는 --ignored가 전달되지 않으면 테스트를 건너뜀. 이름 패턴으로 테스트 필터. --nocapture는 println! 출력 표시. 테스트는 기본적으로 병렬 실행. 순차를 위해 --test-threads=1 사용. 커스텀 하네스가 기본 테스트 러너를 대체 가능. 벤치마크와 속성 테스트에 유용.
#[test]
fn it_works() { /* ... */ }
// Custom test harness
#[test]
#[ignore = "slow test"]
fn slow_test() { /* ... */ }
// Run only ignored tests
cargo test -- --ignored
// Filter by name
cargo test test_add
// Show output
cargo test -- --nocapture어설션
assert!는 불리언 검사. assert_eq!/assert_ne!는 실패 시 디버그 출력으로 값 비교. 커스텀 메시지가 디버깅에 도움. 부동소수점을 위해 approx 크레이트 사용. 부분 동등성을 위해 PartialEq 구현. 어설션 실패 시 디버그 출력이 두 값을 모두 표시.
assert!(true); // Boolean
assert_eq!(2 + 2, 4); // Equality
assert_ne!(3, 4); // Inequality
assert!(x > 0, "x must be positive"); // Custom message
assert_eq!(a, b, "got {}, expected {}", a, b);
// Debug output on failure
assert_eq!(vec![1, 2], vec![1, 2]);속성 테스팅
proptest는 무작위 입력을 생성하여 실패 케이스를 찾음. 전략(a in range)이 입력 생성기 정의. prop_assert!가 최소 반례로 실패 보고. 축소가 가장 작은 실패 입력을 찾음. 에지 케이스에 대해 손으로 쓴 테스트보다 나음. Haskell의 QuickCheck와 유사.
// Cargo.toml: proptest = "1"
use proptest::prelude::*;
proptest! {
#[test]
fn test_add_commutative(a in -1000..1000, b in -1000..1000) {
prop_assert_eq!(add(a, b), add(b, a));
}
#[test]
fn test_string_len(s in ".{0,100}") {
prop_assert!(s.len() <= 100);
}
}관련 Rust 스니펫
Copy-paste ready code for common tasks.
메서드가 있는 구조체
Rust 구조체 정의 및 self와 Self로 메서드 구현.
소유권
Rust 소유권 시스템.
대여 및 참조
참조 및 가변 대여.
라이프타임
명시적 라이프타임 주석.
Trait
trait 정의 및 구현.
제네릭
제네릭 함수 및 구조체.
Enum
Enum 및 Option.
패턴 매칭
match 및 구조 분해.
오류 처리
Result 및 ? 연산자.
이터레이터
이터레이터 어댑터 및 소비자.
클로저
클로저 및 Fn trait.
모듈 시스템
모듈, 경로 및 가시성.
동시성 프로그래밍
스레드 및 채널.
스마트 포인터
Box, Rc, RefCell.
매크로
선언적 및 절차적 매크로.
Unsafe Rust
안전하지 않은 연산.
Was this helpful?