[RUST] 8. 범용 컬렉션

    시작하며

    이번 장에서는 자주 사용되는 벡터, 문자열, 해시맵 세 가지 컬렉션을 알아보자

    벡터에 일련의 값 저장하기

    벡터는 하나 이상의 값을 하나의 데이터 구조에 담을수 있으며 모든 값은 메모리상에 연속으로 저장된다. 벡터는 같은 타입의 값만 저장할 수 있다.

    새로운 벡터 생성하기

    새로운 빈 벡터를 생성하려면 Vec::new 함수를 호출하면 된다.

    let v: Vec<i32> = Vec::new();
    
    //값을 포함하는 벡터 생성 Vec<i32> 타입으로 유추
    let v2 = vec![1,2,3];

    벡터 수정

    벡터를 생성하고 값을 추가하려면 push 메서드를 사용한다.

    let mut v = Vec::new();
    
    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);

    벡터 해제

    벡터 역시 범위를 벗어날 때 drop 메서드가 호출된다.

    {
        let v = vec![1,2,3];
    }   // 변수 v가 범위를 벗어나면 drop 메서드가 호출되어 메모리가 해제된다

    벡터로부터 값 읽기

    인덱스 문법(indexing syntax)과 get 메서드로 값을 읽어올 수 있다.

    let v = vec![1,2,3,4,5];
    
    let third: &i32 = &v[2];
    println!("세 번째 원소: {}", third);
    
    match v.get(2) {
        Some(third) => println!("세 번째 원소: {}", third),
        None => println!("세 번째 원소가 없습니다."),
    }

    유효하지 않은 인덱스 값에 접근하게 되면 패닉(panic)이 발생한다. 존재하지 않은 값에 접근 하여 프로그램이 충돌을 일으켜 강제 종료된다.

    let v = vec![1,2,3,4,5];
    
    let does_not_exist = &v[100];
    let does_net_exist = &v.get(100);

    벡터에도 소유권과 대여 규칙이 적용된다. 그래서 같은 범위 내의 가변 참조와 불변 참조를 동시에 가질 수 없다.

    let mut v = vec![1,2,3,4,5];
    
    let first = &v[0];
    
    v.push(6);

    벡터에 저장된 값을 순회하기

    인덱스를 이용해 하나씩 접근하는 것 보다 순회(iterate)하여 접근하는 편이 좋다. 다음은 for 루프를 이용해 값의 불변 참조를 얻어와 출력하는 코드이다.

    let v = vec![1,2,3,4,5];
    for i in &v {
        println!("{}", i);
    }

    또한 가변 벡터의 가변 참조를 얻어와 값 변경도 가능하다.

    let mut v = vec![1,2,3,4,5];
    for i in &mut v {
        *i += 50;
    }

    열거자를 이용해 여러 타입 저장하기

    열거자를 이용해 하나의 벡터에 다른 타입의 값을 저장할 수도 있다.

    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }
    
    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("블루")),
        SpreadsheetCell::Float(10.12),
    ];

    String 타입에 UTF-8 형식의 텍스트 저장하기

    문자열이란 무엇일까?

    러스트는 언어의 명세 안에서 오직 한가지 문자열 타입인 str(문자열 슬라이스)만을 지원한다. String 타입은 러스트의 표준 라이브러리가 제공하는 타입이다. 러스트의 문자열이란 대부분 String 타입과 str(문자열 슬라이스)타입을 동시에 의미한다.

    새 문자열 생성하기

    String 타입은 Vec 타입이 지원하는 대부분의 작업을 지원한다. new 함수, to_string 메서드, String::from 함수 등으로 생성 가능하다

    let mut s = String::new();
    
    let data = "문자열초깃값";
    
    let s = data.to_string();
    
    let s = "문자열초깃값".to_string();
    let s = String::from("문자열초깃값");

    문자열 수정하기

    push_str과 push 메서드를 이용해 문자열 덧붙이기

    let mut s = String::from("foo");
    s.push_str("bar");

    변수 s는 'foobar' 라는 문자열이 저장된다. push_str 메서드가 문자열 슬라이스를 이용하는 이유는 매개변수의 소유권을 가질 필요가 없기 때문이다.

    String 타입에 변수의 데이터를 덧붙인 후 해당 변수를 다시 사용
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2: {}", s2);
    push 메서드를 이용해 String 값에 하나의 문자 추가
    let s = String::from("lo");
    s.push('l');

    변수 s는 'lol' 값이 저장된다.

    + 연산자나 format! 매크로를 이용한 문자열 연결

    + 연산자를 이용해 두 개의 String 값을 새로운 String 값으로 연결
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2;  //이렇게 하면 변수 s1은 메모리가 해제되어 더 이상 사용할 수 없다.

    문자열을 여러 개 결합할 때에는 + 연산자보다는 format! 매크로가 더 적합하다. println! 매크로와 같은 방식으로 동작하지만 결과 출력이 아닌 결합된 String 값을 리턴한다.

    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");
    
    let splus = s1 + "-" + &s2 + "-" + &s3;
    let sformat = format!("{}-{}-{}", s1, s2, s3);

    문자열의 인덱스

    다른 언어에서는 문자열의 개별 문자를 인덱스를 이용해 접근할수 있지만 러스트에서는 에러가 발생한다.

    let s1 = String::from("hello");
    let h = s1[0];

    문자열의 내부

    String은 Vec 타입을 한 번 감싼 타입이다.

    let len1 = String::from("Hola").len();          //4
    let len2 = String::from("안녕하세요").len();    //15

    문자열을 UTF-8 형식으로 인코딩하면 문자열 안 유니코드의 스칼라값이 3byte 공간을 차지하기 때문이다. 그래서 문자열의 바이트에 인덱스로 접근하면 올바른 유니코드 스칼라값을 가져올 수 없을 수 있다.

    바이트와 스칼라값, 그리고 그래핌 클러스터

    UTF-8 에 대해 러스트 관점에서 보자면 문자열은 크게 바이트(bytes), 스칼라값(scalar value), 그래핌 클러스(grapheme clusters)등 세가지로 구분한다. 한글 "안녕하세요"는 다음과 같이 u8 값들의 벡터에 저장되고 이어 유니코드 스칼라값으로, 그래핌 클러스터로 표현할 수 있다.

    [236, 149, 136, 235, 133, 149, 237, 149, 152, 236, 132, 184, 236, 154, 148]
    ['안', '녕', '하', '세', '요']
    ["안", "녕", "하", "세", "요"]

    러스트가 String 타입에서 인덱스 사용을 지원하지 않는 마지막 이유는 인덱스 처리에는 항상 일정한 시간이 소요되어야 하지만 일정한 성능을 보장할 수 없어서이다.

    문자열 슬라이스 하기

    인덱스를 이용해 문자열 슬라이스를 생성하고 싶다면 [] 기호에 하나의 숫자를 인덱스로 전달하는 대신 []를 이용하여 슬라이스에 저장할 특정 바이트의 범위를 지정해야 한다.

    let hello = "안녕하세요";
    let s = &hello[0..3];

    하지만 &hello[0..1] 과 같이 코드를 작성하기 되면 벡터에 유효하지 않은 인덱스 접근과 같이 런타임에 패닉이 발생한다.

    문자열을 순회하는 메서드

    개별 유니코드 스칼라값을 조작해야 한다면 가장 좋은 방법은 chars 메서드를 사용하는 방법이다. bytes 메서드는 문자열의 각 바이트를 리턴할 필요가 있을때 활용한다.

    for c in "안녕하세요".chars() {
        println!("{}", c);
    }
    
    for b in "안녕하세요".bytes() {
        println!("{}", b);
    }

    키와 값을 저장하는 해시 맵

    HashMap<K,V> 타입은 K타입의 키에 V 타입의 값을 매핑하여 저장한다.

    새로운 해시 맵 생성하기

    new 함수를 이용하여 빈 해시 맵을 생성하고, insert 함수를 이용해 새로운 키와 값을 추가할 수 있다.

    use std::collections::HashMap;
    
    let mut scores = HashMap::new();
    
    scores.insert(String::from("블루"), 10);
    scores.insert(String::from("옐로"), 50);

    해시 맵을 생성하는 다른 방법은 키와 값을 가지고 있는 튜플의 벡터에 대해 collect 메서드를 호출하는 방법이다.

    use std::collections::HashMap;
    
    let teams = vec![String::from("블루"), String::from("옐로")];
    let initial_scores = vec![10, 50];
    
    let scroes: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();

    collect 메서드는 여러 가지 데이터 구조를 생성할수 있고, 어떤 타입을 생성할 것인지를 명시하기 위해 HashMap<_, _> 타입 애노테이션이 필요하다.

    해시 맵과 소유권

    use std::collections::HashMap;
    
    let field_name = String::from("Favorite color");
    let field_value = String::from("블루");
    
    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // field_name과 field_value 변수는 여기서 부터 유효하지 않다.
    // 이 값들을 사용하려고 하면 컴파일러가 에러를 발생한다.

    field_name과 field_value 변수는 insert 메서드를 통해 해시 맵으로 이동한 후에는 사용할 수 없다. 소유권이 이동하기 때문이다.

    해시 맵의 값에 접근하기

    해시 맵의 값을 읽으러면 get 메서드에 키를 전달하면 된다.

    use std::collections::HashMap;
    
    let mut scores = HashMap::new();
    
    scores.insert(String::from("블루"), 10);
    scores.insert(String::from("옐로"), 50);
    
    let team_name = String::from("블루");
    let score = scores.get(&team_name);
    
    for (key, value) in &scores 
    {
        println!("{}: {}", key, value);
    }

    score 변수는 블루팀에 연결된 값을 갖게 되며 그 결과 타입은 Some(&10) 이다. get 메서드가 Option<&V> 타입을 리턴하기 때문이다. 그리고 for 루프를 이용하면 벡터와 마찬가지로 해시 맵에서 키와 값의 쌍을 순회할 수 있다.

    해시 맵 수정하기

    값 덮어쓰기

    해시 맵에 키와 값을 추가한 후 같은 키에 다른 값을 추가하면 값이 교체된다.

    use std::collections::HashMap;
    
    let mut scores = HashMap::new();
    
    scores.insert(String::from("블루"), 10);
    scores.insert(String::from("블루"), 25);
    
    println!("{:?}", scores);   //{"블루":25} 출력

    키에 값이 할당되어 있지 않을 때만 추가하기

    use std::collections::HashMap;
    
    let mut scores = HashMap::new();
    
    scores.insert(String::from("블루"), 10);
    scores.entry(String::from("옐로")).or_insert(50);
    scores.entry(String::from("블루")).or_insert(50);
    
    println!("{:?}", scores);   //{"옐로":50, "블루":10} 출력

    entry 메서드의 리턴값을 값이 존재하는지 알려주는 Entry 열거자다. Entry 열거자의 or_insert 메서드는 키가 존재하면 그 키에 연결된 값에 대한 가변 참조를 리턴하고, 키가 존재하지 않으면 매개변수로 전달한 키에 새로운 값을 추가한 후 이 새 값에 대한 가변 참조를 리턴한다.

    기존 값에 따라 값 수정하기

    use std::collections::HashMap;
    
    let text = "hello world wonderful world";
    
    let mut map = HashMap::new();
    
    for word in text.split_whitespace() 
    {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }
    
    println!("{:?}", map);  //{"world":2, "hello":1, "wonderful":1} 출력

    or_insert 메서드는 키에 할당된 값에 대한 가변 참조(&mut V)를 리턴한다. 이 변수에 새 값을 할당하려면 애스터리스트(*) 기호를 이용행 count 변수를 역참조 해야한다.

    요약

    • 벡터(vector): 연속된 일련의 값을 저장한다.
    • 문자열(string): 문자(character)의 컬렉션
    • 해시 맵(hash map): 특정 키에 값을 연결할 때 사용한다.
    반응형

    댓글

    Designed by JB FACTORY