← 목록 · 스마트 포인터 · 동시성 · 이터레이터 · 고급 트레이트
Rust의 타입 시스템과 소유권 모델을 활용해 다른 언어에선 불가능하거나 불편한 패턴들을 구현하는 방법. 기초 문법은 알고 있다고 가정한다.
type Meters = f64는 별칭일 뿐이다 — 컴파일러 눈엔 그냥 f64. Newtype은 새 타입을 진짜로 만든다.
type Meters = f64;
type Kilograms = f64;
fn launch(dist: Meters, mass: Kilograms) {}
let d: Meters = 100.0;
let m: Kilograms = 75.0;
launch(m, d); // 컴파일 OK — 버그!
struct Meters(f64);
struct Kilograms(f64);
fn launch(dist: Meters, mass: Kilograms) {}
let d = Meters(100.0);
let m = Kilograms(75.0);
launch(m, d); // 컴파일 에러!
Orphan Rule 우회 — 외부 타입에 외부 트레이트를 직접 impl 할 수 없다. Newtype으로 감싸면 된다.
use std::fmt;
// Vec에 Display를 impl 하고 싶다 → orphan rule 위반
// 해결: newtype
struct Wrapper(Vec);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
let w = Wrapper(vec!["hello".to_string(), "world".to_string()]);
println!("{}", w); // [hello, world]
impl Deref for Meters를 추가하면 .0 없이 내부 값처럼 쓸 수 있다. 단, 그러면 타입 안전성이 일부 희생되니 트레이드오프를 고려할 것.
선택적 필드가 많거나, 생성 시점에 validation이 필요할 때. 생성자를 new(required)로 단순하게 유지하면서 선택 옵션은 체이닝으로.
#[derive(Debug)]
struct HttpRequest {
method: String,
url: String,
timeout_ms: u64,
headers: Vec<(String, String)>,
body: Option>,
}
#[derive(Default)]
struct HttpRequestBuilder {
method: Option,
url: Option,
timeout_ms: u64,
headers: Vec<(String, String)>,
body: Option>,
}
impl HttpRequestBuilder {
pub fn new() -> Self { Self { timeout_ms: 5000, ..Default::default() } }
pub fn method(mut self, m: impl Into) -> Self {
self.method = Some(m.into()); self
}
pub fn url(mut self, u: impl Into) -> Self {
self.url = Some(u.into()); self
}
pub fn timeout_ms(mut self, t: u64) -> Self {
self.timeout_ms = t; self
}
pub fn header(mut self, k: impl Into, v: impl Into) -> Self {
self.headers.push((k.into(), v.into())); self
}
pub fn body(mut self, b: Vec) -> Self {
self.body = Some(b); self
}
pub fn build(self) -> Result {
Ok(HttpRequest {
method: self.method.ok_or("method is required")?,
url: self.url.ok_or("url is required")?,
timeout_ms: self.timeout_ms,
headers: self.headers,
body: self.body,
})
}
}
impl HttpRequest {
pub fn builder() -> HttpRequestBuilder { HttpRequestBuilder::new() }
}
// 사용
let req = HttpRequest::builder()
.method("POST")
.url("https://api.example.com/data")
.timeout_ms(3000)
.header("Content-Type", "application/json")
.body(b"{\"key\":\"value\"}".to_vec())
.build()
.unwrap();
#[derive(Builder)] 한 줄로 줄일 수 있다. 실 프로젝트에서 자주 씀.
가장 Rust스러운 패턴. 잘못된 상태에서 메서드를 호출하면 컴파일 에러가 난다. 런타임 패닉 없음.
impl한다.
use std::marker::PhantomData;
// 상태 마커 타입 (데이터 없음, 제로 사이즈)
struct Disconnected;
struct Connected;
struct Authenticated;
struct Connection {
host: String,
_state: PhantomData,
}
// Disconnected 상태에서만 connect() 가능
impl Connection {
pub fn new(host: impl Into) -> Self {
Connection { host: host.into(), _state: PhantomData }
}
pub fn connect(self) -> Result, String> {
println!("{}에 연결 중...", self.host);
// 실제로는 TCP 연결 시도
Ok(Connection { host: self.host, _state: PhantomData })
}
}
// Connected 상태에서만 login() 가능
impl Connection {
pub fn login(self, user: &str, pass: &str) -> Result, String> {
if pass == "secret" {
println!("{} 로그인 성공", user);
Ok(Connection { host: self.host, _state: PhantomData })
} else {
Err("인증 실패".into())
}
}
pub fn disconnect(self) -> Connection {
println!("연결 해제");
Connection { host: self.host, _state: PhantomData }
}
}
// Authenticated 상태에서만 query() 가능
impl Connection {
pub fn query(&self, sql: &str) -> Vec {
println!("실행: {}", sql);
vec!["row1".into(), "row2".into()]
}
}
// 사용 예시
fn main() {
let conn = Connection::::new("db.example.com");
let conn = conn.connect().unwrap();
let conn = conn.login("admin", "secret").unwrap();
let rows = conn.query("SELECT * FROM users");
// conn.connect() → 컴파일 에러! Connected 상태가 아님
// 미연결 상태에서 query() → 컴파일 에러!
}
sm, statig)를 고려할 것.
C++에서 온 개념이지만 Rust에서 더 강제된다. 소유권 + Drop = 자원 누수 불가.
use std::sync::{Mutex, MutexGuard};
// 커스텀 파일 핸들 — Drop으로 자동 닫기
struct FileHandle {
path: String,
fd: i32, // 실제로는 OS 파일 디스크립터
}
impl FileHandle {
fn open(path: &str) -> Result {
println!("열기: {}", path);
Ok(FileHandle { path: path.to_string(), fd: 42 })
}
fn write(&self, data: &[u8]) { /* ... */ }
}
impl Drop for FileHandle {
fn drop(&mut self) {
println!("닫기: {} (fd={})", self.path, self.fd);
// 실제로 close(self.fd) 호출
}
}
// Guard 패턴 — 락을 스코프에 묶기
struct DbTransaction<'a> {
conn: &'a mut Connection,
committed: bool,
}
impl<'a> DbTransaction<'a> {
fn new(conn: &'a mut Connection) -> Self {
conn.execute("BEGIN");
DbTransaction { conn, committed: false }
}
fn execute(&mut self, sql: &str) { self.conn.execute(sql); }
fn commit(mut self) {
self.conn.execute("COMMIT");
self.committed = true;
}
}
impl<'a> Drop for DbTransaction<'a> {
fn drop(&mut self) {
if !self.committed {
self.conn.execute("ROLLBACK"); // 커밋 없이 드롭되면 자동 롤백
}
}
}
// ManuallyDrop — 드롭을 직접 제어해야 할 때 (FFI, 저수준 코드)
use std::mem::ManuallyDrop;
let v = ManuallyDrop::new(vec![1, 2, 3]);
// v는 스코프를 벗어나도 drop()이 호출되지 않는다
// 직접 호출하려면:
// unsafe { ManuallyDrop::drop(&mut v); }
알고리즘을 교체 가능하게. Rust에선 세 가지 방법이 있고 트레이드오프가 다르다.
trait Sorter {
fn sort(&self, data: &mut Vec);
fn name(&self) -> &str;
}
struct QuickSort;
struct MergeSort;
struct InsertionSort;
impl Sorter for QuickSort {
fn sort(&self, data: &mut Vec) { data.sort_unstable(); }
fn name(&self) -> &str { "QuickSort" }
}
impl Sorter for MergeSort {
fn sort(&self, data: &mut Vec) { data.sort(); }
fn name(&self) -> &str { "MergeSort" }
}
impl Sorter for InsertionSort {
fn sort(&self, data: &mut Vec) {
for i in 1..data.len() {
let mut j = i;
while j > 0 && data[j-1] > data[j] { data.swap(j-1, j); j -= 1; }
}
}
fn name(&self) -> &str { "InsertionSort" }
}
// 단형화(monomorphization) → 최적화 최대
// 단, 런타임에 전략 교체 불가
fn sort_with(sorter: &S, data: &mut Vec) {
println!("{}로 정렬", sorter.name());
sorter.sort(data);
}
sort_with(&QuickSort, &mut data);
// vtable 오버헤드 있음
// 런타임에 전략 교체 가능
struct Processor {
sorter: Box,
}
impl Processor {
fn set_sorter(&mut self, s: Box) {
self.sorter = s;
}
fn run(&self, data: &mut Vec) {
self.sorter.sort(data);
}
}
| 방식 | 속도 | 런타임 교체 | 바이너리 크기 |
|---|---|---|---|
impl Trait | 최고 | ❌ | 증가 |
제네릭 <T: Trait> | 최고 | ❌ | 증가 |
Box<dyn Trait> | vtable 호출 | ✅ | 작음 |
외부 타입에 메서드를 추가하고 싶을 때. 특히 표준 라이브러리 타입을 확장하거나 서드파티 타입에 편의 메서드를 붙일 때 쓴다.
// &str / String에 편의 메서드 추가
pub trait StringExt {
fn to_snake_case(&self) -> String;
fn truncate_at(&self, max: usize, suffix: &str) -> String;
fn is_valid_email(&self) -> bool;
}
impl StringExt for str {
fn to_snake_case(&self) -> String {
let mut result = String::new();
for (i, ch) in self.chars().enumerate() {
if ch.is_uppercase() && i > 0 { result.push('_'); }
result.push(ch.to_lowercase().next().unwrap());
}
result
}
fn truncate_at(&self, max: usize, suffix: &str) -> String {
if self.chars().count() <= max {
return self.to_string();
}
let truncated: String = self.chars().take(max).collect();
format!("{}{}", truncated, suffix)
}
fn is_valid_email(&self) -> bool {
self.contains('@') && self.contains('.')
}
}
// 이제 모든 &str, String에서 사용 가능
// (use crate::StringExt; 가 필요)
let s = "CamelCaseString";
println!("{}", s.to_snake_case()); // camel_case_string
let long = "이것은 아주 긴 문장입니다.";
println!("{}", long.truncate_at(7, "...")); // 이것은 아주 긴...
*Ext 접미사를 붙인다 (IteratorExt, FutureExt 등). futures 크레이트의 FutureExt가 대표적인 예.