본 도구에서 사용하는 오픈소스 라이브러리

본 도구의 코드에는 1개의 오픈소스 라이브러리가 포함되어 있습니다.

Rust 치트시트 — 간편 참조

Rust 2021 문법, 소유권, 에러 처리와 가장 자주 쓰는 std API를 모은 치트시트. 일상의 약 80%를 커버.

Rs

Rust Rust 2021 edition

rustc / cargo · 컴파일형 · 시스템 · 함수형 · 동시성 · 정적 · 강한 타입 · 아핀(이동 의미론)

추천 학습 경로

먼저 cargo new/run와 println! 습득 → 변수 바인딩, 소유권, 빌림(Rust 핵심) 마스터 → match, 함수, 클로저로 깊이 들어가기 → Vec/HashMap/반복자로 데이터 정리 → 트레이트와 제네릭 이해 → Result와 ?로 에러 처리 → 스레드와 channel로 동시성 작성 → 그 다음 필요에 따라 네트워크, 시간, 빌드/테스트 학습. FAQ 섹션은 함정 회피를 위한 복습용으로 좋습니다.

1.Hello World 와 빌드 환경

Rust 프로그램 실행, cargo 프로젝트, 툴체인.

최소 프로그램

fn main이 진입점입니다. println! 매크로가 출력합니다. 문장은 세미콜론으로 끝납니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 새 프로젝트 생성:
// cargo new hello
// cd hello
// src/main.rs 편집:
fn main() {
println!("Hello, world!");
}
// 실행:
// cargo run
// 출력:Hello, world!
// 핵심:
// fn 키워드로 함수 정의
// println! 은 매크로(! 가 붙음)
// main 은 () 타입을 반환
// 표현식 끝 세미콜론 생략 가능(함수 절 참조)

실행과 빌드

cargo run 컴파일 후 실행, cargo build 바이너리 생성, cargo check 빠른 타입 검사.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 컴파일 및 실행:
// cargo run
// 빌드(디버그):
// cargo build
// 빠른 검사(바이너리 미생성):
// cargo check
// 릴리스 빌드(최적화):
// cargo build --release
// 산출물 위치:
// target/debug/hello
// target/release/hello
// 단일 파일 직접 컴파일:
// rustc main.rs
// 실행 파일 main 생성
// 대규모 프로젝트는 항상 cargo

cargo 프로젝트

cargo new로 프로젝트 초기화. Cargo.toml에 패키지와 의존성 선언. src/에 소스.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 바이너리 프로젝트 생성:
// cargo new hello --bin
// 라이브러리 프로젝트 생성:
// cargo new mylib --lib
// Cargo.toml 핵심:
// [package]
// name = "hello"
// version = "0.1.0"
// edition = "2021"
// 구성:
// Cargo.toml 매니페스트
// src/main.rs 진입점
// src/lib.rs 라이브러리 진입점
// target/ 빌드 산출물
// 문서 생성:
// cargo doc --open
// 빌드 캐시 target 는 삭제 가능

edition

edition이 언어 에디션을 선언. 2021이 현재. rust-version이 최소 툴체인을 선언.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Cargo.toml:
// [package]
// name = "app"
// version = "0.1.0"
// edition = "2021" // 언어 에디션
// rust-version = "1.70" // 최소 rustc
// 현재 버전 확인:
// rustc --version
// edition 업그레이드:
// cargo fix --edition
// 각 에디션:
// 2015(구)/ 2018 / 2021
// edition 차이:
// 키워드 사용 여부
// 매크로와 프렐루드 차이
// 한 프로젝트에 하나만 사용

출력 매크로

println! / print!는 표준 출력, eprintln!은 에러 스트림, format!은 문자열 생성.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let name = "Rust";
let ver = 1.82;
// 플레이스홀더:
println!("Hello, {}!", name);
println!("版本: {}", ver);
// 번호와 이름 지정:
println!("{0} {1} {0}", "a", "b");
println!("{name} v{ver}", name = name, ver = ver);
// 서식 제어:
println!("{:>10}", "右对齐"); // 너비
println!("{:.2}", 3.14159); // 정밀도
println!("{:b}", 10); // 2진수
println!("{:?}", vec![1, 2]); // Debug
// 줄바꿈 없음:
print!("连续");
// 오류 스트림:
eprintln!("出错了");
}

커맨드라인 인자

std::env::args로 커맨드라인 인자를 가져옵니다. 첫 번째는 프로그램 경로.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::env;
fn main() {
// 인수 수집:
let args: Vec<String> = env::args().collect();
println!("共 {} 个参数", args.len());
for a in &args {
println!("{}", a);
}
// 실인수는 1번째부터:
// args[0] 은 프로그램 경로
// 잘못된 UTF-8 무시하려면 args_os:
// env::args_os()
// 일반적인 사용법:
// let name = args.get(1).unwrap();
// 복잡한 파싱은 clap 라이브러리 사용
}

여러 파일

mod로 모듈 선언. 서브모듈은 파일 또는 디렉터리. use로 가져오기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/main.rs:
mod math; // 모듈 선언
use math::add;
fn main() {
println!("{}", add(1, 2));
}
// src/math.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 또는 디렉터리:
// src/math/mod.rs
// src/math/utils.rs
// 공개:
// pub fn / pub struct
// 기본은 비공개:
// 같은 모듈 내에서만 접근 가능
// 자식 모듈은 super:: 로 부모 참조

직접 컴파일

rustc는 단일 파일 컴파일. cargo 의존성 없음. 연습과 작은 스크립트에 적합.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 컴파일:
// rustc hello.rs
// 실행:
// ./hello
// 출력 이름 지정:
// rustc hello.rs -o app
// 최적화:
// rustc -O hello.rs
// 디버그 정보:
// rustc -g hello.rs
// 모든 경고 활성화:
// rustc -D warnings hello.rs
// 버전 확인:
// rustc --version
// 제한:
// 의존성 관리 없음
// 표준 라이브러리 외 크레이트는
// cargo 가 필요
// 학습이나 단일 파일 실험용

2.변수와 바인딩

let 바인딩, 가변성, 섀도잉, 스코프, 타입.

let 과 mut

let 바인딩은 기본 불변. mut로 가변 선언. Rust 변수는 기본 불변입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// 불변 바인딩:
let x = 5;
// x = 6; // 오류:불변
// 가변 바인딩:
let mut y = 5;
y = 6; // 허용
println!("{}", y);
// 명시적 타입:
let n: u32 = 42;
// 선언 후 대입:
let mut z;
z = 10;
// 미사용 경고:
// _ 접두사로 무시
let _unused = 0;
}

섀도잉

같은 이름의 let이 이전 바인딩을 가립니다(섀도잉). 타입 변경 가능. mut와는 다릅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let x = 5;
let x = x + 1; // 섀도잉
let x = x * 2;
println!("{}", x); // 12
// 타입 변경:
let spaces = " ";
let spaces = spaces.len(); // usize
// mut 과의 차이:
// 섀도잉은 새 바인딩
// mut 은 기존 값 변경
// 섀도잉 후 이전 값 접근 불가
// 스코프 내 섀도잉:
{
let x = 99; // 내부에서 섀도
println!("{}", x); // 99
}
println!("{}", x); // 12
}

const 과 static

const는 컴파일 타임 상수. static은 정적 변수. 명명은 SCREAMING_SNAKE_CASE.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 상수:
const MAX_SIZE: u32 = 100;
const PI: f64 = 3.141592;
// 정적 변수(고정 주소):
static APP_NAME: &str = "demo";
// 가변 정적(unsafe):
static mut COUNTER: u32 = 0;
fn inc() {
unsafe {
COUNTER += 1; // unsafe 필요
}
}
// 차이:
// const 는 컴파일 타임 인라인, 주소 없음
// static 은 고정 메모리 주소
// static mut 접근에는 unsafe 필요
// 현대적 방법:
// 우선 const 사용
// 전역 상태는 뮤텍스 사용
// 상수 표현식으로 계산 가능

타입 추론

컴파일러가 컨텍스트에서 타입을 추론. 필요하면 명시. 정수 기본 i32.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
// 자동 추론:
let x = 42; // i32(기본)
let y = 3.14; // f64(기본)
// 문맥 추론:
let mut v = Vec::new();
v.push(1); // Vec<i32> 추론
// 명시적 표기:
let n: u64 = 42;
let f: f32 = 3.14;
// 메서드 반환값으로 타입 결정:
let s: u32 = "42".parse().unwrap();
// 추론 불가 시 표기 필요
// 정수 타입 기본 i32
// 부동소수 기본 f64

구조분해 바인딩

let은 패턴 구조분해 지원. 튜플, 구조체, 배열을 한 번에 꺼낼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// 튜플 분해:
let (a, b, c) = (1, 2, 3);
println!("{} {} {}", a, b, c);
// 배열 분해:
let [x, y, _] = [10, 20, 30];
// 구조체 분해:
struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 2 };
let Point { x, y } = p;
// 이름 변경:
let Point { x: px, y: py } = p;
// 나머지:
let (first, rest..) = (1, 2, 3, 4);
// 와일드카드 _ 로 무시:
let (_, b2) = (1, 2);
}

스코프

블록 스코프 {}. 변수 수명. 안쪽에서 바깥쪽에 접근 가능.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let outer = 1;
{
// 내부에서 외부 접근 가능:
println!("{}", outer);
let inner = 2;
println!("{}", inner);
}
// println!("{}", inner); // 오류:스코프 밖
// 같은 스코프 내 같은 이름 불가:
// 섀도잉은 중첩 스코프 필요
// 소유권은 스코프 끝에서 해제
// 해제 순서(역순):
// Drop 은 스코프 종료 시 호출
// 조기 종료에는 drop() 사용

네이밍 규칙

Rust 네이밍 규칙. 변수/함수는 snake_case, 타입은 CamelCase.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 변수/함수:snake_case
let user_name = "nick";
fn get_user() {}
// 타입/enum/trait:CamelCase
struct UserAccount {}
enum ColorChoice {}
trait Displayable {}
// 상수/정적:SCREAMING_SNAKE
const MAX_ITEMS: usize = 100;
// 비공개 식별자는 _ 접두사
// 제네릭 인자:대문자 1자 T
// 라이프타임:'a 'b
// 내장 관례:
// is_ / has_ 접두사 불리언 함수
// into_ / to_ / as_ 변환
// 컴파일러 lint 가 네이밍 검사

타입 변환

as는 명시적 숫자 변환을 수행합니다. From/Into trait은 안전한 변환을 제공합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
// as 숫자 변환:
let i: i32 = 42;
let u: u64 = i as u64;
let f: f64 = i as f64;
// 잘림 위험:
let big: u16 = 300;
let small: u8 = big as u8; // 44
// From/Into:
let s = String::from("hello");
let s2: String = "hi".into();
// 정확한 변환:
let x: u8 = 255;
let y: u32 = x.into(); // From<u8> for u32
// 문자열 파싱:
let n: i32 = "42".parse().unwrap();
// 명시적 표기 필요:
// let n: i32 = "42".parse()?;

3.타입 시스템

기본 타입, 튜플, 구조체, 열거형.

기본 타입

정수, 부동소수점, 불리언, 문자. 스칼라 타입과 그 폭.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// 정수:
let i: i32 = -10; // 부호 있음
let u: u32 = 10; // 부호 없음
// 부동소수:
let f: f32 = 1.5;
let d: f64 = 3.14159;
// 불리언:
let b: bool = true;
let c: bool = false;
// 문자(Unicode):
let ch: char = '中';
let emoji: char = '🚀';
// 문자열 슬라이스:
let s: &str = "text";
// 정수 기본 i32, 부동소수 기본 f64
// char 는 4바이트(Unicode 스칼라)

정수 타입

i8~u128, usize / isize. 부호 있음/없음. 플랫폼 의존 크기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// 부호 있음:i8 i16 i32 i64 i128
// 부호 없음:u8 u16 u32 u64 u128
// 플랫폼 의존:usize isize
let a: i8 = -128; // 범위 -128..127
let b: u8 = 255; // 범위 0..255
let c: i64 = 9_223_372_036_854_775_807;
// 리터럴 접미사:
let d = 255u8;
let e = 1_000_000i64; // 밑줄 구분
// 16진수/2진수:
let h = 0xff;
let bin = 0b1010;
// usize 용도:
// 인덱스, 길이, 포인터 크기
// 오버플로 검사(디버그 빌드에서 panic)

부동소수점

f32 / f64는 IEEE 754를 따릅니다. NaN, 무한, 연산.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let a: f64 = 3.14;
let b: f64 = 2.0;
// 연산:
let sum = a + b;
let div = a / b;
let pow = a.powi(2); // 거듭제곱
let sqrt = a.sqrt();
// 특수값:
let inf = f64::INFINITY;
let neg_inf = f64::NEG_INFINITY;
let nan = f64::NAN;
// 판별:
// nan.is_nan()
// inf.is_infinite()
// 주의:
// NaN != NaN
// 부동소수는 정확한 비교에 부적합
// 정밀도:
// f32 는 약 7자리 10진수
// f64 는 약 15-16자리

char 타입

char는 유니코드 스칼라 값이며, 4바이트를 차지합니다. 작은따옴표 리터럴입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let c = 'A';
let zh = '中';
let emoji = '😀';
// 이스케이프:
let tab = '\t';
let quote = '\'';
let newline = '\n';
// Unicode 코드포인트:
let cp = '\u{1F600}'; // 😀
// 연산:
println!("{}", c.is_ascii());
println!("{}", zh.is_alphabetic());
// 숫자와의 관계:
let n = '9' as u8; // 57
// char 와 String:
// 문자를 문자열로 변환:
let s = c.to_string();
// char 은 &str 이 아님

튜플

고정 길이 이종 컬렉션. 인덱스 접근. 구조분해. 빈 튜플은 ().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
// 정의:
let t = (1, "two", 3.0);
// 인덱스로 접근:
println!("{}", t.0); // 1
println!("{}", t.1); // "two"
// 타입 표기:
let t2: (i32, bool) = (5, true);
// 분해:
let (a, b, c) = t;
// 단일 요소 튜플:
let one = (42,); // 콤마 주의
// 빈 튜플/유닛 타입:
let u: () = ();
// () 는 함수의 기본 반환값
// 일반적인 용도:
// 여러 값 반환 (val, err)
// 교환: (a, b) = (b, a)

