벡터(Vector), 문자열(String), 슬라이스(Slice)

Rust에서는 데이터를 저장하고 조작하기 위해 다양한 컬렉션을 제공합니다. 컬렉션에는 벡터, 문자열, 슬라이스와 해시맵이 있는데, 오늘은 그중 자주 쓰이는 벡터, 문자열, 그리고 슬라이스에 대해 알아보고 다음 편에 해시맵(HashMap)에 대해 알아보겠습니다. 문자열은 Rust의 특이한 요소 중 하나입니다.


1. 벡터 (Vector)

Vec는 가변 길이의 배열로, 가장 자주 쓰이는 컬렉션 중 하나입니다.

fn main() {
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);

    println!("{:?}", v); // [1, 2, 3]
}
  • 고정된 길이의 array와 대비되고, 같은 데이터 타입이어야 하는 것은 동일합니다.
    데이터 형식으로 Vec안에 i32라고 하나만 지정되어 있어서 여러가지 형식 입력이 가능한 tuple과 다릅니다.
  • Vec::new()로 생성하고 push()로 요소 추가. pop()으로 마지막 요소 삭제
    v.pop(); // 마지막 요소 제거
    println!(“{:?}”, v); // [1, 2]
  • 벡터 v는 mut로 가변 변수로 선언해야 데이터 추가, 삭제, 수정 가능
  • println!(“{:?}”, v)로 Debug 포맷으로 벡터 출력.

가. 벡터 초기화

let v = vec![10, 20, 30];
  • vec!를 이용해 여러 요소를 한꺼번에 입력할 수 있습니다. vec!에서 !는 매크로라고 읽습니다.

나. 벡터 접근

fn main() {
    let v = vec![10, 20, 30];
    let third = v[2];
    println!("세 번째 값: {}", third);
    // println!("세 번째 값: {}", v[3]); // panic 발생

    let maybe = v.get(1); // Option 타입 반환

    if let Some(val) = maybe {
        println!("값: {}", val);
    } else {
        println!("없음"); // None일 때 실행
    }
}
  • 벡터의 값을 추출할 때 변수명 다음에 대괄호를 입력하고 그 안에 인덱스를 입력할 수도 있고, .get을 이용할 때는 소괄호를 이용하는데, 둘의 차이점은 대괄호를 이용할 때는 인덱스가 존재하지 않으면 패닉이 발생하나, get을 이용하면 None이 반환됩니다.
  • 위 코드를 실행하면 println!(“세 번째 값: {}”, third);은 실행되는데,
    println!(“세 번째 값: {}”, v[3]);에서 패닉이 발생하므로 이후 코드는 실행되지 않습니다.
  • 따라서, println!(“세 번째 값: {}”, v[3]);을 Ctrl + /를 눌러 주석처리한 다음 실행하면 뒷 부분 get으로 구한 값까지 표시됩니다.
  • get 다음에 index로 범위를 벗어난 5를 입력하고 실행하면 None이 되므로 else문이 실행되어 “없음”이 표시됩니다.

2. 문자열 (String)

가. 문자열의 정의

fn main() {
    let s1 = String::from("Hello");
    let s2 = "World!".to_string();

    println!("{s1}, {s2}");
}
  • String은 가변 문자열 타입으로 Heap에 저장되며,
  • 일반적인 프로그래밍 언어는 큰 따옴표안에 문자열을 입력하는데, Rust는 ① String::from 다음의 괄호안에 큰 따옴표를 이용해 문자열을 입력하거나, ②큰 따옴표 안에 문자열을 입력한 후 .to_string을 추가해서 입력합니다.
  • String::from없이 큰 따옴표 안에 문자열을 넣으면 String이 아니라 다음에 설명하는 문자열 슬라이스가 되어 성격이 다릅니다.
  • 위 코드를 실행하면

나. 문자열 연결

    let s2 = "World!".to_string();
    let s3 = s1 + ", " + &s2;
    println!("{s3}");
    // println!("{s1}");
  • 문자열 연결은 + 연산자를 사용합니다.
  • let s3 = s1 + “, ” + &s2;에서 s2는 빌림(&)을 사용해서 + 후에도 존재하나, s1은 + 후에 s3로 move되었으므로 더 이상 사용할 수 없습니다.

다. 슬라이스 (Slice)

슬라이스는 컬렉션의 일부를 참조하는 타입입니다.

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];

    println!("{}, {}", hello, world);
}
  • &s[a..b]는 a부터 b-1까지의 부분 문자열을 참조합니다. 범위 설정과 마찬가지로 b앞에 =을 추가하면 b가 포함됩니다.
  • 슬라이스는 원본이 유효한 동안만 유효합니다.

3. 문자열 리터럴(&str)과 String 비교

