Programming/Rust

러스트 컬렉션 (collection)

2mukee 2025. 9. 2. 21:06
320x100
320x100

컬렉션 (collection)

러스트의 표준 라이브러리에서 지원하는 데이터 타입으로, 단일한 특정 값을 나타내는 다른 데이터 타입과 달리 다수의 값을 담을 수 있음

컬렉션들이 가리키고 있는 데이터들은 힙에 저장되기 때문에 데이터의 양이 컴파일 타임에 결정되지 않아도 되며 프로그램 실행 중에 변동될 수 있음

각 컬렉션의 종류는 서로 다른 크기와 비용을 가지고 있으며 현재 상황에 따라 적절한 컬렉션을 고르는 것이 중요

 

- 벡터 (vector)

여러 개의 값을 서러 붙어 있게 저장할 수 있도록 해줌

 

- 문자열 (string)

문자(character)의 모음

 

- 해시 맵 (hash map)

어떤 값을 특정한 키와 연관지어주도록 해줌. 맵(map)이라 일컫는 좀 더 일반적인 데이터 구조의 특정한 구현 형태

 

- 기타 컬렉션

https://doc.rust-kr.org/ch08-00-common-collections.html

 

일반적인 컬렉션 - The Rust Programming Language

러스트의 표준 라이브러리에는 컬렉션 (collection) 이라 불리는 매우 유용한 데이터 구조들이 여러 개 포함되어 있습니다. 대부분의 다른 데이터 타입은 단일한 특정 값을 나타내지만, 컬렉션은

doc.rust-kr.org

 

 

 

 

 

벡터에 여러 값 목록 저장하기

벡터를 사용하면 메모리에서 모든 값을 서로 이웃하도록 배치하는 단일 데이터 구조에 하나 이상의 값을 저장할 수 있음

같은 타입의 값만 저장할 수 있으며,

파일 내의 텍스트 라인들이나 장바구니 품목 가격 같은 아이템 목록을 저장하는 상황일 때 유용

 

fn main() {
    let v1: Vec<i32> = Vec::new(); // 벡터에 저장할 값의 타입은 제네릭을 이용하여 표현하나, 표준 라이브러리에서 제공하는 Vec 타입에는 어떤 값이든 저장할 수 있음
    let v2 = vec![1, 2, 3]; // 값을 저장하고 있는 새로운 벡터 생성
    
    // 벡터에 값 추가
    let mut v3 = Vec::new(); // Vec(i32) 타입 명시를 붙이지 않아도 되는 이유는 이후에 push하는 숫자가 i32 타입인 점을 통해 러스트가 타입을 추론하기 때문
    v3.push(1);
    v3.push(2);
    
    // 벡터 요소 읽기
    let second: &i32 = &v3[1]; // 2. &와 []을 사용하면 인덱스 값에 위치한 요소의 참조자를 얻게됨
    let second: Option<&i32> = v.get(1); // 2. get 함수에 인덱스를 매개변수로 넘기면 match를 통해 처리할 수 있는 Option<&T>를 얻게됨
    
    match second {
        Some(second) => println!("{second}");
        None => println!("no second element");
    }
}

 

&와 []을 통해 인덱스 값에 위치한 요소의 참조자를 얻을 때 벡터의 끝을 넘어서는 요소에 접근할 경우 패닉을 일으키면서 프로그램이 죽게됨

반면 get 함수를 통해 Option<&T>를 얻을 때는 인덱스를 벗어나도 패닉 없이 None이 반환됨

대신 Some(&element) 혹은 None에 대해 처리하는 로직이 있어야함

 

- 아이템의 참조자를 가지고 있는 상태에서 벡터에 새로운 요소 추가

let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6); // 이미 요소에 대한 참조자가 있어 메모리 이동이 불가하기 때문에 오류 발생

 

벡터는 모든 요소가 붙어서 메모리에 저장되기 때문에