구조체

struct는 명명된 필드를 정의. 생성, 필드 접근, 업데이트 구문.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct User {
name: String,
age: u8,
active: bool,
}
fn main() {
// 인스턴스화:
let mut u = User {
name: String::from("nick"),
age: 30,
active: true,
};
// 필드 접근:
println!("{}", u.name);
u.age += 1; // mut 필요
// 갱신 문법:
let u2 = User { age: 20, ..u };
// 튜플 구조체:
struct Point(i32, i32);
let p = Point(1, 2);
println!("{}", p.0);
// 유닛 구조체:
struct Unit;
}

열거형

enum은 여러 배리언트를 정의. 데이터를 담을 수 있음. match로 완전 분기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Message {
Quit,
Move { x: i32, y: i32 }, // 구조체 변형
Write(String), // 튜플 변형
ChangeColor(u8, u8, u8),
}
fn main() {
let m = Message::Write(String::from("hi"));
// match 는 완전 매치:
match m {
Message::Quit => println!("退出"),
Message::Move { x, y } => {
println!("移动 {} {}", x, y);
}
Message::Write(s) => println!("{}", s),
Message::ChangeColor(r, g, b) => {
println!("颜色 {}-{}-{}", r, g, b);
}
}
// 열거형은 제네릭/메서드 보유 가능
// Option/Result 도 열거형

유닛 타입

()는 값 없음. 함수의 기본 반환 타입. 인자/반환 없는 관례.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// () 타입:
let unit: () = ();
// 함수 기본 반환값은 ():
fn say() {}
// 동일:
fn say2() -> () {}
// 식이 () 반환:
let r = { let x = 1; }; // 빈 블록
// 용도:
// 타입 인자로:
// Result<T, ()> 는 오류만 신경 씀
// HashMap<K, ()> 를 집합으로 사용
// 제네릭 자리 표시자:
// 구체적 타입을 신경 쓰지 않음
// Debug 출력:
println!("{:?}", ());

4.소유권과 참조

소유권, 빌림, 참조, 슬라이스 — Rust 메모리 안전의 핵심.

소유권

값은 유일한 소유자를 가집니다. 스코프 종료 시 자동 해제. 복사가 아니라 이동.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// 소유권 규칙:
// 1. 각 값에는 소유자 1명
// 2. 소유자가 스코프를 벗어나면 해제
// 3. 소유권 이동을 무브라 부름
let s1 = String::from("hello");
let s2 = s1; // 무브:s1 무효
// println!("{}", s1); // 오류:무브됨
println!("{}", s2);
// 복사 타입(스택에서 Copy):
let a = 5;
let b = a; // 복사, 둘 다 유효
println!("{} {}", a, b);
// 함수 인자도 소유권 이동:
// fn take(s: String) {}
// 되돌리려면 반환값 사용

참조

&T는 불변 참조이며, 소유권을 옮기지 않습니다. 빌린 뒤에도 원래 값을 계속 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let s = String::from("hello");
// 불변 참조:
let len = calculate_length(&s);
println!("{}", len); // 5
println!("{}", s); // 아직 사용 가능
}
fn calculate_length(s: &String) -> usize {
s.len() // 빌림, 소유하지 않음
}
// 참조 규칙:
// &T 는 읽기 전용 빌림
// 참조는 소유권을 옮기지 않음
// 참조는 기본적으로 불변
// 불변 참조는 여러 개 공존 가능
// 참조가 스코프를 벗어나도 값은 해제되지 않음
// 일반적인 사용법:
// 인자는 &str / &T 로 복사 방지

가변 참조

&mut T는 가변 참조. 같은 스코프에서 한 변수에 가변 참조는 하나만 가능.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // hello!
}
fn change(s: &mut String) {
s.push_str("!");
}
// 규칙:
// 가변 참조는 &mut T
// 같은 스코프에서 1개만
// 불변과 가변은 공존 불가:
// let r1 = &s;
// let r2 = &mut s; // 오류
// 데이터 경쟁은 컴파일 타임에 금지:
// 읽기 많고 쓰기 적은 상황
// 빌림이 끝난 후 다시 빌림 가능
// 중괄호로 빌림 범위를 짧게 유지

역참조

* 연산자는 참조가 가리키는 값에 접근. 역참조는 참조와 짝을 이룹니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let x = 5;
let r = &x; // 참조
// 역참조로 값 접근:
println!("{}", *r);
// 역참조 후 대입:
let mut y = 5;
let r2 = &mut y;
*r2 += 1; // y 변경
println!("{}", y); // 6
// 메서드 호출은 자동 역참조:
// s.len() 에 (*s).len() 불필요
// 인덱스 내부도 역참조
// 참조 비교:
// == 은 값인가 참조인가?
// 보통 역참조한 값을 비교
// 댕글링 참조는 컴파일러가 거부

수명

'a로 참조의 유효 범위를 표기. 빌림 검사기가 유효성을 보장. 생략 규칙.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 라이프타임 표기:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long");
let s2 = String::from("s");
let r = longest(&s1, &s2);
println!("{}", r);
}
// 규칙:
// 'a 는 참조의 유효 범위
// 반환값 수명은 입력의 교집합
// 구조체는 참조를 보관 가능:
// struct X<'a> { part: &'a str }
// 생략 규칙:
// 입력 1개면 출력에 생략
// 메서드는 self 수명 생략
// 대부분 명시 표기 불필요

슬라이스

&[T]는 연속 요소의 참조. 문자열 슬라이스는 &str. 소유권 없음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// 배열 슬라이스:
let arr = [1, 2, 3, 4, 5];
let s: &[i32] = &arr[1..4]; // [2,3,4]
println!("{:?}", s);
// 문자열 슬라이스:
let st = String::from("hello");
let part: &str = &st[0..2]; // "he"
// 경계는 문자 경계, 아니면 panic
// 전체 슬라이스:
let all = &arr[..];
// 일반적인 사용법:
// 함수 인자는 Vec 보다 &[T]
// &str 은 String 의 빌림
// 슬라이스 길이는 컴파일 타임에 불명
// 슬라이스는 참조라 복사 안 함

Deref 자동 역참조

Deref 트레이트로 &String을 &str 등으로 자동 변환. 매개변수 다형이 편리.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::ops::Deref;
fn main() {
let s = String::from("hello");
// &String 이 자동으로 &str:
let t: &str = &s;
// 함수 인자도 자동 변환:
fn takes_str(s: &str) {}
let s2 = String::from("world");
takes_str(&s2); // 자동 역참조
// 직접 구현한 Deref:
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// 메서드 호출도 자동:
// my_box.len() 은 단계적으로 deref
// 변환 체인:&MyBox<T> -> &T

Copy 타입

Copy 트레이트 타입은 대입 시 복사. 정수, 부동소수점, bool, char 등.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// Copy 타입:대입 후에도 사용 가능
let a = 5;
let b = a; // 복사
println!("{} {}", a, b); // 둘 다 가능
// Copy 타입 목록:
// 모든 정수/부동소수/bool/char
// 튜플(요소가 모두 Copy)
// 배열
// &T 참조(Copy)
// 비 Copy:
// String、Vec、Box
// 구조체는 기본적으로 비 Copy:
#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 }
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy
println!("{} {}", p1.x, p2.x);
// 판단:
// 스코프 종료 시 Drop 이 없으면 Copy 가능
// 클론은 명시적 .clone()

5.제어 흐름

if, match, 루프. Rust의 제어 흐름은 표현식 기반입니다.

if 표현식

if / else는 표현식이며 값을 반환할 수 있음. 조건에 괄호 불필요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let n = 7;
// 분기:
if n > 0 {
println!("正数");
} else if n == 0 {
println!("零");
} else {
println!("负数");
}
// if 는 식:
let sign = if n > 0 { "+" } else { "-" };
println!("{}", sign);
// 조건에 괄호 불필요
// 반드시 bool 타입
// 분기 반환 타입 일치 필요
// else 없는 if 는 () 반환

match

match는 패턴을 완전 매칭. 각 가지는 표현식. 패턴, 범위, 가드 사용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let x = 3;
// 리터럴 값 매치:
match x {
1 => println!("一"),
2 | 3 => println!("二或三"), // OR
4..=6 => println!("四到六"), // 범위
_ => println!("其他"), // 와일드카드
}
// 바인딩과 가드:
let num = Some(10);
match num {
Some(n) if n > 5 => println!("大数 {}", n),
Some(n) => println!("小数 {}", n),
None => println!("无"),
}
// 완전해야 함(_ 없으면 컴파일 오류)
// match 는 식으로 값을 반환 가능

while 루프

while은 조건 루프. 조건이 참인 동안 반복.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let mut n = 3;
// 카운트 루프:
while n > 0 {
println!("{}", n);
n -= 1;
}
// 조건 루프:
let mut line = String::new();
while line.trim() != "quit" {
line.clear();
// 입력 읽기...
break; // 예제는 바로 탈출
}
// while 루프에 인덱스 없음
// 컬렉션 순회는 for 우선
// 인덱스 필요한 경우:
// 카운터 + while
// while 은 () 반환

for 루프

for는 범위와 반복자를 순회. Rust에서 우선되는 루프 형태.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
// 범위:
for i in 0..5 {
println!("{}", i); // 0..4
}
// 끝점 포함:
for i in 0..=5 {
println!("{}", i); // 0..5
}
// 배열 순회:
let arr = [10, 20, 30];
for v in &arr {
println!("{}", v);
}
// 인덱스 필요:
for (i, v) in arr.iter().enumerate() {
println!("{}: {}", i, v);
}
// 역순:
for i in (0..3).rev() {}
// 컬렉션 순회 시 소유권 이동 없음
// 이동 순회하려면 into_iter

무한 루프

loop는 break까지 무한 반복. 값을 반환할 수 있음. 레이블도 지원.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// 무한 루프:
let mut count = 0;
let result = loop {
count += 1;
if count == 3 {
break count * 10; // 30 반환
}
};
println!("{}", result);
// 라벨로 바깥 루프 탈출:
'outer: loop {
loop {
break 'outer; // 바깥 루프 탈출
}
}
// 용도:
// 재시도, 폴링, 이벤트 루프
// while true 와 동일
// loop 가 관례적이며 값 반환 가능

break 와 continue

break는 루프 종료. continue는 현재 반복 건너뜀. 레이블과 함께 사용 가능.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fn main() {
let mut n = 0;
loop {
n += 1;
// 짝수 건너뛰기:
if n % 2 == 0 {
continue;
}
if n > 7 {
break;
}
println!("{}", n); // 1 3 5 7
}
// continue 는 현재 반복 종료
// break 는 값 반환 가능:
let v = loop { break 42; };
// 중첩 라벨:
'a: for i in 0..3 {
for j in 0..3 {
if i + j == 2 {
continue 'a; // 바깥 루프 계속
}
}
}
}

if let

if let은 단일 패턴 매칭을 간결하게. 한 배리언트만 볼 때 사용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let opt = Some(5);
// 단일 패턴 매치:
if let Some(v) = opt {
println!("值 {}", v);
}
// else 포함:
if let Some(v) = opt {
println!("有值 {}", v);
} else {
println!("无值");
}
// 다른 패턴:
let res: Result<i32, &str> = Ok(10);
if let Ok(v) = res {
println!("成功 {}", v);
}
// 대응하는 match:
// match opt {
// Some(v) => ...,
// _ => (),
// }
// 더 간결, 한 경우에 집중
// 완전하지 않으며 조합 가능

while let

while let은 패턴이 더는 매칭되지 않을 때까지 루프. 컬렉션 소진에 적합.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let mut stack = vec![1, 2, 3];
// 빌 때까지 팝:
while let Some(top) = stack.pop() {
println!("{}", top); // 3 2 1
}
// 문자 열 처리:
let mut chars = "abc".chars();
while let Some(c) = chars.next() {
println!("{}", c);
}
// 대응하는 loop + match:
// loop {
// match v.next() {
// Some(x) => ...
// None => break,
// }
// }
// 이터레이터는 반복 가능 객체
// 조건 복잡하면 loop 사용

6.함수

함수 정의, 반환값, 소유권 이동, 클로저, 제네릭.

함수 정의

fn은 함수를 정의합니다. 매개변수에는 타입 표기가 필요합니다. 함수 본문은 표현식입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 반환값 없음:
fn greet(name: &str) {
println!("你好 {}", name);
}
// 반환값 있음:
fn add(a: i32, b: i32) -> i32 {
a + b // 마지막 식
}
// 명시적 return:
fn early(x: i32) -> i32 {
if x < 0 {
return 0;
}
x * 2
}
fn main() {
greet("Rust");
println!("{}", add(1, 2));
println!("{}", early(-5));
}
// 반환값이 () 면 -> 생략 가능

반환값

마지막 표현식이 반환값. 세미콜론으로 문장과 표현식을 구분.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let x = five();
println!("{}", x); // 5
let y = with_semi();
println!("{:?}", y); // ()
}
fn five() -> i32 {
5 // 식, 값을 반환
}
fn with_semi() {
5; // 문, () 반환
}
// 핵심:
// 식 = 값을 가진 코드
// 문 = 세미콜론으로 끝남
// 세미콜론 없음 = 그 값을 반환
// 블록은 식:
let n = { let a = 1; a + 2 }; // 3
// if/match/loop 는 모두 식

매개변수

매개변수는 기본 불변. 매개변수에서의 패턴 구조분해. 참조로 복사 회피.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 참조 인자:
fn show(name: &str) {
println!("{}", name);
}
// 가변 인자는 mut 필요:
fn append(v: &mut Vec<i32>) {
v.push(1);
}
// 튜플 분해 인자:
fn sum((a, b): (i32, i32)) -> i32 {
a + b
}
fn main() {
show("nick");
let mut v = vec![];
append(&mut v);
println!("{}", sum((1, 2)));
}
// 인자는 값 복사/무브
// 큰 타입은 참조로 전달
// 수정 필요 시 &mut 전달
// 새 값을 반환해 참조 회피

소유권 매개변수

