본문으로 건너뛰기

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 + capacityptr + len (fat pointer)
힙 사용OX (데이터를 가리키기만)
소유권있음없음 (빌려봄)
수정 가능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 coercion

Rust 철학: 비용 없는 건 자동, 비용 있는 건 명시적으로


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 자동 제공)

비교 표

항목asFrom/Into
안전성값 잘릴 수 있음안전한 변환만 허용
컴파일항상 됨안전하지 않으면 컴파일 에러
값 손실 시조용히 잘림애초에 컴파일 안 됨
사용 시점값 손실 감수할 때안전한 변환이 보장될 때

5. ⚠️ Python 개발자 주의사항

PythonRust차이
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)33.14f64 as i323 (위험)as는 조용히 잘림
자동 타입 변환 빈번자동 변환 거의 없음Rust는 명시성 우선

6. 핵심 원칙 정리

  1. Fat pointer: &str = ptr + len, &[T] = ptr + len
  2. UTF-8 안전: 인덱싱 금지, chars()로 문자 접근
  3. Deref coercion: &소유타입&슬라이스 자동 (비용 없음)
  4. 역방향 수동: &strString은 힙 할당이라 명시적
  5. 타입 변환: 안전하면 From/Into, 감수하면 as
  6. 함수 설계: 읽기만 하면 슬라이스(&str, &[T])로 받기