새로운 요소를 벡터에 추가할 때 현재 벡터 메모리 위칭[ 새로운 요소를 추가할 공간이 없다면

다른 넉넉한 곳에 메모리를 새로 할당하고 기존 요소를 새로 할당한 공간에 복사

이 때문에 기존 요소의 참조자는 해제된 메모리를 가리키기 떄문에 대여 규칙에 어긋남

 

참고로 벡터가 버려질때는 벡터의 내용물도 전부 버려지며 메모리에 정리됨

대여 검사기는 벡터의 내용물에 대한 참조자의 사용이 해당 벡터가 유효할 때만 발생했는지를 확인

 

 

 

 

 

벡터 값에 대해 반복하기

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
        *i += 50; // 역참조 연산자를 통해 i의 값을 얻은 뒤 += 연산 수행
    }
}

 

벡터에 대한 반복 처리는 불변이든 가변이든 상관없이 대여 검사 규칙에 의해 안전

for 루프 내에서 아이템을 추가하거나 지우려고 할 시 컴파일 오류가 발생함

벡터가 동시다발적으로 수정되는 것을 막기위함

 

 

 

 

 

열거형을 이용해 벡터에 여러타입 저장하기

벡터는 같은 타입의 값만 저장할 수 있지만, 열거형을 정의하면 다양한 타입의 값을 저장할 수 있음

이는 열거형의 배리언트는 같은 열거형 타입 내에 정의가 되기 때문

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

 

러스트가 컴파일 타임에 백터 내 저장될 타입을 알아야하는 이유는 각 요소를 저장하기 위해 얼마만큼의 힙 메모리가 필요한지 알아야하고, 벡터가 담을 수 있는 타입을 명시적으로 보여줘야하기 때문

 

- 표준 라이브러리 Vec에 정의된 유용한 메서드 목록들

https://doc.rust-lang.org/std/vec/struct.Vec.html

 

Vec in std::vec - Rust

Creates a Vec directly from a NonNull pointer, a length, a capacity, and an allocator. §Safety This is highly unsafe, due to the number of invariants that aren’t checked: ptr must be currently allocated via the given allocator alloc. T needs to have the

doc.rust-lang.org

 

 

 

 

 

문자열에 UTF-8 텍스트 저장하기

러스트는 다른 언어에 비해 문자열이 복잡한 자료구조임

러스트에서 문자열은 컬렉션으로 구현되어 있고, 이 바이트들을 텍스트로 통역할 때 유용한 기능을 다양한 메서드를 통해 제공함

러스티안들이 문자열에 대해 이야기 할 때는 보통 String이나 문자열 슬라이스 &str 타입 중 하나를 얘기하는 것

 

- str

러스트 언어 핵심 기능에서 유일하게 제공하는 문자열 타입

&str은 UTF-8로 인코딩되어 다른 어딘가에 저장된 문자열 데이터의 참조자

 

- String

러스트 언어 핵심 기능이 아닌 표준 라이브러리를 통해 제공되는 UTF-8로 인코딩된 문자열 타입

커질 수 있고 가변적이며, 소유권을 가지고 있음

 

 

 

 

 

문자열 생성하기

VEc<T>에서 쓸 수 있는 연산 다수가 String에서도 사용할 수 있는데, 이는 String이 실제로 바이트 벡터에 더하여 몇 가지 보장, 제한, 기능들을 추가한 wrapper로 구현되어 있기 때문

fn main() {
    let data = "initial";
    let s = data.to_string(); // to_string 메서드는 리터럴에서도 바로 작동 가능 "initial".to_string();
    
    // 문자열 업데이트
    let mut s = String::from("initial");
    s.push_str("bar"); // push_str은 매개변수의 소유권을 가져올 필요가 없어 문자열 슬라이스를 매개변수로 사용

    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // s1은 더하기 연산 이후 더이상 유효하지 않게됨 (add 메서드를 사용하면서 self 매개변수로 가져가기 때문)
    // add 메서드는 fn add(self, s: &str) -> String { 으로 구현되어 있는데,
    // 러스트에서 &String 인수를 &str로 강제될 수 있어서 &s2를 더할 수 있음
    // 이를 역참조 강제라고 하며, add 내에서 사용되는 &s2를 &s2[..]로 변환함
    // 즉, s1의 소유권을 가져다가 s2의 내용물의 복사본을 추가한 다음 결과물의 소유권을 반환 (뽁사를 하는 개념이 아님)

    let s4 = String::from("tic");
    let s5 = String::from("tac");
    let s6 = String::from("toe");
    let ss = format!("{s1}-{s2}-{s3}"); // s1 + "-" + &s2 "-" + &s3; 와 동일
}

 

 

 

 

 

 