값으로 전달하면 소유권이 이동. 빌리면 재사용 가능. 필요시 소유권을 반환.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn takes(s: String) {
println!("{}", s); // s 는 사용 후 해제
} // s 가 스코프 벗어나며 해제
fn main() {
let s = String::from("hi");
takes(s);
// println!("{}", s); // 오류:무브됨
// 소유권 되돌리기:
let s2 = String::from("keep");
let s2 = give_back(s2);
println!("{}", s2);
}
fn give_back(s: String) -> String {
s // 무브하여 반환
}
// 대안:
// 빌림 &str 이 보통 적절
// 소유권 필요 시에만 값 전달
// 여러 값 반환은 튜플

빌림 매개변수

&T는 읽기 전용 빌림, &mut T는 가변 빌림. 소유권 이동 회피.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 읽기 전용 빌림:
fn len(s: &str) -> usize {
s.len()
}
// 여러 읽기 전용 빌림 공존 가능:
fn print_two(a: &str, b: &str) {
println!("{} {}", a, b);
}
// 가변 빌림:
fn push_item(v: &mut Vec<i32>, x: i32) {
v.push(x);
}
fn main() {
let s = String::from("abc");
println!("{}", len(&s));
println!("{}", s); // 빌림 후에도 사용 가능
let mut v = vec![1];
push_item(&mut v, 2);
println!("{:?}", v);
}
// &str 인자가 &String 보다 범용적

클로저

클로저는 환경 변수를 캡처합니다. |매개변수| 구문. 변수에 저장하거나 함수에 전달 가능.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// 정의:
let add = |a: i32, b: i32| a + b;
println!("{}", add(1, 2));
// 외부 변수 캡처:
let factor = 3;
let mul = |x: i32| x * factor;
println!("{}", mul(5)); // 15
// 클로저는 캡처를 변경 가능(mut 필요):
let mut count = 0;
let mut inc = || {
count += 1;
count
};
println!("{}", inc()); // 1
// 인자로 전달:
let evens: Vec<i32> =
(1..=6).filter(|x| x % 2 == 0).collect();
println!("{:?}", evens);
// 소유권 이동 move 클로저는 병행 절 참조

함수 포인터

fn 타입은 일반 함수를 참조합니다. fn(T) -> U 구문을 사용합니다. 클로저를 fn으로 변환할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn add_one(x: i32) -> i32 {
x + 1
}
// 함수 포인터 타입:
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
let f: fn(i32) -> i32 = add_one;
println!("{}", f(10)); // 11
println!("{}", apply(add_one, 10));
// 캡처 없는 클로저는 fn 으로 변환 가능:
let c = |x: i32| x * 2;
let g: fn(i32) -> i32 = c;
println!("{}", g(5));
// 함수 배열/벡터:
let funcs = [add_one, add_one];
// 제네릭 상황에서는 Fn trait 사용

제네릭 함수

<T> 제네릭 매개변수로 타입 추상화. 경계 추가 가능. 타입 추론.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 제네릭 함수:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in list {
if item > max {
max = item;
}
}
max
}
fn main() {
let nums = vec![34, 50, 25];
println!("{}", largest(&nums));
let chars = vec!['y', 'm', 'a'];
println!("{}", largest(&chars));
}
// T: PartialOrd 는 트레이트 제약
// 여러 인자:fn pair<T, U>
// 호출 시 타입 자동 추론
// 명시도 가능:largest::<i32>(&nums)
// 제네릭은 성능 저하 없음(단형화)

7.문자열

&str 과 String, 연산, 서식, 파싱, 순회.

&str 과 String

&str은 빌린 문자열 슬라이스, String은 소유권을 가진 가변 길이 문자열.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
// &str 문자열 리터럴:
let s: &str = "hello";
// String 은 소유권 보유:
let mut owned = String::from("hello");
owned.push_str(" world");
// &str -> String:
let a: String = s.to_string();
let b: String = String::from(s);
// String -> &str(빌림):
let c: &str = &owned;
let d: &str = owned.as_str();
// 차이:
// &str 은 고정 길이, 데이터 미소유
// String 은 확장 가능, 힙에 할당
// 인자는 &str 우선
// 소유 필요 시 String 변환

String 연산

추가, 삽입, 치환, 제거 등 String을 수정하는 메서드.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let mut s = String::from("hello");
// 추가:
s.push_str(", world"); // 문자열
s.push('!'); // 1자
// 삽입:
s.insert(5, ' ');
// 치환:
let t = s.replace("hello", "hi");
// 삭제:
s.pop(); // 끝 제거
s.truncate(5); // 잘라냄
// 클리어:
s.clear();
// 결합:
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = s1 + &s2; // s1 무브됨
let s4 = format!("{} {}", s3, "c");
// 길이:s.len() 은 바이트 수
}

서식

format!으로 문자열을 조립. 자리표시자와 서식 제어. 안전하고 효율적.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let name = "Nick";
let age = 30;
// 기본 결합:
let s = format!("{} 今年 {} 岁", name, age);
println!("{}", s);
// 번호/이름 지정:
format!("{0} {1} {0}", "a", "b");
format!("{name}-{age}", name = name, age = age);
// 정렬과 너비:
format!("{:>8}", "right"); // 오른쪽 정렬
format!("{:<8}", "left"); // 왼쪽 정렬
format!("{:^8}", "center");
// 숫자 서식:
format!("{:.2}", 3.14159); // 3.14
format!("{:+}", 42); // +42
format!("{:08b}", 10); // 2진수 0 채움
format!("{:#x}", 255); // 0xff
// Debug 디버그 서식:
format!("{:?}", vec![1, 2]);

문자열 슬라이스

&s[a..b]는 바이트 단위 슬라이스. 문자 경계에서만 가능, 아니면 panic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let s = String::from("hello 世界");
// 바이트 단위 슬라이스:
let part = &s[0..5]; // "hello"
println!("{}", part);
// 문자 경계:
// "世界" 는 1자 3바이트
let w = &s[6..12]; // "世界"
// 잘못된 경계는 panic:
// let bad = &s[0..6]; // 문자 경계 아님
// 안전한 순회 방법:
for c in s.chars() {
println!("{}", c);
}
// 1자 가져오기:
let first = s.chars().next();
// 줄 단위:
for line in s.lines() {}
// 인덱스는 바이트 위치, 문자 위치 아님

문자열 순회

chars()는 문자 단위, bytes()는 바이트 단위, char_indices는 위치를 함께.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
let s = "你好Rust";
// 문자 단위 순회:
for c in s.chars() {
println!("{}", c);
}
// 바이트 단위 순회:
for b in s.bytes() {
println!("{:02x}", b);
}
// 문자 + 바이트 오프셋:
for (i, c) in s.char_indices() {
println!("{} at {}", c, i);
}
// 줄 단위:
for line in "a\nb\n".lines() {
println!("<{}", line);
}
// 분할:
for part in "a,b,c".split(",") {
println!("{}", part);
}
// 문자 수 세기:
let n = s.chars().count();
println!("{} 个字符", n);
// 바이트 수 len() 은 문자 수와 다름

문자열 파싱

str::parse는 값을 숫자로 파싱합니다. parse::<T>()는 대상 타입을 표기해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// i32 로 파싱:
let n: i32 = "42".parse().unwrap();
// 명시적 제네릭:
let f: f64 = "3.14".parse().unwrap();
let u: u32 = "255".parse().unwrap();
// 오류 처리:
let result = "abc".parse::<i32>();
match result {
Ok(v) => println!("{}", v),
Err(e) => println!("解析失败: {}", e),
}
// 불리언:
let b: bool = "true".parse().unwrap();
// 숫자에서 문자열로:
let s = 42.to_string();
let s2 = format!("{}", 3.14);
// ? 로 간소화:
// fn f() -> Result<i32, ParseIntError> {
// Ok("42".parse()?)
// }

문자 연산

char 메서드: 대소문자, 숫자 판정, 공백. 문자열 메서드와 조합.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let c = 'a';
// 판별:
println!("{}", c.is_alphabetic());
println!("{}", c.is_numeric());
println!("{}", c.is_whitespace());
println!("{}", c.is_ascii());
// 대문자/소문자:
let up = 'a'.to_uppercase().to_string();
let low = 'A'.to_lowercase().to_string();
// 문자열 메서드:
let s = "Hello, Rust!";
s.to_lowercase();
s.to_uppercase();
s.trim(); // 공백 제거
s.starts_with("Hello");
s.contains("Rust");
s.replace("Rust", "Go");
// 부분 문자열:
let sub = &s[7..]; // "Rust!"
// 빈 검사:s.is_empty()
// 반복:"ab".repeat(3)

문자열 메서드

검색, 트림, 분할, 판정. 자주 쓰는 str 메서드를 체이닝.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let s = " Hello, Rust 2024 ";
// 트림과 대소문자:
let t = s.trim();
let lower = t.to_lowercase();
let upper = t.to_uppercase();
// 검색:
t.starts_with("Hello");
t.ends_with("2024");
t.contains("Rust");
t.find("Rust"); // Option<usize>
// 분할:
let parts: Vec<&str> = t.split(",").collect();
let lines: Vec<&str> = t.lines().collect();
let words: Vec<&str> = t.split_whitespace().collect();
// 치환과 삭제:
t.replace("Rust", "Go");
t.replacen("o", "0", 1);
// 반복과 0 채움:
"ab".repeat(3);
format!("{:0>5}", 42); // 00042
// 빈 판별:
s.is_empty();
}

8.컬렉션

Vec, HashMap, 집합 타입, 반복자.

Vec

Vec<T>는 동적 배열. 길이 가변, 추가와 제거 가능. 스택에 참조, 힙에 데이터.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// 생성:
let mut v = Vec::new();
v.push(1);
v.push(2);
// 매크로로 생성:
let mut v2 = vec![10, 20, 30];
let zero = vec![0; 5]; // [0,0,0,0,0]
// 접근:
let a = &v2[0]; // 인덱스, 범위 밖은 panic
let b = v2.get(1); // Option, 안전
// 변경:
v2[0] = 99;
v2.push(40);
v2.pop(); // 끝 제거
v2.remove(1); // 인덱스 제거
v2.insert(0, 5); // 앞에 삽입
// 길이:v2.len()
// 클리어:v2.clear()
// 순회:for x in &v2 {}
// 정렬:v2.sort()

Vec 연산

순회, 필터, 매핑, 수집. 슬라이스 뷰와 소유권.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let v = vec![1, 2, 3, 4];
// 이터레이터 변환:
let doubled: Vec<i32> =
v.iter().map(|x| x * 2).collect();
// 필터:
let evens: Vec<&i32> =
v.iter().filter(|x| **x % 2 == 0).collect();
// 합계:
let sum: i32 = v.iter().sum();
// 최댓값:
let max = v.iter().max();
// 검색:
let found = v.iter().find(|x| **x == 3);
// 순회하며 변경:
let mut w = vec![1, 2];
for x in w.iter_mut() {
*x += 10;
}
// 이동 순회:
let owned: Vec<i32> = w.into_iter()
.map(|x| x + 1).collect();
// 슬라이스 뷰:
let part = &v[1..3];

HashMap

HashMap<K, V>는 키/값 쌍. 해시 구현, 순서 없음. 조회 O(1).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::collections::HashMap;
fn main() {
// 생성:
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 20);
// 매크로로 생성:
let mut m: HashMap<&str, i32> =
HashMap::from([("a", 1), ("b", 2)]);
// 읽기:
let score = scores.get("Blue"); // Option
// 순회:
for (k, v) in &scores {
println!("{}: {}", k, v);
}
// 삽입 또는 갱신:
*m.entry("a").or_insert(0) += 10;
m.insert("c", 3);
// 삭제:
m.remove("a");
// 확인:
m.contains_key("b");
// 길이:m.len()
// 순서 없음, 순회 순서는 랜덤

HashSet