Rust에서 &str과 String은 모두 문자열을 나타내는 데 사용되지만, 그 역할과 특징이 다릅니다. &str은 문자열 슬라이스로, 고정 길이이고 값을 직접 소유하지 않습니다. 반면, String은 힙에 할당되어 동적으로 길이를 변경할 수 있으며 값을 소유합니다.

구분&str(문자열 리터럴 )String
저장프로그램 실행 시 정적 메모리(static memory)에 저장됩니다.힙(heap)에 할당되어 동적으로 크기가 변할 수 있습니다.
소유권소유하지 않고 참조만 합니다.데이터를 소유합니다.
가변성변경할 수 없습니다.문자열 내용을 추가, 수정, 삭제할 수 있습니다.
표현&str 또는 “문자열 리터럴” 형태로 표현됩니다. 
예1) let s = “hello world”;
예2) let s = String::from(“hello world”);
let hello = &s[0..5];
String::from(“문자열”) 또는 to_string()과 같은 메서드를 통해 생성합니다. 
예) let s = String::from(“hello world”);

간단하게 말하자면 “hello world”는 문자열 리터럴이고, type은 &str인데, String::from(“hello world”)은 type이 String입니다.
그런데, &str은 &str의 예2처럼 String을 참조하기도 합니다.

Rust의 String은 UTF-8로 인코딩됩니다.

📌 &str과 String 비교 예제 코드