문자열 인덱싱

fn main() {
    let s1 = String::from("Hello");
    let h = s1[0]; // 러스트 문자열은 인덱싱을 지원하지 않기 때문에 오류 발생
}

 

러스트에서는 문자열에 대한 인덱싱을 지원하지 않음

문자열 인덱싱의 반환 타입이 바이트 값인지, 캐릭터인지, 문자소 클러스터인지, 문자열 슬라이스인지 명확하지 않기 때문

 

 

 

 

 

 

문자열 슬라이싱

fn main() {
    let hello = "Здравствуйте";
    let s = &hello[0..4]; // Зд, s는 첫 4바이트를 담고있는 &str이 됨
}

 

러스트에서는 []에 숫자 하나를 사용하는 인덱싱이 아닌 []와 범위를 사용하여 특정 바이트들이 담고있는 문자열 슬라이스를 만들 수 있음

하지만 &hello[0..1] 처럼 문자 바이트의 일부를 슬라이스를 하여 얻으려고 하면 패닉이 발생

즉, 문자열에 대한 정확한 바이트 수로 슬라이싱을 해야함

 

 

 

 

 

문자열 반복

fn main() {
    for c in "Зд".chars() {
        println!("{c}"); // З д
    }
    
    for b in "Зд".bytes() {
        println!("{b}"); // 208 151 208 180. 유효한 유니코드 스칼라 값이 하나 이상의 바이트로 구성될지도 모른다는 점을 기억해야함
    }
}

 

문자열 조각에 대한 연산을 하는 가장 좋은 방법은 명시적으로 문자를 원하는 것인지 아니면 바이트를 원하는 것인지 지정하는 것

데바나가리 문서와 같은 문자열로 부터 문자소 클러스터를 얻는 방법은 복잡해서 이 기능은 표준 라이브러리에서 제공하지 않고 crates.io에서 크레이트로 제공

 

러스트에서는 문자열에대한 복잡한 상황을 처리하기 위한 String 및 &str 기반의 다양한 기능을 제공 (contains, replace 등)

 

 

 

 

 

해시 맵

HashMap<K, V> 타입은 K 타입의 키와 V 타입의 값에 대해 해시 함수(hashing function)를 사용하여 매핑한 것을 저장

이 해시 함수는 키와 값을 메모리 어디에 저장할지 결정

다른 언어에서는 해시, 맵, 오브젝트, 애시 테이블, associative 배열 등의 이름으로 이러한 데이터 구조를 지원

 

해시 맵은 벡터에서처럼 인덱스를 이용하는 것이 아니라 임의의 타입으로 된 키를 이용하여 데이터를 찾고 싶을 때 유용

 

해시맵은 세 가지 일반적인 컬렉션 중 가장 적게 사용되기 때문에 프렐루드의 자동으로 가져오는 기능에 포함되어 있지 않으며 표준 라이브러리로 부터 덜 지원을 받음 (해시 맵을 생성하는 기본 제공 매크로도 없음)

use std::collections::HashMap; // 해시 맵을 이용하려면 표준 라이브러리 컬렉션에서 HashMap을 use로 가져와야함