HashSet<T>는 유일한 원소의 집합. 중복 제거, 교집합, 합집합, 차집합.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::collections::HashSet;
fn main() {
// 생성:
let mut set = HashSet::new();
set.insert(1);
set.insert(2);
set.insert(2); // 중복 무시
println!("{:?}", set); // {1, 2}
// 연산:
set.contains(&1); // true
set.remove(&1);
set.len();
// 집합 연산:
let a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [2, 3, 4].into_iter().collect();
// 합집합:
let u: HashSet<_> = a.union(&b).cloned().collect();
// 교집합:
let i: HashSet<_> = a.intersection(&b).cloned().collect();
// 차집합:
let d: HashSet<_> = a.difference(&b).cloned().collect();
// 부분집합 판별:a.is_subset(&b)

BTree

BTreeMap / BTreeSet는 키가 정렬됨. 순서대로 순회. 성능은 해시보다 약간 느림.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::BTreeMap;
fn main() {
// 정렬된 키-값:
let mut m = BTreeMap::new();
m.insert("b", 2);
m.insert("a", 1);
m.insert("c", 3);
// 정렬 순서대로 순회:
for (k, v) in &m {
println!("{}: {}", k, v);
} // a b c 순
// 범위 쿼리:
for (_k, v) in m.range("a".."c") {
println!("{}", v);
}
// 최대/최소:
let first = m.first_key_value();
let last = m.last_key_value();
// BTreeSet 도 같은 방식:
use std::collections::BTreeSet;
let s: BTreeSet<i32> = [3, 1, 2].into_iter().collect();
println!("{:?}", s); // {1, 2, 3}
// 적용:
// 정렬 순서 순회/범위 쿼리가 필요한 경우
// 키가 비교 가능하면 사용 가능

반복자

Iterator trait은 체이닝 연산을 구동합니다. 지연 평가이며, 소비할 때만 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let v = vec![1, 2, 3, 4, 5];
// 체인:
let result: Vec<i32> = v.iter()
.filter(|x| **x % 2 == 1) // 홀수
.map(|x| x * 10)
.collect();
println!("{:?}", result); // [10, 30, 50]
// 지연 평가:소비하지 않으면 실행 안 됨
// 자주 쓰는 어댑터:
// .take(n) 앞 n개
// .skip(n) n개 건너뜀
// .rev() 뒤집기
// .zip(iter) 쌍으로 묶기
// .enumerate() 인덱스 붙이기
// 소비 메서드:
let sum: i32 = v.iter().sum();
let count = v.iter().count();
let any = v.iter().any(|x| x > 3);
let all = v.iter().all(|x| x > 0);
// 배열을 이터레이터로:
for x in v.iter() { println!("{}", x); }

VecDeque

VecDeque는 양쪽 끝 큐. 양쪽에서 효율적 추가/제거. 링 버퍼.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::VecDeque;
fn main() {
// 생성:
let mut d = VecDeque::new();
// 양끝 연산:
d.push_back(1);
d.push_back(2);
d.push_front(0);
println!("{:?}", d); // [0, 1, 2]
d.pop_front(); // 0
d.pop_back(); // 2
// 접근:
d.front(); // Option
d.back();
d.front_mut();
// 범위:
d.make_contiguous();
// 적합한 용도:
// 큐/FIFO
// 슬라이딩 윈도우
// 양끝에서 스캔
// Vec 과의 차이:
// Vec 앞 삽입은 O(n)
// VecDeque 는 양끝 O(1)
// 랜덤 접근은 약간 느림

중첩 컬렉션

컬렉션의 조합. Vec<HashMap>, 중첩 HashMap, 다차원 Vec.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::HashMap;
fn main() {
// Vec 에 구조체 저장:
let users = vec![
("a", 1),
("b", 2),
];
// HashMap 중첩:
let mut matrix: HashMap<String, Vec<i32>> =
HashMap::new();
matrix.insert("row1".to_string(), vec![1, 2]);
// 없으면 새로 생성:
matrix.entry(String::from("row2"))
.or_insert(vec![]).push(3);
// 다차원 Vec:
let mut grid = vec![vec![0; 3]; 3];
grid[0][1] = 5;
// 구조체 컬렉션:
#[derive(Debug)]
struct Item { id: u32, tags: Vec<String> }
let items = vec![
Item { id: 1, tags: vec!["x".into()] },
];
println!("{:?}", items);
}

9.메모리와 소유권 확장

힙 할당, 스마트 포인터, 공유 소유권.

스택과 힙

스택은 고정 크기로 빠름. 힙은 가변 크기로 할당 필요. 소유권이 수명을 관리.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// 스택의 데이터:
let x = 42; // 고정 크기, 스택
// 힙의 데이터:
let s = String::from("动态大小");
// String 구조는 스택, 데이터는 힙
// 자동 관리:
// 스코프 벗어나면 스택 즉시 팝
// 힙은 소유권 시스템이 해제
// 수동 free 없음, 누수 없음
// 해제 시점:
// 소유자가 스코프 벗어남(Drop)
// Box/스마트 포인터가 힙 객체 관리
// 재귀 구조에는 Box 필요
// 큰 배열은 힙에 둬서 스택 오버플로 방지
}

Box

Box<T>는 값을 힙에 보관. 크기 확정, 소유권을 이동. 재귀 타입에 필수.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
// 힙에 할당:
let b = Box::new(5);
println!("{}", *b); // 역참조
// 재귀 타입에는 Box 필수:
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1,
Box::new(List::Cons(2, Box::new(List::Nil))));
// Box 는 단일 소유권만 제공
// 용도:
// 큰 구조체 복사 감소
// 동적 크기 트레이트 객체
// 재귀 데이터 구조
// 자동 역참조:
// b.len() 은 자동으로 deref
// 스코프 벗어나면 자동 해제

Rc

Rc<T>는 참조 카운트 기반 공유 소유권. 단일 스레드. 읽기 전용 다중 소유.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::rc::Rc;
fn main() {
// 공유 소유권:
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // 참조 카운트 +1
let c = Rc::clone(&a);
println!("{}", a);
println!("{}", b);
// 카운트:
println!("{}", Rc::strong_count(&a)); // 3
// 읽기 전용 공유:
// Rc 내부 데이터는 불변
// 가변 필요 시 RefCell 사용
// 단일 스레드 전용:
// 멀티 스레드에서는 Arc
// 순환 참조로 누수:
// Weak 로 해결
// 스코프 벗어나면 카운트 감소

RefCell

RefCell<T>는 런타임 빌림 검사. 내부 가변성. 빌림 충돌 시 panic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::cell::RefCell;
fn main() {
// 내부 가변성:
let cell = RefCell::new(42);
*cell.borrow_mut() += 1; // 변경
println!("{}", cell.borrow());
// 실행 시 빌림 규칙:
// 가변 빌림 1개, 또는
// 불변 빌림 여러 개
// 충돌:
// let b1 = cell.borrow();
// let b2 = cell.borrow_mut(); // panic!
// 용도:
// 구조체 내 가변 필드
// Rc 와 조합해 공유 가변
// &mut 과의 차이:
// 컴파일 타임 -> 실행 시
// 빌림 체커 우회
// 주의 필요, 빌림 범위 짧게 유지

Arc

Arc<T>는 스레드 안전한 참조 카운트. 다중 스레드 간 공유 소유권. 원자 카운터.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::sync::Arc;
use std::thread;
fn main() {
// 스레드 안전 공유:
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..3 {
let d = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("线程 {}: {:?}", i, d);
}));
}
for h in handles {
h.join().unwrap();
}
// Rc 와의 차이:
// Arc 는 원자적 카운트로 스레드 안전
// Rc 는 단일 스레드
// 읽기 전용 공유:
// 가변은 Mutex 와 조합:
// Arc<Mutex<T>>
// 순환 참조 주의

Mutex

Mutex<T>는 내부 값을 상호 배타적으로 접근하게 합니다. 잠금으로 보호됩니다. Arc와 함께 쓰면 가변 상태를 공유할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 공유 가변 상태:
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1; // 락 후 변경
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap());
// 락 수명:
// lock() 은 Guard 반환
// 스코프 벗어나면 자동 언락
// 포이즈닝:
// 락 보유 중 panic 하면 중독
// unwrap 처리 or_else
// 여러 락은 데드락 순서 주의

원자 타입

AtomicU32 등 원자 연산. 락 프리 동시 카운터. Ordering 메모리 순서.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::sync::atomic::{
AtomicU32, Ordering
};
use std::thread;
fn main() {
// 원자 카운터:
let counter = AtomicU32::new(0);
let mut handles = vec![];
for _ in 0..10 {
let c = &counter;
handles.push(thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.load(Ordering::SeqCst));
// 타입:
// AtomicU8..AtomicI64
// AtomicBool AtomicUsize
// 연산:
// fetch_add / fetch_sub
// compare_exchange
// load / store
// 메모리 순서:
// Relaxed/Release/Acquire
// SeqCst 가 가장 엄격
// 락프리, 블록 없음

스마트 포인터

Deref / Drop 트레이트. Box / Rc / Arc가 구현. 포인터 추상과 자동 정리.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::ops::Deref;
// 직접 만든 스마트 포인터:
struct MyBox<T>(T);
// Deref 로 역참조 가능:
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
// Drop 으로 해제와 정리:
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
println!("释放");
}
}
fn main() {
let m = MyBox(5);
println!("{}", *m); // 역참조 5
// 스코프 벗어나면 자동으로 drop
// 표준 스마트 포인터:
// Box<T> 힙 할당
// Rc<T> 단일 스레드 공유
// Arc<T> 멀티 스레드 공유
// RefCell/Mutex 내부 가변
// String/Vec 도 포인터 + 용량

10.트레이트와 제네릭

트레이트 추상화, 구현, 제네릭 경계, 다형성.

trait 정의

트레이트는 공유 동작을 정의. 메서드 시그니처. 인터페이스와 유사.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// trait 정의:
#[derive(Debug)]
struct Dog;
struct Cat;
// 행동 추상화:
trait Speak {
fn sound(&self) -> &'static str;
}
// 타입에 구현:
impl Speak for Dog {
fn sound(&self) -> &'static str {
"汪汪"
}
}
impl Speak for Cat {
fn sound(&self) -> &'static str {
"喵"
}
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", dog.sound());
println!("{}", cat.sound());
}
// trait 는 기본 구현 가능
// 구현은 메서드 시그니처 충족 필요
// 같은 타입에 여러 trait 구현 가능

trait 구현

impl Trait for Type로 동작 구현. 고아 규칙: 트레이트나 타입 중 하나는 로컬.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 내장 타입에 직접 trait 구현:
trait Greeting {
fn hello(&self) -> String;
}
// &str 에 구현:
impl Greeting for &str {
fn hello(&self) -> String {
format!("你好,{}", self)
}
}
// Vec 에 구현:
impl Greeting for Vec<i32> {
fn hello(&self) -> String {
format!("{:?} 长度 {}", self, self.len())
}
}
fn main() {
println!("{}", "Nick".hello());
println!("{}", vec![1, 2].hello());
}
// 고아 규칙:
// trait 또는 타입 중 하나는 로컬
// 아니면 컴파일 오류
// 빈 구현:
// 마커 타입(sealed)
// 제네릭 구현은 재사용 가능

기본 메서드

trait 메서드는 기본 구현을 제공할 수 있습니다. 오버라이드는 선택 사항이며, 호출 순서는 자동으로 결정됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
trait Animal {
fn name(&self) -> String;
// 기본 구현:
fn intro(&self) -> String {
format!("我是 {}", self.name())
}
}
struct Dog;
impl Animal for Dog {
fn name(&self) -> String {
"小狗".to_string()
}
// 오버라이드 가능:
fn intro(&self) -> String {
format!("{} 汪汪", self.name())
}
}
struct Cat;
impl Animal for Cat {
fn name(&self) -> String {
"小猫".to_string()
}
// 기본 intro 사용
}
fn main() {
println!("{}", Dog.intro()); // 小狗 汪汪
println!("{}", Cat.intro()); // 我是 小猫
}
// 기본 메서드는 trait 의 다른 메서드 호출 가능

트레이트 객체

dyn Trait은 동적 디스패치입니다. 이종 컬렉션을 허용합니다. 런타임 다형성.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
trait Draw {
fn draw(&self);
}
struct Circle;
struct Square;
impl Draw for Circle {
fn draw(&self) {
println!("画圆");
}
}
impl Draw for Square {
fn draw(&self) {
println!("画方");
}
}
// 동적 디스패치:
fn draw_all(shapes: &[&dyn Draw]) {
for s in shapes {
s.draw();
}
}
// 박싱된 trait 객체:
fn make() -> Box<dyn Draw> {
Box::new(Circle)
}
fn main() {
let shapes: Vec<&dyn Draw> =
vec![&Circle, &Square];
draw_all(&shapes);
// 런타임에 어느 구현을 호출할지 결정
// dyn에는 런타임 오버헤드 있음
// 정적 디스패치는 제네릭으로 더 빠름
// 객체 안전성 제약
}

제네릭

제네릭 타입과 함수. T로 추상화. 코드 재사용. 단형화로 무비용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 제네릭 함수:
fn identity<T>(x: T) -> T {
x
}
// 제네릭 구조체:
struct Point<T> {
x: T,
y: T,
}
// 여러 타입 매개변수:
struct Pair<A, B> {
first: A,
second: B,
}
// 제네릭 메서드:
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 1, y: 2 }; // i32로 추론
println!("{}", p.x());
let ip = Point { x: 1.5, y: 2.5 };
println!("{}", ip.x());
// 단형화:
// 컴파일 시점에 구체적 버전 생성
// 런타임 오버헤드 없음
// dyn보다 효율적

제네릭 경계

T: Trait 경계. where 절, + 로 경계 결합.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 기본 제약:
fn print_it<T: std::fmt::Debug>(x: &T) {
println!("{:?}", x);
}
// where 절:
fn complex<T, U>(a: &T, b: &U)
where
T: std::fmt::Display + Clone,
U: std::fmt::Debug,
{
println!("{}", a);
println!("{:?}", b);
}
// 반환의 제네릭:
fn pair() -> impl std::fmt::Display {
42 // 불투명 반환 타입
}
// trait 제약이 있는 제네릭 컬렉션:
fn sortable<T: Ord>(list: &mut [T]) {
list.sort();
}
// 제약으로 제네릭 메서드 사용 가능
// impl Trait 인자 축약 표기
// 여러 제약은 +로 조합

impl 블록

impl 블록에 연관 함수와 연관 메서드. self / Self. 생성자 관례.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 연관 함수(self 없음):
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
// 메서드(&self):
fn area(&self) -> u32 {
self.width * self.height
}
// 가변 메서드(&mut self):
fn set_width(&mut self, w: u32) {
self.width = w;
}
// self 소비:
fn into_string(self) -> String {
format!("{}x{}", self.width, self.height)
}
}
fn main() {
let mut r = Rectangle::new(2, 3);
println!("{}", r.area());
r.set_width(5);
// Self 타입은 impl 내에서 자신을 가리킴
// 여러 impl 블록으로 나눌 수 있음
}

조합과 다형성

Rust에는 상속이 없습니다. trait + 구성으로 재사용. 기본 구현이 베이스 클래스 역할.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 상속 대신 합성:
struct Engine {
power: u32,
}
impl Engine {
fn start(&self) {
println!("引擎启动 {} 马力", self.power);
}
}
struct Car {
engine: Engine, // 합성
model: String,
}
impl Car {
fn start(&self) {
self.engine.start(); // 위임
println!("{} 发车", self.model);
}
}
// trait 기본 메서드를 기반 클래스 대용으로:
trait Fly {
fn fly(&self) {
println!("飞行");
}
}
struct Bird;
impl Fly for Bird {} // 기본값 사용
// 다형성:
fn make_fly(f: &dyn Fly) {
f.fly();
}
fn main() {
let car = Car {
engine: Engine { power: 150 },
model: String::from("Guru"),
};
car.start();
}
// 합성이 더 유연하고 다이아몬드 상속 문제가 없음

11.에러 처리

Result, Option, ? 연산자, panic, 사용자 정의 에러.

Result

Result<T, E>는 성공 값 또는 에러를 반환합니다. Ok/Err 변형으로 구성됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs::File;
fn main() {
// Result 열거형:
let result = File::open("nope.txt");
// match로 처리:
match result {
Ok(file) => println!("打开成功"),
Err(e) => println!("打开失败: {}", e),
}
// 제네릭 시그니처:
// enum Result<T, E> {
// Ok(T),
// Err(E),
// }
// 수동으로 생성:
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> =
Err(String::from("失败"));
// 메서드:
let v = ok.unwrap_or(0);
let v2 = ok.unwrap_or_else(|_| -1);
}