fn main() {
    let s = String::from("hello world");
    let first_word = first_word(&s);
    print!("첫 번째 단어: {}", first_word);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
  • let s = String::from(“hello world”); : s란 변수에 hello world란 String을 저장합니다.
  • let first_word = first_word(&s); : 변수 s를 참조 형식으로 받아 first_word 함수의 인수로 전달하고, 반환 값을 다시 first_word란 변수에 저장합니다.
  • print!(“첫 번째 단어: {}”, first_word); : 위에서 구한 first_world를 화면에 출력합니다.
  • fn first_word(s: &str) -> &str { : first_word 함수는 인수 s를 &str(String 참조) 타입으로 받고, &str 형식으로 반환합니다.
  • let bytes = s.as_bytes(); : &str인 s를 string slice를 byte slice로 바꿉니다.
  • for (i, &b) in bytes.iter().enumerate() { : 위에서 구한 bytes를 하나씩 옮겨가면서 처리하는데(iter), 인덱스를 같이 반환하도록 enumerate를 같이 사용합니다.
  • if b == b’ ‘ { : b가 b’ ‘, 다시 말해 byte literal ‘ ‘와 같은 경우, 다시 말해 공백을 만나게 되면
  • return &s[0..i]; : 공백 전까지의 글자를 반환합니다.
  • &s[..] : &s가 공백 전까지의 글자이므로 이 글자 전체를 반환합니다. 세미콜론이 없으므로 표현식(expression)이고 반환값입니다.
  • 따라서, 위 코드를 실행하면 hello가 반환됩니다.

🧠 요약

타입설명
Vec<T>가변크기 배열, push, get, pop 지원
StringUTF-8로 인코딩된 힙 문자열
&str슬라이스 타입, 컬렉션 일부 참조
슬라이스소유권 없이 일부분만 안전하게 사용 가능

함수, if와 match 표현식

함수는 코드의 재사용과 구조화를 위한 기본 단위로서 매개변수와 반환값이 있을 수 있습니다. 또한 if와 match는 중요한 제어 흐름 도구로서, let과 결합하여 변수에 값을 대입하는 표현식도 됩니다. match의 경우 모든 경우를 망라하기 위해 _를 사용하는 것이 특이합니다.


🔧 함수 정의

fn main() {
greet("Rust");
}

fn greet(name: &str) {
println!("Hello, {}!", name);
}
  • fn 키워드로 함수를 정의합니다.
  • 함수는 매개변수와 반환 타입을 명시할 수 있습니다. 그러나, 매개변수나 반환 값이 있다면 반드시 형식(타입)을 지정해야 합니다. 위에서 main 함수에는 매개변수가 없고, greet에는 매개변수 name이 있으므로 형식을 &str로 지정했습니다.
  • &str은 문자열 슬라이스(문자열 참조)입니다.
  • main함수에서 greet 함수를 호출하고, greet 함수의 name 매개변수로 Rust를 전달하고 있으므로, 위 코드를 실행하면 아래 화면과 같이 Hello, Rust!라고 화면에 표시됩니다.
Run을 실행한 결과 Hello, Rust!가 화면에 출력된 화면입니다.

위 화면은 D:\rust-practice 폴더에서 cargo new day3를 실행한 다음 위 코드로 대체하고 실행한 화면입니다.

name 다음의 형식을 제거하고 실행(Run) 하면 아래와 같이 복잡한 에러 메시지가 표시되는데, name에 대한 형식을 지정하라는 의미입니다.

name 다음에 형식 지정이 없어서 지정하라는 에러 화면입니다.

위 화면에서 name 다음에 :을 입력하면 &str이 제시되므로 tab키를 눌러 제안을 수용하면 쉽게 코드를 완성할 수 있습니다.


🔁 반환값이 있는 함수

fn add(a: i32, b: i32) -> i32 {
a + b // 세미콜론 없음 → 반환값
}
  • 함수의 마지막 표현식(Expression)이 반환값입니다. 여기서는 a + b 입니다.
  • -> 다음의 i32가 반환 값의 형식을 지정하는 것입니다.
  • 세미콜론(;)이 붙으면 실행문(Statement)으로 값이 반환되지 않습니다.
  • return키워드를 사용할 수도 있지만, 마지막 줄에 return 없이 값을 놓는 것이 일반적입니다.
fn add(a: i32, b: i32) -> i32{
    return a + b
}
  • 위 함수는 출력문이 없으므로 화면에 어떠한 값도 출력하지 않습니다.
    값을 출력하려면 println! 매크로를 사용해야 합니다.
fn main() {
    let sum = add(5, 10);
    println!("5와 10의 합은: {sum}"); // 15
}

fn add(a: i32, b: i32) -> i32{
    a + b
}

위 코드는 main함수에서 add 함수에 5와 10을 전달하고 a + b의 값을 반환받아 값 15를 sum 변수에 대입한 후 println!를 이용해 “5와 10의 합은: 15″라고 화면에 출력하는 것입니다.


🔸 if 표현식

Rust에서 if는 표현식이며, 값으로 사용할 수 있습니다. 다시 말해 let 예약어를 이용해 변수에 if 표현식으로 결정되는 값을 변수에 대입할 수 있습니다.

fn main() {
let score = 85;
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else {
"C"
};
println!("성적: {grade}");
}
  • if는 블록의 결과를 반환합니다.
  • 각 분기의 결과는 같은 타입이어야 합니다.
  • 위 코드를 실행하면 score가 90보다 작고, 80보다 크므로 “성적: B”가 화면에 출력됩니다.

🔶 match 표현식

match는 패턴 매칭을 제공하는 강력한 제어문입니다.

fn main() {
let number = 3;

match number {
1 => println!("하나"),
2 => println!("둘"),
3 => println!("셋"),
_ => println!("기타"),
}
}
  • _는 위에 해당하지 않는 모든 경우를 의미하는 와일드카드입니다. ‘아무거나(any value)’라고 이해하면 편합니다.
  • 각 분기(arm)에는 =>로 실행 코드를 지정합니다(The => operator that separates the pattern and the code to run).
  • println!를 사용했는데도 ;을 붙이지 않는 점을 주의해야 합니다.
  • 위 코드를 실행하면 “셋”이라고 화면에 표시됩니다.
  • match는 반드시 모든 경우를 처리해야 합니다.
    다시 말해 _가 없으면 “i32 형식에 해당하는 수 중 1,2,3만 처리해서 i32의 최소값부터 0까지와 4부터 i32의 최대값은 커버하지 못했다”고 하는 non-exaustive patterns(총망라 하지 않은 패턴) 에러가 표시됩니다.

또한 아래와 같이 _를 맨 위에 놓으면 ‘모든 경우’가 되므로, number의 값이 1이거나 2 또는 3이더라도 “기타”를 출력하게 됩니다. 1,2,3이 아닌 4인 경우 “기타”를 출력하는 것은 너무나 당연합니다.

fn main() {
    let number = 3;

    match number {
        _ => println!("기타"),
        1 => println!("하나"),
        2 => println!("둘"),
        3 => println!("셋"),        
    }
}

📌 match를 값으로 사용하기

fn main() {
let day = 3;
let weekday = match day {
1 => "월요일",
2 => "화요일",
3 => "수요일",
_ => "기타",
};
println!("요일: {}", weekday);
}
  • match는 if와 마찬가지로 표현식이므로 변수에 바로 match 표현식의 결과 값을 할당할 수 있습니다.
  • 이전 예에서는 => 다음에 println!를 사용했는데, 여기서는 “화요일” 등의 반환값을 지정한 점이 다릅니다.
  • 위 코드를 실행하면 “요일: 수요일”이 출력됩니다.


🧠 요약

  • 함수는 fn으로 정의하며, 매개변수와 반환 타입 지정 가능
  • if와 match는 모두 표현식으로, 값을 반환할 수 있음
  • match는 매우 강력한 패턴 매칭 도구이며, 모든 경우를 반드시 다뤄야 함