fn main() {
    let mut scores = HashMap::new();
    
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    
    let team_name = String::from("Blue");
    let score = scores.get(&team_name).copied().unwrap_or(0); // Blue에 해당하는 10
    // get 메서드는 Option(&V)를 반환
    // 해시 맵에 해당 키에 대한 값이 없으면 get은 None을 반환
    // copied > Option<i32>를 얻어오게 해줌
    // unwrap_or > scores가 해당 키에 대한 아이템을 가지고 있지 않은 경우 score에 0을 설정하도록 처리
}
use std::collections::HashMap; // 해시 맵을 이용하려면 표준 라이브러리 컬렉션에서 HashMap을 use로 가져와야함

fn main() {
    let mut scores = HashMap::new();
    
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    
    // 벡터와 유사한 방식으로 for 루프로 해시 맵 내 키/값 쌍에 대한 반복 작업 가능
    for (key, value) in &scores {
       println!("{key}: {value}");
    }
}

 

 

 

 

 

해시맵과 소유권

i32 처럼 Copy 트레이트를 구현한 타입의 값은 해시 맵 안으로 복사됨

String 처럼 소유권이 있는 값의 경우 값들이 이동되어 해시 맵이 그 값의 소유자가 됨

use std::collections::HashMap; // 해시 맵을 이용하려면 표준 라이브러리 컬렉션에서 HashMap을 use로 가져와야함

fn main() {
    let field_name = String::from("Favorate Color");
    let field_value = String::from("Blue");
    
    let mut map = HashMap::new();
    map.insert(field_name, field_value); // field_name과 field_value는 이 시점부터 유효하지 않아짐
    // 해시 맵에 값들의 참조자를 삽입하면 이 값들은 해시맵으로 이동되지 않음
    // 하지만 참조자가 가리키고 있는 값은 해시 맵이 유효할 때까지 계속 유효해야함
}

 

 

 

 

 

 

해시 맵 업데이트

해시 맵은 키와 값 쌍의 개수는 늘어날 수 있지만 각각의 유일한 키는 연관된 값을 하나만 가질 수 있음

 

- 값 덮어쓰기

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    
    socres.insert(String::from("Blue"), 10);
    socres.insert(String::from("Blue"), 25); // 덮어 쓰기됨
    
    println!("{::?}", scores); // {"Blue": 25}
}

 

- 키가 없을 때만 키와 값 추가

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    socres.insert(String::from("Blue"), 10);
    
    // or_insert > 해당 키가 존재할 경우 Entry 키에 대한 연관된 값을 반환, 
    // 그렇지 않은 경우 매개변수로 제공된 값을 해당 키에 대한 새 값으로 삽입하고 
    // 수정된 Entry에 대한 값을 반환
    socres.entry(String::from("Yellow")).or_insert(50);
    socres.entry(String::from("Blue")).or_insert(50);
    
    println!("{::?}", scores); // {"Yellow": 50, "Blue": 10}
}

 

- 예전 값에 기초하여 업데이트

use std::collections::HashMap;

fn main() {
    let text = "hello world wonderfule world";
    
    let mut map = HashMap::new();
    
    // split_whitespce > text의 값을 공백 문자로 나눈 서브 슬라이스에 대한 반복자를 반환
    // or_insert > 실제로는 해당 키에 대한 값의 가변 참조저 &mmut V를 반환
    // count 변수에 가변 참조자를 저장 (값 할당을 위해 *을 사용하여 count를 역참조)
    // 가변 참조나느 for 루프 끝에서 스코프 밖으로 벗어나기 때문에 안전하게 처리됨
    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }
    
    println!("{::?}", map); // {"world": 2, "hello": 1, "wonderful": 1}
}

 

 

 

 

 

해시함수

기본적으로 HaspMap은 해시 테이블과 관련된 DDOS 공격 저항을 위한 SipHash라는 해시 함수를 사용

이는 가장 빠른 해시 알고리즘은 아니지만 보안이 더 강함

만일 기본 해시 함수가 목적에 사용되기에 느리다면 해셔를 다른 함수로 바꿀 수 있음

crated.io에는 수많은 범용적인 해시 알고리즘을 구현한 해셔를 제공하는 공유 라이브러리가 있음

 

- 해셔

BuildHasher 트레이트를 구현한 타입

300x250
728x90