unwrap 과 expect

unwrap은 성공 시 값, 실패 시 panic. expect는 panic 메시지를 함께. 디버그용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let ok: Result<i32, &str> = Ok(10);
// unwrap:
let v = ok.unwrap();
println!("{}", v);
// 실패 시 panic:
// let e: Result<i32, &str> = Err("bad");
// e.unwrap(); // panic!
// expect는 메시지 포함:
// let e = Err("bad");
// e.expect("应该成功"); // panic 메시지
// 적합한 상황:
// 프로토타입/예제/테스트
// 논리적으로 실패할 수 없음
// 프로덕션 권장:
// ?로 전파
// 또는 match로 처리
// unwrap_or로 기본값 제공

? 연산자

?로 에러 전파를 간소화. Err는 자동 반환, Ok는 풀고 계속.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs::File;
use std::io::Read;
// 오류 전파:
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut f = File::open(path)?; // Err는 즉시 반환
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// 체이닝:
fn first_line(path: &str) -> Result<String, std::io::Error> {
let content = read_file(path)?;
Ok(content.lines().next().unwrap_or("").to_string())
}
// ?는 오류를 자동 변환:
// From 구현 필요
// fn f() -> Result<(), Box<dyn Error>> {
// File::open("x")?; // io::Error가 자동 박싱
// Ok(())
// }
// 함수는 Result/Option을 반환해야 함

Error 구현

표준 Error 트레이트. Display + Debug 출력. 에러 체인.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::error::Error;
use std::fmt;
// 사용자 정의 오류 타입:
#[derive(Debug)]
struct MyError {
msg: String,
}
// Display:
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "发生错误: {}", self.msg)
}
}
// Error 구현:
impl Error for MyError {}
fn main() {
let e = MyError { msg: "xxx".to_string() };
println!("{}", e); // Display
println!("{:?}", e); // Debug
println!("{}", e.source()); // 원인 없음
}
// Error trait은 Display + Debug를 요구
// source()는 근본 오류를 반환
// ?와 오류 체인에 사용

사용자 정의 에러

자체 에러 타입 설계. 열거 배리언트가 컨텍스트 보유. 에러 분류.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#[derive(Debug)]
// 분류 오류:
enum AppError {
NotFound(String),
InvalidInput { field: String, reason: String },
Io(std::io::Error),
}
// Display 수동 구현:
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::NotFound(msg) =>
write!(f, "未找到: {}", msg),
AppError::InvalidInput { field, reason } =>
write!(f, "{} 无效: {}", field, reason),
AppError::Io(e) => write!(f, "IO: {}", e),
}
}
}
impl std::error::Error for AppError {}
// 변환:
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> AppError {
AppError::Io(e)
}
}
// 사용:
// fn f() -> Result<(), AppError> {
// std::fs::read_to_string("x")?; // 자동 변환
// Ok(())
// }

thiserror

thiserror derive 매크로로 Display / Error를 자동 구현. 보일러플레이트 대폭 감소.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Cargo.toml에 추가:
// thiserror = "2"
use thiserror::Error;
// 파생 매크로:
#[derive(Error, Debug)]
// 오류 열거형:
#[derive(Error, Debug)]
enum DataError {
#[error("记录不存在: {0}")]
NotFound(String),
#[error("校验失败: {field} {reason}")]
Validation {
field: String,
reason: String,
},
#[error(transparent)]
Io(#[from] std::io::Error),
}
// 자동으로 얻는 것:
// Display 구현
// Error 구현
// From<std::io::Error> 변환
fn main() {
let e = DataError::NotFound("id".into());
println!("{}", e);
}
// 수기 보일러플레이트 대폭 감소
// 생태계 표준 방식

Option

Option<T>는 값이 없을 수 있음을 표현. Some / None. 안전한 접근.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
// Option 타입:
let some = Some(5);
let none: Option<i32> = None;
// match:
match some {
Some(v) => println!("值 {}", v),
None => println!("无值"),
}
// 자주 쓰는 메서드:
let x = some.unwrap_or(0);
let y = some.unwrap_or_else(|| compute());
let z = none.unwrap_or(0); // 0
// 체이닝:
let doubled = some.map(|v| v * 2);
let filtered = some.filter(|v| v > 3);
let flat = Some(Some(1)).flatten();
// 조합:
let a = Some(1);
let b = Some(2);
let sum = a.zip(b).map(|(x, y)| x + y);
// 흔한 사용:
// Vec::get / HashMap::get
// ?로 None 전파

panic

panic!는 복구 불가능 에러. 스택을 풀며 종료. 버그 상황에서만.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// panic 발생:
// panic!("致命错误");
// 인덱스 초과:
let v = vec![1, 2, 3];
// let x = v[10]; // panic!
// 빈 슬라이스의 첫 요소:
let empty: &[i32] = &[];
// let x = empty[0]; // panic!
// panic 동작:
// 오류 메시지 출력
// 스택 풀고 Drop 실행
// 프로그램 종료
// 사용자 지정 메시지:
// panic!("索引 {} 越界", 10);
// 적합한 상황:
// 계속할 수 없는 버그
// 계약 위반
// 복구 가능하면 Result
}

12.입출력

표준 입출력, 파일 읽기/쓰기, 파일 시스템.

입력 읽기

stdin에서 read_line으로 한 줄을 String에. trim으로 개행 제거.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::io;
fn main() {
// 한 줄 읽기:
let mut input = String::new();
io::stdin().read_line(&mut input)
.expect("读取失败");
// 줄바꿈 제거:
let trimmed = input.trim();
println!("你输入了: {}", trimmed);
// 숫자 읽기:
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
let n: i32 = line.trim().parse().unwrap();
println!("数字: {}", n);
// 반복으로 읽기:
loop {
let mut s = String::new();
let bytes = io::stdin().read_line(&mut s).unwrap();
if bytes == 0 { break; } // EOF
print!("{}", s);
}
}

파일 읽기

read_to_string으로 한 번에 읽기. Read 트레이트로 스트림 읽기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs;
fn main() {
// 한 번에 문자열로 읽기:
let content = fs::read_to_string("data.txt")
.expect("读取失败");
println!("{}", content);
// 바이트로 읽기:
let bytes = fs::read("data.bin").unwrap();
// 스트림 읽기(큰 파일):
use std::io::{Read, BufReader};
let f = fs::File::open("big.log").unwrap();
let mut reader = BufReader::new(f);
let mut buf = String::new();
reader.read_to_string(&mut buf).unwrap();
// 줄 단위 읽기:
use std::io::BufRead;
let f = fs::File::open("data.txt").unwrap();
for line in std::io::BufReader::new(f).lines() {
println!("{}", line.unwrap());
}
}

파일 쓰기

write / append로 쓰기. OpenOptions로 모드 제어. 버퍼 쓰기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs;
fn main() {
// 덮어쓰기:
fs::write("out.txt", "hello\n")
.expect("写入失败");
// 추가 쓰기:
use std::fs::OpenOptions;
use std::io::Write;
let mut f = OpenOptions::new()
.append(true)
.create(true)
.open("log.txt")
.unwrap();
f.write_all(b"more data\n").unwrap();
// 버퍼 쓰기:
let mut writer = std::io::BufWriter::new(
fs::File::create("buf.txt").unwrap());
writeln!(writer, "第 {} 行", 1).unwrap();
writer.flush().unwrap(); // 디스크에 플러시
// 포맷 쓰기:
// write!(f, "{}", value)
}

파일 시스템

디렉터리 생성, 삭제, 이름 변경, 존재 판정. 경로 조작.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fs;
fn main() {
// 디렉터리 생성:
fs::create_dir("data").unwrap();
fs::create_dir_all("data/a/b").unwrap();
// 삭제:
fs::remove_file("tmp.txt").unwrap();
fs::remove_dir("data/a").unwrap();
fs::remove_dir_all("data").unwrap();
// 이름 변경:
fs::rename("a.txt", "b.txt").unwrap();
// 존재 확인:
if fs::metadata("b.txt").is_ok() {
println!("存在");
}
// 메타데이터:
let meta = fs::metadata("b.txt").unwrap();
println!("大小: {}", meta.len());
println!("目录: {}", meta.is_dir());
// 복사:
fs::copy("b.txt", "c.txt").unwrap();
}

BufRead 한 줄씩

BufRead 트레이트로 한 줄씩. lines(), read_line, split.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::io::BufRead;
use std::io::BufReader;
use std::fs::File;
fn main() {
let f = File::open("data.txt").unwrap();
let reader = BufReader::new(f);
// 줄 단위:
for line in reader.lines() {
let l = line.unwrap();
println!("{}", l);
}
// 줄 번호 포함:
let f = File::open("data.txt").unwrap();
for (i, line) in
BufReader::new(f).lines().enumerate() {
println!("{}: {}", i + 1, line.unwrap());
}
// 한 줄 수동 읽기:
// let mut buf = String::new();
// reader.read_line(&mut buf)
// 바이트 분할:
// for part in reader.split(b",") {}
// stdin에도 사용 가능
}

출력과 쓰기

print! / println! / eprintln!. write! / writeln!은 임의의 writer 대상.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::io::{self, Write};
fn main() {
// 표준 출력:
println!("普通输出");
print!("无换行");
// 표준 오류:
eprintln!("错误信息");
// Vec에 쓰기:
let mut buf = Vec::new();
writeln!(buf, "写入 Vec {}", 1).unwrap();
println!("{:?}", buf);
// String에 쓰기:
let mut s = String::new();
write!(s, "格式化 {}", 42).unwrap();
println!("{}", s);
// 파일은 파일 절 참조
// io::stdout()도 사용 가능:
// let mut out = io::stdout();
// writeln!(out, "直接写 stdout")
// Flush로 수동 플러시
}

표준 스트림

stdin / stdout / stderr. 대화형과 파이프 데이터 모두 지원.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::io::{self, Read, Write};
fn main() {
// 파이프로 전체 읽기:
// echo data | program
let mut buf = String::new();
io::stdin().read_to_string(&mut buf).unwrap();
// 파이프를 줄 단위 처리:
// for line in io::stdin().lines() {
// println!("{} ", line.unwrap());
// }
// stdout에 출력:
println!("处理完成");
// stderr에 오류:
eprintln!("警告: 非致命");
// stdout에 쓰기:
// io::stdout().write_all(b"raw\n")
// 대화형 프롬프트:
// print!("输入名字: ");
// io::stdout().flush().unwrap();
}

바이트 I/O

read로 바이트를 버퍼에. 바이너리 스트림 처리. write_all로 전부 쓰기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::fs::File;
use std::io::{self, Read, Write};
fn main() {
// 버퍼에 읽기:
let mut f = File::open("data.bin").unwrap();
let mut buf = [0u8; 1024];
let n = f.read(&mut buf).unwrap();
println!("读取 {} 字节", n);
// EOF까지 반복 읽기:
let mut f = File::open("data.bin").unwrap();
let mut total = 0;
let mut buf = [0u8; 256];
loop {
let n = f.read(&mut buf).unwrap();
if n == 0 { break; } // EOF
total += n;
// buf[..n] 처리
}
// 전체 쓰기:
let mut out = File::create("out.bin").unwrap();
out.write_all(&[1, 2, 3, 4]).unwrap();
// 전체 바이트 읽기:
let all = std::fs::read("data.bin").unwrap();
println!("总 {} 字节", all.len());
}

13.자주 하는 실수

Rust 초보자가 가장 자주 빠지는 함정과 올바른 작성법.

이동 후 사용

이동된 값은 더는 사용 불가. 빌리거나 clone으로 소유권 보존.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BAD:이동 후 접근
fn main() {
let s = String::from("hi");
let t = s; // 이동
// println!("{}", s); // 오류:이동됨
println!("{}", t);
}
// GOOD:빌림
fn main() {
let s = String::from("hi");
let t = &s; // 빌림
println!("{} {}", s, t);
}
// GOOD:명시적 클론
let s = String::from("hi");
let t = s.clone();
// 상황:
// 함수에 값을 넘기면 소유권이 이동
// 두 곳에서 써야 할 때
// 빌림이 효율적, clone은 더 직접적

빌림 충돌

불변 빌림과 가변 빌림은 공존 불가. 스코프로 빌림 범위를 좁힘.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// BAD:동시 빌림
fn main() {
let mut v = vec![1, 2, 3];
// let r1 = &v; // 불변
// v.push(4); // 가변, 충돌
// println!("{:?}", r1);
}
// GOOD:불변 빌림을 먼저 끝냄
fn main() {
let mut v = vec![1, 2, 3];
let r1 = &v[0];
println!("{}", r1); // 빌림 종료
v.push(4); // 이제 허용
}
// GOOD:스코프 분리
{
let r = &v;
println!("{:?}", r);
} // 빌림 종료
v.push(4);
// 규칙:
// 읽기-읽기 가능, 읽기-쓰기 불가
// 컴파일러가 데이터 경쟁 없음 보장

댕글링 참조

지역 변수의 참조를 반환하면 댕글링이 발생합니다. 소유권을 가진 값이나 수명을 명시해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// BAD:로컬 참조 반환
// fn bad() -> &String {
// let s = String::from("x");
// &s // 오류:s가 해제됨
// }
// GOOD:소유권 반환
fn good() -> String {
String::from("x")
}
// GOOD:입력 참조
fn first_char(s: &str) -> &str {
&s[..1]
}
// GOOD:수명 매개변수
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 규칙:
// 반환 참조는 유효해야 함
// 힙이나 인자의 수명을 가리킴
// 컴파일러(대여 검사기)가 차단

String 연결

+는 왼쪽을 이동. 반복 +는 비효율. format! 또는 push_str 사용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BAD:+ 반복 결합
let mut s = String::new();
// for _ in 0..10 {
// s = s + "x"; // 매번 재할당
// }
// GOOD:push_str로 추가
let mut s = String::new();
for _ in 0..10 {
s.push_str("x");
}
// GOOD:format!로 일괄
let a = "hello";
let b = "world";
let s = format!("{}, {}", a, b);
// + 문법:
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = s1 + &s2; // s1은 이동
// 이유:
// +는 좌변에 String 필요
// 대량 결합은 용량 확장이 잦음
// format!/join이 우수

