2-2. slices, string type, conversion
· 6분 읽기
Day 16-17: Slices 심화, String 내부, 타입 변환
참고: The Rust Book Chapter 4.3 (Slices), Chapter 8.2 (Strings)
1. 메모리 구조: String vs &str
String (소유자, 힙 할당)
스택 힙
┌──────────────┐ ┌───────────┐
│ ptr ─────────────▶ h e l l o │
│ len: 5 │ └───────────┘
│ capacity: 5 │
└──────────────┘
- ptr: 힙 데이터의 메모리 주소 (pointer)
- len: 현재 사용 중인 바이트 수
- capacity: 힙에 확보된 총 공간 (여유 공간 포함)
- 수정 가능 (push_str, push 등)
&str (빌려보기, 읽기 전용, fat pointer)
스택 프로그램 바이너리 (읽기전용)
┌──────────────┐ ┌───────────┐
│ ptr ─────────────▶ h e l l o │
│ len: 5 │ └───────────┘
└──────────────┘
- ptr + len 2개만 가짐 → fat pointer
- capacity 불필요 (수정 불가)
- 읽기 전용
핵심 비교
| 항목 | String | &str |
|---|---|---|
| 스택 데이터 | ptr + len + capacity | ptr + len (fat pointer) |
| 힙 사용 | O | X (데이터를 가리키기만) |
| 소유권 | 있음 | 없음 (빌려봄) |
| 수정 가능 | O (mut일 때) | X |
| 생성 | String::from("...") | "..." (리터럴) |
2. UTF-8과 String 인덱싱
len()은 바이트 수
"hello".len() // 5 (영문 1글자 = 1바이트)
"안녕".len() // 6 (한글 1글자 = 3바이트)
"러스트".len() // 9 (한글 3글자 × 3바이트)인덱싱 규칙
let s = String::from("안녕하세요");
// ❌ 컴파일 에러: 정수 인덱싱 금지
let c = s[0];
// ✅ 바이트 슬라이스 (경계 맞아야 함)
let slice = &s[0..3]; // "안" (3바이트 정확히)
// ⚠️ 런타임 panic: UTF-8 경계 불일치
let bad = &s[0..2]; // panic! "안"의 3바이트 중 2바이트만 자름
// ✅ 안전한 문자 접근
let ch = s.chars().nth(0); // Some('안')
let first: String = s.chars().take(1).collect(); // "안"컴파일 에러 vs panic
| 구분 | 컴파일 에러 | panic |
|---|---|---|
| 발생 시점 | 컴파일 시 (프로그램 생성 전) | 실행 중 |
| 예시 | s[0] (인덱싱 자체 금지) | &s[0..2] (UTF-8 경계 위반) |
| 특징 | 안전 (실행 자체 안 됨) | 위험 (실행 중 터짐) |
Rust의 방어 전략: 1차 — 컴파일에서 최대한 잡기, 2차 — 런타임 panic으로 보호
3. Deref Coercion (자동 역참조 변환)
핵심 규칙
&String → &str 자동 변환 (Deref coercion)
&Vec<T> → &[T] 자동 변환 (Deref coercion)
String → &str ❌ (& 필요)
Vec<T> → &[T] ❌ (& 필요)
&str → String ❌ (명시적 변환 필요: .to_string() 또는 String::from())
작동 원리
// String의 ptr + len + capacity에서
// capacity만 빼면 &str이 됨
// → 새로운 데이터 복사 없음, 비용 거의 없음 → 자동 허용
let s = String::from("hello");
// &s → &String → Deref coercion → &str실전 예시
fn greet(name: &str) {
println!("Hello, {}", name);
}
fn main() {
let s = String::from("Karpathy");
greet(&s); // ✅ &String → &str (Deref coercion)
greet("world"); // ✅ 리터럴은 이미 &str
greet(s); // ❌ String 자체, &가 없음
}fn sum(numbers: &[i32]) {
let total: i32 = numbers.iter().sum();
println!("{}", total);
}
fn main() {
let v = vec![1, 2, 3];
sum(&v); // ✅ &Vec<i32> → &[i32] (Deref coercion)
sum(&v[0..2]); // ✅ 이미 &[i32] 슬라이스
sum(v); // ❌ Vec<i32> 자체, &가 없음
}함수 파라미터 규칙
// 👍 읽기만 할 때: 슬라이스로 받기 (더 유연)
fn process_text(msg: &str) { ... } // String도 &str도 받을 수 있음
fn process_nums(data: &[i32]) { ... } // Vec도 슬라이스도 받을 수 있음
// 👎 제한적
fn process_text(msg: &String) { ... } // String만 받을 수 있음
fn process_nums(data: &Vec<i32>) { ... } // Vec만 받을 수 있음변환 방향 정리
// &str → String (수동, 힙 할당 비용)
let s: &str = "hello";
let owned = s.to_string(); // 방법 1
let owned = String::from(s); // 방법 2
// &String → &str (자동, 비용 거의 없음)
let owned = String::from("hello");
let borrowed: &str = &owned; // Deref coercionRust 철학: 비용 없는 건 자동, 비용 있는 건 명시적으로
4. 타입 변환: as vs From/Into
as (위험할 수 있는 변환)
let x: i32 = 42;
let y: f64 = x as f64; // 42.0 ✅ 안전
let big: i32 = 300;
let small: u8 = big as u8; // 44 ⚠️ 값 잘림 (300 % 256 = 44)- 항상 컴파일됨
- 값 손실이 조용히 발생할 수 있음 (에러도 panic도 없음)
- 개발자 책임
From/Into (안전한 변환만 허용)
// 안전한 변환: 작은 → 큰, 값 손실 없음
let a: i64 = i64::from(42i32); // ✅ i32 → i64
let b: f64 = f64::from(42i32); // ✅ i32 → f64
// 안전하지 않은 변환: 컴파일 에러
let c: i32 = i32::from(42i64); // ❌ i64 → i32 (값 잘릴 수 있음)
let d: i32 = i32::from(3.14f64); // ❌ f64 → i32 (소수점 잘림)
let e: u8 = u8::from(300i32); // ❌ i32 → u8 (범위 초과 가능)Into (From의 반대 방향)
let x: f64 = 42i32.into(); // i32 → f64 (From이 있으면 Into 자동 제공)비교 표
| 항목 | as | From/Into |
|---|---|---|
| 안전성 | 값 잘릴 수 있음 | 안전한 변환만 허용 |
| 컴파일 | 항상 됨 | 안전하지 않으면 컴파일 에러 |
| 값 손실 시 | 조용히 잘림 | 애초에 컴파일 안 됨 |
| 사용 시점 | 값 손실 감수할 때 | 안전한 변환이 보장될 때 |
5. ⚠️ Python 개발자 주의사항
| Python | Rust | 차이 |
|---|---|---|
s[0] → "h" | s[0] → 컴파일 에러 | Rust는 인덱싱 금지 |
len("안녕") → 2 (글자 수) | "안녕".len() → 6 (바이트 수) | len()의 의미가 다름 |
x = 42; y = float(x) | let y: f64 = x as f64; | 명시적 변환 필수 |
int(3.14) → 3 | 3.14f64 as i32 → 3 (위험) | as는 조용히 잘림 |
| 자동 타입 변환 빈번 | 자동 변환 거의 없음 | Rust는 명시성 우선 |
6. 핵심 원칙 정리
- Fat pointer:
&str= ptr + len,&[T]= ptr + len - UTF-8 안전: 인덱싱 금지, chars()로 문자 접근
- Deref coercion:
&소유타입→&슬라이스자동 (비용 없음) - 역방향 수동:
&str→String은 힙 할당이라 명시적 - 타입 변환: 안전하면
From/Into, 감수하면as - 함수 설계: 읽기만 하면 슬라이스(
&str,&[T])로 받기