인덱스 타입

인덱스와 길이는 usize입니다. 컬렉션 인덱스 타입이 맞지 않을 수 있음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// BAD:i32 인덱스
fn main() {
let v = vec![1, 2, 3];
let i: i32 = 1;
// let x = v[i]; // 오류:usize 필요
// let x = v[i as usize]; // 변환
}
// GOOD:직접 usize
fn main() {
let v = vec![1, 2, 3];
let i: usize = 1;
let x = v[i]; // 정상
println!("{}", x);
}
// 상황:
// for (i, x) in v.iter().enumerate()
// 반환은 usize
// len()도 usize
// 거리 계산에서 타입이 안 맞을 때:
// as usize으로 변환
// 또는 변수 타입 조정

match 가지 타입

match의 모든 가지는 같은 반환 타입이어야 함. 누락된 가지는 _ 로 완전 매칭.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// BAD:분기의 타입 불일치
// let x = match 5 {
// 1 => "one",
// _ => 42, // 오류:str vs i32
// };
// GOOD:반환 타입 통일
let x = match 5 {
1 => "one",
_ => "other", // 둘 다 &str
};
// BAD:철저하지 않음
// enum Color { R, G, B }
// let c = Color::R;
// match c {
// Color::R => {}
// Color::G => {}
// // B 누락, 컴파일 오류
// }
// GOOD:와일드카드 추가
// match c {
// Color::R => {}
// _ => {}
// }
// 규칙:
// 철저성은 컴파일 시점에 검사
// 타입 추론은 첫 분기를 따름

클로저 캡처

클로저는 필요에 따라 자동으로 빌림 또는 이동. 동시 빌림은 충돌. move로 명시적 이동.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// BAD:클로저 빌림과 변경 충돌
fn main() {
let mut count = 0;
// let print = || println!("{}", count);
// count += 1; // 충돌:이미 빌림
// print();
}
// GOOD:클로저를 먼저 끝냄
fn main() {
let mut count = 0;
{
let print = || println!("{}", count);
print();
} // 빌림 종료
count += 1;
}
// GOOD:move 클로저
let data = vec![1, 2];
let handler = move || {
println!("{:?}", data); // 소유권 가짐
};
// 규칙:
// Fn / FnMut / FnOnce
// 캡처 방식으로 자동 선택
// 스레드에 넘기는 클로저는 move 필요
// 충돌 시 빌림 범위를 좁힘

정수 오버플로

디버그 빌드는 오버플로에 panic. 릴리스 빌드는 wrap. 명시 처리 또는 더 큰 타입.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BAD:디버그 빌드에서 오버플로 panic
// fn main() {
// let x: u8 = 255;
// let y = x + 1; // 디버그에서 panic!
// }
// GOOD:명시적으로 검사
fn main() {
let x: u8 = 255;
match x.checked_add(1) {
Some(v) => println!("{}", v),
None => println!("溢出"),
}
// 다른 메서드:
// x.checked_add / checked_mul
// x.wrapping_add // 순환
// x.saturating_add // 상한에서 포화
// x.overflowing_add // 올림 반환
// 릴리스 빌드는 기본적으로 순환
// 큰 카운터는 u64/usize
// 명확한 의미로 메서드 선택

14.동시성

스레드, channel, 공유 상태, Send / Sync.

스레드

thread::spawn으로 새 스레드 시작. join으로 완료 대기. 반환값도 전달 가능.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::thread;
use std::time::Duration;
fn main() {
// 스레드 생성:
let handle = thread::spawn(|| {
for i in 1..3 {
println!("子线程 {}", i);
thread::sleep(Duration::from_millis(10));
}
});
// 메인 스레드는 계속 진행:
for i in 1..3 {
println!("主线程 {}", i);
}
// 자식 스레드 종료 대기:
handle.join().unwrap();
// join으로 반환값 획득:
let h = thread::spawn(|| 42);
let result = h.join().unwrap();
println!("{}", result); // 42
}

move 클로저

move 클로저는 캡처한 값을 스레드로 옮깁니다. 빌림의 댕글링을 피할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::thread;
fn main() {
let data = String::from("hello");
// 스레드가 data를 빌림:
let h1 = thread::spawn(move || {
println!("{} 世界", data);
});
// data는 이동됨, 메인에서 사용 불가
// 멀티스레드 공유에는:
// Arc 사용(공유 절 참조)
// 루프 안의 move:
let mut handles = vec![];
for i in 0..3 {
handles.push(thread::spawn(move || {
println!("线程 {}", i);
}));
}
for h in handles {
h.join().unwrap();
}
// move 없는 클로저:
// 참조를 캡처, 메인을 빌릴 수도
// 스레드가 빌림보다 오래 살 수 있음
// move는 소유권 이동을 명시

channel

mpsc는 다중 생산자 단일 소비자. send / recv로 데이터 전달. 타입 안전.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::sync::mpsc;
use std::thread;
fn main() {
// 채널 생성:
let (tx, rx) = mpsc::channel();
// 송신측을 자식 스레드로 이동:
thread::spawn(move || {
tx.send(String::from("hello")).unwrap();
});
// 수신:
let received = rx.recv().unwrap();
println!("收到: {}", received);
// recv는 블로킹 대기
// 송신자가 모두 Drop되면 recv 오류
// try_recv는 비차단:
// match rx.try_recv() {
// Ok(v) => ...,
// Err(_) => println!("暂无数据"),
// }
// 반복 수신:
// for v in rx {
// println!("{}", v);
// }

다중 생산자

Sender를 복제해 다중 생산자. 수신 측은 공유.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
// 송신단 복제:
let tx1 = tx.clone();
let tx2 = tx.clone();
// 생산자 3개:
thread::spawn(move || {
tx.send(1).unwrap();
});
thread::spawn(move || {
tx1.send(2).unwrap();
});
thread::spawn(move || {
tx2.send(3).unwrap();
});
// 메인 스레드가 소비:
for _ in 0..3 {
let v = rx.recv().unwrap();
println!("收到 {}", v);
}
// 수신 순서 보장 안 됨
// 단일 소비자:
// mpsc는 rx 1개만 허용
// 다중 소비자는 분할이나 뮤텍스 필요
// 채널은 메시지 전달의 첫 선택
// 공유 가변 상태 회피

공유 가변

Arc<Mutex<T>>로 스레드 간 가변 데이터 공유. 잠금이 읽기/쓰기를 보호.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 공유 카운터:
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1; // 임계 영역
}));
}
for h in handles {
h.join().unwrap();
}
println!("结果: {}", *counter.lock().unwrap());
// Mutex는 상호 배제 보장
// Arc는 참조 카운트를 스레드 안전하게
// 락 Guard는 스코프를 벗어나면 해제
// 락 보유 시간은 짧게
// 락 안에서 재락 호출 피함
// 교착 상태는 순서 주의

scoped 스레드

thread::scope로 'static이 아닌 데이터를 빌림. 소유권 이동 불필요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::thread;
fn main() {
let mut data = vec![1, 2, 3];
// 스코프 내 데이터 빌림:
thread::scope(|s| {
// 불변 빌림:
s.spawn(|| {
println!("{:?}", data);
});
// 가변 빌림(스레드별):
s.spawn(|| {
data.push(4); // 이 스레드만
});
}); // 스코프 종료, 모든 스레드 완료
println!("{:?}", data);
// thread::spawn과의 차이:
// spawn은 'static 소유권 필요
// scope는 로컬 변수 빌림 가능
// scope 종료 시 자동 join
// 적용:로컬 데이터의 병렬 처리
}

rayon 병렬

rayon 병렬 반복자. par_iter로 iter 대체. 데이터 병렬이 쉬워짐.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Cargo.toml:
// rayon = "1"
use rayon::prelude::*;
fn main() {
let v: Vec<i32> = (0..1000).collect();
// 병렬 합계:
let sum: i32 = v.par_iter().sum();
println!("{}", sum);
// 병렬 맵:
let doubled: Vec<i32> =
v.par_iter().map(|x| x * 2).collect();
// 병렬 필터:
let evens: Vec<&i32> =
v.par_iter().filter(|x| **x % 2 == 0).collect();
// 병렬 정렬:
let mut sorted = vec![3, 1, 2];
sorted.par_sort();
// 데이터는 Send여야 함
// 자동 분할 병렬화
// 큰 컬렉션에서 효과 명확
// 작은 컬렉션은 오버헤드 큼
// 결과는 직렬과 일치

Send 와 Sync

Send는 스레드 간 이동 가능. Sync는 참조 공유 가능. 안전한 동시성 보장.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Send:소유권을 스레드 간에 이동 가능
// 대부분의 타입은 Send
// 예외:Rc<T>는 Send 아님
// Sync:&T로 공유 가능
// 예외:RefCell은 Sync 아님
use std::rc::Rc;
use std::thread;
fn main() {
// Rc는 스레드에 넣을 수 없음:
// let rc = Rc::new(5);
// thread::spawn(move || println!("{}", rc));
// 오류:Rc는 Send 아님
// Arc로 대체:
let arc = std::sync::Arc::new(5);
let h = thread::spawn(move || println!("{}", arc));
h.join().unwrap();
// 규칙:
// Send 타입은 스레드를 넘어갈 수 있음
// Sync 타입은 참조 공유가 안전
// 컴파일러가 스레드 간 사용 검사
// 사용자 정의 타입은 자동 파생
// unsafe impl은 신중히

15.네트워크

TCP, UDP, HTTP, 서드파티 네트워크 crate.

TCP 연결

TcpStream으로 연결. read / write로 바이트 송수신. 스트림 프로토콜.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::net::TcpStream;
use std::io::{Read, Write};
fn main() {
// 연결 수립:
let mut stream = TcpStream::connect(
"example.com:80").unwrap();
// HTTP 요청 전송:
stream.write_all(
b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
.unwrap();
// 응답 읽기:
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).unwrap();
println!("{}", String::from_utf8_lossy(&buf[..n]));
// 연결 오류 처리:
// TcpStream::connect는 Result 반환
// 타임아웃 설정:
// stream.set_read_timeout(Some(Duration::from_secs(5)))
// 양방향 스트림 통신
}

TCP 리슨

TcpListener로 바인딩/리스닝. accept로 연결 수락. 다중 연결은 스레드로 처리.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;
fn main() {
// 포트에 바인딩:
let listener = TcpListener::bind("127.0.0.1:8080")
.unwrap();
println!("监听 8080");
// 연결 수락:
for stream in listener.incoming() {
match stream {
Ok(stream) => {
// 연결마다 스레드:
thread::spawn(|| handle(stream));
}
Err(e) => eprintln!("连接错误: {}", e),
}
}
}
fn handle(mut stream: TcpStream) {
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).unwrap();
println!("收到 {} 字节", n);
stream.write_all(&buf[..n]).unwrap();
}

HTTP 요청

reqwest로 HTTP 요청. JSON 요청/응답 지원.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Cargo.toml:
// reqwest = { version = "0.12", features = ["json"] }
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// GET 요청:
let resp = reqwest::Client::new()
.get("https://api.example.com/users")
.timeout(Duration::from_secs(10))
.send()
.await?;
// 상태 코드:
println!("状态: {}", resp.status());
// 텍스트 읽기:
let body = resp.text().await?;
println!("{}", &body[..200]);
Ok(())
}
// tokio 런타임 필요:
// tokio = { version = "1", features = ["full"] }
// JSON:
// .json::<T>()로 역직렬화
// serde와 조합

HTTP 서버

axum으로 HTTP 서비스 구축. 라우팅과 핸들러. 비동기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Cargo.toml:
// axum = "0.7"
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use axum::{Router, routing::get};
use axum::response::Json;
use serde_json::{json, Value};
#[tokio::main]
async fn main() {
// 라우팅:
let app = Router::new()
.route("/", get(root))
.route("/api/ping", get(ping));
// 실행:
let listener = tokio::net::TcpListener::bind(
"127.0.0.1:3000").await.unwrap();
println!("服务运行在 3000");
axum::serve(listener, app).await.unwrap();
}
async fn root() -> &'static str {
"Hello, GuruToolkit!"
}
async fn ping() -> Json<Value> {
Json(json!({ "ok": true }))
}
// 경로 매개변수:
// .route("/user/{id}", get(user_by_id))

URL 처리

url crate로 URL 파싱 및 구성. 쿼리 파라미터.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Cargo.toml:
// url = "2"
use url::Url;
fn main() {
// 파싱:
let url = Url::parse(
"https://[email protected]/path?q=rust#sec")
.unwrap();
// 필드 접근:
println!("{}", url.scheme()); // https
println!("{}", url.host_str().unwrap());
println!("{}", url.path());
println!("{}", url.fragment().unwrap());
// 쿼리 매개변수:
let pairs: Vec<(String, String)> =
url.query_pairs().into_owned().collect();
for (k, v) in pairs {
println!("{}={}", k, v);
}
// URL 구성:
let mut u = Url::parse("https://api.com").unwrap();
u.set_path("/v1/users");
u.query_pairs_mut()
.append_pair("page", "2");
println!("{}", u);
}

IP 파싱

std::net::IpAddr로 IP 파싱. IPv4 / IPv6 판별.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
fn main() {
// IP 파싱:
let ip: IpAddr = "192.168.1.1".parse().unwrap();
println!("{}", ip);
// 버전 판별:
match ip {
IpAddr::V4(v) => println!("IPv4: {}", v),
IpAddr::V6(v) => println!("IPv6: {}", v),
}
// 생성:
let v4 = Ipv4Addr::new(127, 0, 0, 1);
let v6: Ipv6Addr = "::1".parse().unwrap();
// 프로퍼티:
println!("回环: {}", v4.is_loopback());
println!("私有: {}", v4.is_private());
// 포트와 조합:
let addr = "127.0.0.1:8080".parse::<std::net::SocketAddr>().unwrap();
println!("{}", addr.port());
// 도메인 해석:
// use std::net::ToSocketAddrs;
// let mut it = "example.com:80".to_socket_addrs().unwrap();
}

UDP

UdpSocket은 비연결 데이터그램. send_to / recv_from. 핸드셰이크 불필요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::net::UdpSocket;
fn main() {
// 로컬에 바인딩:
let socket = UdpSocket::bind("127.0.0.1:0")
.unwrap();
// 데이터그램 전송:
socket.send_to(b"hello", "127.0.0.1:9999")
.unwrap();
// 수신:
let mut buf = [0u8; 1024];
let (n, src) = socket.recv_from(&mut buf).unwrap();
println!("来自 {}: {}", src,
String::from_utf8_lossy(&buf[..n]));
// 서버측:
let server = UdpSocket::bind("127.0.0.1:9999")
.unwrap();
// 특징:
// 비연결형, 핸드셰이크 없음
// 패킷 유실·순서 바뀜 가능
// TCP보다 효율적
// 적합:DNS、동영상、게임
// 신뢰성 있는 전송은 직접 구현
}

DNS 해석

ToSocketAddrs로 호스트명 해석. 주소 반복자 반환.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::net::ToSocketAddrs;
fn main() {
// 도메인 해석:
let addrs = "example.com:80"
.to_socket_addrs().unwrap();
for addr in addrs {
println!("{}", addr);
}
// 포트 기본값:
let addrs = "example.com"
.to_socket_addrs().unwrap();
// 해석은 여러 주소를 반환할 수 있음
// 순서대로 연결 시도
// 오류 처리:
match "bad-domain.invalid:80".to_socket_addrs() {
Ok(it) => println!("解析成功"),
Err(e) => println!("解析失败: {}", e),
}
// 비동기 DNS:
// 서드파티 tokio::net
// 프로덕션은 resolv-conf
// 테스트는 로컬 hosts

16.시간과 날짜

SystemTime, Instant, Duration, chrono.

SystemTime

SystemTime은 시스템 시계. 현재 시각을 UNIX_EPOCH와 비교.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::time::SystemTime;
fn main() {
// 현재 시간:
let now = SystemTime::now();
// UNIX 에포크:
let epoch = SystemTime::UNIX_EPOCH;
// 에포크로부터 경과:
match now.duration_since(epoch) {
Ok(d) => println!("距纪元 {:?}", d),
Err(e) => println!("系统时间早于纪元: {:?}", e),
}
// 비교:
let later = SystemTime::now();
let elapsed = later.duration_since(now).unwrap();
println!("经过 {:?}", elapsed);
// 제한:
// 부정확(시스템 시계가 바뀔 수 있음)
// 측정에는 Instant 사용
// 날짜 처리에는 chrono
// 타임스탬프:
let secs = now.duration_since(epoch)
.unwrap().as_secs();
println!("{} 秒", secs);
}

Instant 측정

Instant는 단조 시계로 경과 시간 측정. 시스템 시각 변경 영향 없음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::time::Instant;
fn main() {
// 측정 시작:
let start = Instant::now();
// 시간이 걸리는 처리:
std::thread::sleep(std::time::Duration::from_millis(50));
// 경과 시간:
let elapsed = start.elapsed();
println!("耗时: {:?}", elapsed);
// 다른 단위:
println!("{} 微秒", elapsed.as_micros());
println!("{} 毫秒", elapsed.as_millis());
// 여러 시작점 비교:
let a = Instant::now();
let b = Instant::now();
let diff = b.duration_since(a);
// 사용 상황:
// 성능 벤치, 타임아웃 제어
// 단조 증가, 시계 영향 없음
// 프로세스 간 비교 불가
}

Duration

Duration은 시간 길이. 초/나노초 정밀도. 산술과 단위 변환.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::time::Duration;
fn main() {
// 생성:
let sec = Duration::from_secs(5);
let ms = Duration::from_millis(1500);
let us = Duration::from_micros(100);
let ns = Duration::from_nanos(1);
// 조합:
let d = Duration::new(2, 500_000_000); // 2.5s
// 연산:
let sum = sec + ms;
let mul = sec * 2;
// 비교:
let a = Duration::from_secs(1);
let b = Duration::from_millis(500);
println!("{} > {}", a > b, a > b);
// 변환:
println!("{} 秒", sec.as_secs());
println!("{} 毫秒", ms.as_millis());
// 용도:
// sleep, 타임아웃, 대기
// 네트워크 타임아웃 설정
}

chrono 현재 시각

chrono crate로 일시 처리. Utc / Local 시간대. 사람이 읽기 쉬움.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Cargo.toml:
// chrono = "0.4"
use chrono::{Utc, Local, NaiveDate};
fn main() {
// UTC 시간:
let utc = Utc::now();
println!("{}", utc);
// 로컬 시간:
let local = Local::now();
println!("{}", local);
// 필드 접근:
println!("年: {}", utc.year());
println!("月: {}", utc.month());
println!("日: {}", utc.day());
println!("时: {}", utc.hour());
println!("星期: {}", utc.weekday());
// 타임스탬프:
println!("{}", utc.timestamp());
// 날짜 구성:
let d = NaiveDate::from_ymd_opt(2026, 8, 2)
.unwrap();
println!("{}", d);
// 타임존 표기:
// +00:00 UTC / +08:00 중국
}

서식 출력

format으로 일시 서식. strftime 스타일 자리표시자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// chrono = "0.4"
use chrono::Utc;
fn main() {
let now = Utc::now();
// 포맷:
println!("{}", now.format("%Y-%m-%d")); // 2026-08-02
println!("{}", now.format("%H:%M:%S")); // 14:30:22
println!("{}", now.format("%Y-%m-%d %H:%M"));
// 자주 쓰는 플레이스홀더:
// %Y 년 %m 월 %d 일
// %H 시 %M 분 %S 초
// %A 요일명 %a 약어
// %B 월명
// %j 연중 통산 일수
// 커스텀:
println!("{}", now.format("%Y/%m/%d %H:%M:%S %z"));
// 기본 Display:
println!("{}", now);
// String 반환:
let s = now.format("%F").to_string();
println!("{}", s); // ISO 날짜
}

문자열 파싱

DateTime::parse_from_str로 문자열 파싱. ISO 8601 지원.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// chrono = "0.4"
use chrono::{DateTime, NaiveDateTime, Utc};
fn main() {
// 고정 포맷 파싱:
let dt = DateTime::parse_from_str(
"2026-08-02 10:30:00 +0800",
"%Y-%m-%d %H:%M:%S %z").unwrap();
println!("{}", dt);
// ISO 8601:
let iso = DateTime::parse_from_rfc3339(
"2026-08-02T10:30:00+08:00").unwrap();
println!("{}", iso);
// 타임존 없이 파싱:
let naive = NaiveDateTime::parse_from_str(
"2026-08-02 10:30:00",
"%Y-%m-%d %H:%M:%S").unwrap();
println!("{}", naive);
// UTC로 변환:
let utc: DateTime<Utc> = iso.with_timezone(&Utc);
println!("{}", utc);
// 파싱 실패는 Err 반환
}

시각 연산

시각의 가감, 간격 계산, 범위 판정.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// chrono = "0.4"
use chrono::{Duration, Utc, Local};
fn main() {
let now = Utc::now();
// 가감산:
let tomorrow = now + Duration::days(1);
let next_hour = now + Duration::hours(1);
let last_week = now - Duration::weeks(1);
println!("{}", tomorrow.format("%F"));
// 간격:
let later = now + Duration::minutes(90);
let diff = later - now;
println!("间隔 {} 分钟", diff.num_minutes());
// 날짜 범위 판정:
let start = now - Duration::hours(1);
let end = now + Duration::hours(1);
let now_local = Local::now();
let n = now_local.with_timezone(&Utc);
if n >= start && n <= end {
println!("在范围内");
}
// 타임존:
// FixedOffset::east_opt(8 * 3600)
// 중국 표준시 +08:00
}

시간대 처리

FixedOffset로 고정 시간대 표현. 시간대 간 변환 가능.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// chrono = "0.4"
use chrono::{Utc, Local, FixedOffset, TimeZone};
fn main() {
// 고정 타임존(중국 +08:00):
let cst = FixedOffset::east_opt(8 * 3600).unwrap();
let now = Utc::now();
let beijing = now.with_timezone(&cst);
println!("北京: {}", beijing);
// 로컬 타임존:
let local = Local::now();
println!("本地: {}", local);
// UTC로 되돌림:
let utc = beijing.with_timezone(&Utc);
println!("UTC: {}", utc);
// 임의 타임존:
let tokyo = FixedOffset::east_opt(9 * 3600).unwrap();
let tokyo_now = now.with_timezone(&tokyo);
println!("东京: {}", tokyo_now);
// 오프셋:
println!("偏移: {}", cst); // +08:00
// 저장은 UTC 권장
// 표시 시 타임존 변환
}

17.프로세스와 환경

커맨드라인 인자, 환경 변수, 자식 프로세스, 경로.

인자와 환경

std::env로 인자와 환경 변수를 읽음. args, var.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::env;
fn main() {
// 명령줄 인자:
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
// 첫 번째는 프로그램 경로
// 환경 변수:
match env::var("PATH") {
Ok(v) => println!("PATH: {}", v),
Err(e) => println!("未设置: {:?}", e),
}
// 기본값 포함:
let port = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string());
// 존재 판정:
// env::var_os("KEY").is_some()
// 환경 변수 설정:
// env::set_var("KEY", "value")
// 전체 순회:
// for (k, v) in env::vars() {}
}

clap 인자 파싱

clap로 커맨드라인 인자 파싱. 서브커맨드, 옵션, 자동 도움말.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Cargo.toml:
// clap = { version = "4", features = ["derive"] }
use clap::Parser;
// 인자 정의:
#[derive(Parser, Debug)]
#[command(name = "app", version, about)]
struct Args {
/// 사용자 이름
#[arg(short, long)]
name: String,
/// 횟수
#[arg(short, long, default_value_t = 1)]
count: u32,
/// 상세 출력
#[arg(short, long)]
verbose: bool,
}
fn main() {
let args = Args::parse();
println!("{:?}", args);
for _ in 0..args.count {
println!("你好 {}", args.name);
}
}
// 실행:
// app --name Nick --count 3 -v
// 도움말 자동 생성:--help
// 인자 검증 자동 완료

std::process

종료 코드, Command로 자식 프로세스 실행, 현재 프로세스 정보.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::process;
fn main() {
// 종료 코드:
// process::exit(0) 성공
// process::exit(1) 실패
// 현재 프로세스 ID:
println!("PID: {}", process::id());
// 자식 프로세스 실행:
let out = process::Command::new("ls")
.arg("-l")
.output()
.expect("运行失败");
println!("状态: {}", out.status);
println!("输出: {}",
String::from_utf8_lossy(&out.stdout));
// 성공 여부 확인:
if out.status.success() {
println!("命令成功");
}
// 완료 대기:
// process::Command::new("x").status()
}

Command 자식 프로세스

Command로 자식 프로세스 제어. 인자, 환경, 파이프, 출력 캡처.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::process::Command;
use std::io::Write;
fn main() {
// 인자 포함 실행:
let mut cmd = Command::new("echo");
cmd.arg("hello").arg("world");
let output = cmd.output().unwrap();
println!("{}",
String::from_utf8_lossy(&output.stdout));
// 파이프 입력:
let mut child = Command::new("cat")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn().unwrap();
child.stdin.as_mut().unwrap()
.write_all(b"data").unwrap();
// 환경 변수 설정:
let out = Command::new("sh")
.arg("-c")
.arg("echo $MYVAR")
.env("MYVAR", "hello")
.output().unwrap();
println!("{}",
String::from_utf8_lossy(&out.stdout));
}

PathBuf

Path / PathBuf로 경로 조작. 결합, 컴포넌트, 정규화. 크로스플랫폼.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::path::{Path, PathBuf};
fn main() {
// 생성:
let mut p = PathBuf::from("/tmp");
p.push("data");
p.push("file.txt");
println!("{}", p.display());
// 구성 요소:
println!("父: {:?}", p.parent());
println!("名: {:?}", p.file_name());
println!("扩展名: {:?}", p.extension());
// 판정:
println!("绝对: {}", p.is_absolute());
println!("存在: {}", p.exists());
// 정규화:
let messy = Path::new("./a/../b");
let clean = messy.canonicalize();
// 문자열과 결합:
let joined = Path::new("/data")
.join(format!("{}.log", 2026));
println!("{}", joined.display());
// &str과 상호 변환:
let s = p.to_string_lossy();
}

작업 디렉터리

현재 디렉터리, 디렉터리 변경, 홈 디렉터리. 경로 관련 헬퍼.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::env;
fn main() {
// 현재 디렉터리:
let cwd = env::current_dir().unwrap();
println!("{}", cwd.display());
// 디렉터리 이동:
// env::set_current_dir("/tmp").unwrap();
// 사용자 홈:
let home = env::home_dir();
match home {
Some(h) => println!("主目录: {}", h.display()),
None => println!("无主目录"),
}
// 임시 디렉터리:
let tmp = env::temp_dir();
println!("临时: {}", tmp.display());
// 실행 파일 경로:
let exe = env::current_exe().unwrap();
println!("程序: {}", exe.display());
// 상대 경로 결합:
let config = env::current_dir().unwrap()
.join("config")
.join("app.toml");
}

디렉터리 순회

read_dir로 디렉터리 열거. 재귀는 직접 구현. 항목 타입으로 필터 가능.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::fs;
fn main() {
// 디렉터리 읽기:
let entries = fs::read_dir(".").unwrap();
for entry in entries {
let e = entry.unwrap();
let path = e.path();
// 타입 판정:
if path.is_dir() {
println!("[目录] {}", path.display());
} else {
println!("[文件] {}", path.display());
}
}
// 파일 이름 수집:
let files: Vec<String> = fs::read_dir("src")
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.map(|e| e.file_name().to_string_lossy()
.into_owned())
.collect();
println!("{:?}", files);
// 재귀는 직접 구현 또는 walkdir 사용
}

시그널 처리

ctrlc crate로 Ctrl-C 포착. 우아한 종료와 자원 정리.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Cargo.toml:
// ctrlc = "3"
use std::sync::atomic::{
AtomicBool, Ordering
};
use std::sync::Arc;
fn main() {
// Ctrl+C 캡처:
let running = Arc::new(AtomicBool::new(true));
let r = Arc::clone(&running);
ctrlc::set_handler(move || {
println!("\n收到中断,退出中...");
r.store(false, Ordering::SeqCst);
}).expect("设置信号处理失败");
// 메인 루프:
while running.load(Ordering::SeqCst) {
// 작업 처리...
std::thread::sleep(
std::time::Duration::from_millis(100));
}
println!("清理完成,再见");
}
// 용도:
// 상태 저장, 연결 종료
// 서비스의 그레이스풀 셧다운
// 표준 라이브러리:
// 시그널 원시 처리는 libc/unsafe 필요

18.정규 표현식

regex crate로 매칭, 캡처, 치환, 분할.

정규식 컴파일

Regex::new로 컴파일. 원시 문자열 r#... 사용. 컴파일 비용이 큼, 재사용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Cargo.toml:
// regex = "1"
use regex::Regex;
fn main() {
// 컴파일:
let re = Regex::new(r"^[a-z]+$").unwrap();
// 원시 문자열 r"..."로 이스케이프 회피
// Result 반환, 잘못된 패턴은 오류
// 컴파일은 느림, 재사용:
// lazy_static/OnceLock으로 캐시
// 또는 스레드 로컬
// 자주 쓰는 패턴:
let email = Regex::new(
r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap();
println!("{}", email.is_match("[email protected]"));
// 오류:
match Regex::new(r"[") {
Ok(_) => {}
Err(e) => println!("正则错误: {}", e),
}
}

매칭 판정

is_match로 매칭 여부 판정. 부분 매칭이 기본. 전체 매칭은 앵커 필요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
// 매치 포함 여부:
println!("{}", re.is_match("abc123")); // true
// 부분 매치가 기본:
// 등장 여부만 판정
// 전체 매치:
let full = Regex::new(r"^\d+$").unwrap();
println!("{}", full.is_match("123")); // true
println!("{}", full.is_match("123a")); // false
// find로 위치 획득:
let m = re.find("abc123").unwrap();
println!("位置 {}..{}", m.start(), m.end());
// 대소문자:
let case = Regex::new(r"(?i)rust").unwrap();
println!("{}", case.is_match("RUST"));
}

캡처 그룹

캡처 그룹으로 부분 문자열을 추출합니다. captures는 그룹을 반환합니다. 명명된 그룹도 지원됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
// 캡처 그룹:
let re = Regex::new(r"(\d{4})-(\d{2})-(\d{2})")
.unwrap();
let caps = re.captures("日期 2026-08-02").unwrap();
// caps[0]은 전체 매치:
println!("{}", &caps[0]); // 2026-08-02
// caps[1..]은 각 그룹:
println!("年: {}", &caps[1]);
println!("月: {}", &caps[2]);
println!("日: {}", &caps[3]);
// 이름 있는 그룹:
let named = Regex::new(r"(?P<year>\d{4})")
.unwrap();
let caps = named.captures("2026").unwrap();
println!("年: {}", caps.name("year").unwrap().as_str());
// 매치 없음은 None 반환
// 여러 그룹 반복:
// captures_iter로 전체 매치

전체 매칭

find_iter로 모든 매칭 순회. captures_iter는 그룹 포함.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
let text = "a1 b22 c333";
// 전체 매치:
let nums: Vec<&str> =
re.find_iter(text).map(|m| m.as_str()).collect();
println!("{:?}", nums); // ["1", "22", "333"]
// 위치 반복:
for m in re.find_iter(text) {
println!("{} 在 {}..{}", m.as_str(), m.start(), m.end());
}
// 그룹 전체 획득:
let pair = Regex::new(r"(\w+)=(\w+)").unwrap();
for caps in pair.captures_iter("a=1 b=2") {
println!("{} = {}", &caps[1], &caps[2]);
}
// 매치 없음은 빈 이터레이터
}

치환

replace로 매칭 치환. $1로 그룹 참조. replace_all로 모두 치환.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
let re = Regex::new(r"\d+").unwrap();
// 전체 치환:
let out = re.replace_all("a1 b2", "#");
println!("{}", out); // a# b#
// 첫 항목만 치환:
let first = re.replace("a1 b2", "#");
println!("{}", first); // a# b2
// 그룹 참조:
let name = Regex::new(r"(\w+),\s*(\w+)").unwrap();
let out = name.replace_all("Doe, John", "$2 $1");
println!("{}", out); // John Doe
// 콜백 치환:
let out = re.replace_all("a1 b2", |caps: &regex::Captures| {
let n: i32 = caps[0].parse().unwrap();
format!("{}", n * 2)
});
println!("{}", out); // a2 b4
}

분할

정규식으로 문자열 split. splitn으로 분할 수 제한.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use regex::Regex;
fn main() {
// 구분자로 분할:
let re = Regex::new(r"[,;]+").unwrap();
let parts: Vec<&str> =
re.split("a,b;c,,d").collect();
println!("{:?}", parts); // ["a", "b", "c", "d"]
// 공백으로 분할:
let ws = Regex::new(r"\s+").unwrap();
let words: Vec<&str> =
ws.split("hello world").collect();
println!("{:?}", words);
// 개수 제한:
let parts2: Vec<&str> = re.splitn("a,b,c", 2).collect();
println!("{:?}", parts2); // ["a", "b,c"]
// 구분자 유지:
// let parts: Vec<&str> =
// re.split_inclusive("a,b", 0).collect();
// 단순 분할은 str::split이 빠름
// 복잡한 패턴만 regex

자주 쓰는 패턴

자주 쓰는 정규식: 이메일, URL, 전화번호, IP. 문자 클래스.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use regex::Regex;
fn main() {
// 이메일:
let email = Regex::new(
r"^[\w.+-]+@[\w-]+\.[\w.]+$").unwrap();
println!("{}", email.is_match("[email protected]"));
// URL:
let url = Regex::new(r"^https?://[\w.-]+(/\S*)?$").unwrap();
println!("{}", url.is_match("https://example.com"));
// 휴대폰 번호(중국):
let phone = Regex::new(r"^1[3-9]\d{9}$").unwrap();
println!("{}", phone.is_match("13800138000"));
// 문자 클래스:
// \d 숫자 \w 단어 \s 공백
// \D \W \S 부정
// + 1회 이상 * 0회 이상
// {2,4} 횟수 {2,} 최소 2회
// ^ 시작 $ 끝
// [a-z] [^0-9] 문자 범위
// 탐욕 ?로 비탐욕:\d+?
// 그룹 ( ) 또는 |
}

플래그와 대소문자

(?i) 등 인라인 플래그. RegexBuilder로 더 세밀히 설정.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use regex::Regex;
fn main() {
// 대소문자 무시:
let re = Regex::new(r"(?i)rust").unwrap();
println!("{}", re.is_match("RUST"));
println!("{}", re.is_match("rust"));
// 멀티라인 모드:
// (?m) ^ $는 각 줄에 매치
let m = Regex::new(r"(?m)^go").unwrap();
println!("{}", m.is_match("x\ngo"));
// 점이 줄바꿈에 매치:
// (?s) . 는 \n에 매치
// 인라인이 아닌 경우:
// RegexBuilder로 설정 가능
let re = regex::RegexBuilder::new("rust")
.case_insensitive(true)
.multi_line(true)
.build().unwrap();
println!("{}", re.is_match("RUST"));
// 단일 행:
// .size_limit()로 복잡도 제한
// 기본 UTF-8, 비활성화는 bytes
}

19.빌드와 테스트

cargo 빌드, 단위 테스트, lint, 릴리스 설정.

cargo build

프로젝트 빌드. 디버그/릴리스 프로파일. 산출물 경로. 증분 컴파일.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 디버그 빌드:
// cargo build
// 릴리스 빌드(최적화):
// cargo build --release
// 검사(바이너리 미생성):
// cargo check
// 산출물:
// target/debug/ target/release/
// 패키지 지정:
// cargo build -p mylib
// 증분 빌드:
// 변경 없는 파일은 재컴파일 안 함
// 피처 선택:
// cargo build --features xxx
// 정리:
// cargo clean
// 실행:
// cargo run
// 바이너리 직접 실행:
// ./target/debug/app
// 빌드 설정은 Cargo.toml [profile]

단위 테스트

#[test] 속성. cargo test가 실행. 단언 매크로.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 유닛 테스트:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_sub() {
assert!(sub(2, 1) == 1);
}
#[test]
#[should_panic] // panic 기대
fn test_bad() {
panic!("boom");
}
}
fn add(a: i32, b: i32) -> i32 { a + b }
fn sub(a: i32, b: i32) -> i32 { a - b }
// 실행:
// cargo test
// 개별 실행:
// cargo test test_add
// 출력 표시:
// cargo test -- --nocapture
// 단언 매크로:
// assert_eq! / assert_ne!
// assert!는 메시지 포함

문서 테스트

/// 문서 주석의 코드는 테스트로 실행. cargo test가 검증. 통합 테스트.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// 두 수를 더합니다.
///
/// # 예시
/// ```
/// let result = add(1, 2);
/// assert_eq!(result, 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 문서 테스트:
// 예제 코드는 컴파일·실행됨
// cargo test로 검증
// 문서 생성:
// cargo doc --open
// 통합 테스트:
// tests/integration.rs
// 공개 API 직접 호출
// 실행:cargo test
// 문서 주석 문법:
// /// 문서 주석
// //! 모듈 레벨 문서
// ```rust,ignore 스킵
// ```text 컴파일 안 함

의존성 관리

Cargo.toml로 의존성 선언. SemVer. cargo update.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Cargo.toml 의존:
// [dependencies]
// serde = "1" // 모든 1.x 호환
// serde = "1.0.100" // 정확한 하한
// serde = "=1.0.100" // 정확한 버전
// serde = "^1.2" // 호환
// 옵션 의존:
// [dependencies]
// serde = { version = "1", optional = true }
// 개발 의존:
// [dev-dependencies]
// pretty_assertions = "1"
// 갱신:
// cargo update // 락 파일 갱신
// cargo add serde // 의존 추가
// cargo remove serde // 제거
// 낡음 검사:
// cargo outdated
// 취약점 감사:
// cargo audit

clippy 검사

cargo clippy가 코드 검사. 잠재적 버그와 스타일 이슈 발견.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 검사 실행:
// cargo clippy
// 엄격 모드:
// cargo clippy -- -D warnings
// 흔한 검사:
// 1. 불필요한 클론
// 2. 간략화할 수 있는 표현
// 3. 성능 함정
// 4. 의심스러운 로직
// 예시 문제:
// let v = vec![1, 2];
// if v.len() > 0 {} // clippy가 제안
// if !v.is_empty() {} // 더 나음
// 규칙 무시:
// #[allow(clippy::all)]
// 국소적 허용:
// #[allow(clippy::redundant_clone)]
// 설정 파일:
// clippy.toml
// CI 통합은 -D warnings가 일반적
// &를 .iter() 대신 쓰는 등의 제안

코드 서식

cargo fmt가 rustfmt로 일관된 서식 적용. 팀 스타일 통일.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 포맷:
// cargo fmt
// 서식 검사(수정 안 함):
// cargo fmt --check
// 설정 파일:
// rustfmt.toml
// 자주 쓰는 설정:
// max_width = 100
// tab_spaces = 4
// 특징:
// 자동 들여쓰기·정렬
// 임포트 정렬(nightly 필요):
// cargo +nightly fmt
// CI 검사:
// cargo fmt --check
// clippy와 병행:
// 먼저 fmt 후 clippy
// 에디터 저장 시 자동
// 팀 스타일 통일
// diff 충돌 감소

릴리스 설정

[profile.release]에서 최적화 설정. 바이너리 스트립과 LTO 등.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Cargo.toml:
// [profile.release]
// opt-level = 3 // 최적화 수준
// lto = true // 링크 타임 최적화
// codegen-units = 1 // 단일 코드 유닛
// panic = "abort" // 직접 종료
// strip = "symbols" // 심볼 제거
// debug = false // 디버그 정보 없음
// 작은 크기 조합:
// [profile.release]
// lto = true
// strip = "symbols"
// opt-level = "z" // 크기 우선
// 빌드:
// cargo build --release
// 크기 확인:
// ls -lh target/release/app
// 릴리스 전:
// cargo fmt --check
// cargo clippy -- -D warnings
// cargo test
// 버전 번호:
// Cargo.toml version 필드

문서 생성

cargo doc이 API 문서 생성. rustdoc 마크업. 릴리스 전 문서 테스트 실행.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 문서 생성:
// cargo doc
// 브라우저로 열기:
// cargo doc --open
// 문서 주석:
/// 두 수의 합을 계산합니다.
///
/// # 매개변수
/// - `a`:첫 번째 수
/// - `b`:두 번째 수
///
/// # 반환값
/// 합
///
/// # 예시
/// ```
/// assert_eq!(add(1, 2), 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 문서의 자주 쓰는 마커:
// # Examples # Panics
// # Errors # Safety
// 모듈 문서://!
// 내부 링크:\[`add`\]
// 비공개 항목은 생성 안 됨
// 릴리스 전 doc 테스트 실행

공식 링크

공식 문서와 리소스에 대한 직접 링크.

이 치트시트에 대해

이 페이지는 Rust 2021 에디션의 자기완결형 치트시트로, 언어 핵심과 실제 프로젝트에서 일상의 약 80%를 다루는 가장 자주 쓰이는 std API를 다룹니다. 현대적 관용구를 우선합니다: ?로 에러 전파, match의 완전 매칭, 수기로 작성한 루프 대신 반복자, String과 &str의 엄격한 구분. 권위 있는 참조는 Rust 공식 Book과 표준 라이브러리 문서를 보세요. 19개 섹션이 각 주제에 집중합니다 — 첫 프로그램부터 소유권, 트레이트, 동시성, 흔한 함정까지. 각 섹션은 예제 8개(5~20줄씩)로 나뉘어 총 약 150개 토픽. 코드 스니펫은 일부러 짧고 자기 설명적입니다. 모든 처리는 브라우저 내에서 이루어집니다 — 업로드도 추적도 없습니다. GuruToolkit 무료 개발자 도구 모음의 일부이며, 여기 스니펫은 무보증으로 자유롭게 사용할 수 있습니다.

버전 2.1.0