Victoree's Blog

[6] 표준 라이브러리 [Option, Result, Box, Cons List, Rc<T>] 본문

Rust

[6] 표준 라이브러리 [Option, Result, Box, Cons List, Rc<T>]

victoree 2023. 3. 2. 01:46
728x90

표준 라이브러리

  • Option과 Result : 어떤 값이 있거나 없거나 하는 경우, 그리고 오류 처리에 사용합니다.
  • String: 기본적인 문자열 타입으로, 문자열 데이터를 소유하는 경우에 사용합니다.
  • Vec: 가변 크기의 표준 벡터 타입입니다.
  • HashMap: 해시 알고리즘을 따로 지정할 수도 있는 해시맵 타입입니다.
  • Box: 힙 데이터에 대한 소유 포인터입니다.
  • Rc: 힙에 할당된 데이터에 대한 참조 카운팅 공유 포인터입니다.

Option 과 Result

Result 타입

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result 타입은 열거형으로 enums라고 부르기도 한다. Result의 variants는 OkErr이다. Ok는 처리가 성공했음을 나타내고, 성공적으로 생성된 결과를 가지고 있는다. Err는 처리가 실패했음과 그 이유에 대한 정보를 가지고 있다.

Result 역시 다른 타입들처럼 메소드들을 가지고 있다. expect메소드는 Result 인스턴스가 Err인 경우, 프로그램의 작동을 멈추게 하고 expect에 인자로 넘긴 메세지를 출력하도록 한다.
Result가 Ok라면, expect는 Ok의 결과값을 돌려준다.
에러인 경우, expect를 호출하지 않는다면 컴파일은 되지만 경고가 나타난다.

let f: u32 = File::open("hello.txt"); // File::open()의 반환타입은 Result<T,E>이다.

let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("There was a problem opening the file: {:?}", error)
        },
    }; // 패턴 매칭을 이용하여 위와 같이 에러 핸들링을 할 수 있다.

let f = File::open("hello.txt").unwrap(); // unwrap함수를 호출하는 경우, Err variant 라면 panic! 매크로를 호출한다.
let f = File::open("hello.txt").expect("Failed to open hello.txt"); // expect 함수 역시 panic! 매크로를 호출하지만, panic!에서 사용되는 메세지보다 우리가 더 이해하기 쉬운 파라미터로 넘긴 에러 메세지로 출력되어 에러 확인이 더 용이하다.

? == 에러 전파를 위한 숏컷

실패할 수도 있는 코드를 호출하는 함수를 작성할 때 이를 함수내에서 에러를 처리하는 대신, 에러를 호출하는 코드 쪽으로 반환하여 그 쪽에서 어떻게 할지 결정하도록 할 수 있다.
Result 값 뒤에 ? 구문을 적어 Ok라면 Ok 내의 값을 얻을 수 있고, Err라면 전체 함수에게 Err내의 값이 반환된다 .

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

스마트 포인터

스마트 포인터는 포인터처럼 작동하나, 추가적인 메타 데이터와 능력들도 가지고 있는 데이터 구조이다.
예를 들어, 참조 카운팅 스마트 포인터 타입 은 소유자의 수를 추적하고, 더이상 소유자가 없으면 데이터를 정리하는 방식으로 어떤 데이터에 대해 여러 소유자를 만들 수 있게 해준다.
소유권의 빌림 개념이 있는 러스트에서 참조자와 스마트 포인터간의 차이점은 참조자는 데이터를 오직 빌리기만 하는 포인터라는 점이다.
StringVec<T>와 같은 스마트 포인터들은 이들이 얼마간의 메모리를 소유하고 우리가 이를 다루도록 허용해준다. 그들의 용량같은 메타데이터와 이 데이터가 유효한 UTF-8 과같은 타입임을 보장하는 보장 데이터 역시 가지고 있다.
스마트 포인터는 DerefDrop 트레잇을 구현하는데, Deref 트레잇은 스마트 포인터 구조체의 인스턴스가 참조자처럼 동작하도록 하여 참조자나 스마트 포인터 둘 중 하나와 함께 작동하는 코드를 작성하게 해준다.
Drop 트레잇은 스마트 포인터의 인스턴스가 스코프 밖으로 벗어났을 때 실행되는 코드를 커스터마이징 하게 해준다.

Box

데이터를 힙에 저장하고 스택에는 이를 가리키는 포인터를 갖는다. 박스가 해제될 때는 힙과 스택에 있는 데이터가 모두 해제된다.
박스는 다음과 같은 상황에서 사용된다.

  • 컴파일 타임에 크기를 알 수 없는 타입을 갖고있고, 정확한 사이즈를 알 필요가 있는 맥락안에서 해당 타입의 값을 이용하고 싶을 때
  • 커다란 데이터를 가지고 있고, 소유권을 옮기고 싶으나 그렇게 하면 데이터가 복사되지 않을 것을 보장하기 원할 때
  • 어떤 값을 소유하고 이 값의 구체화된 타입을 알고 있기 보다, 특정 트레잇을 구현한 타입이라는 점만 신경쓰고 싶을 때
  1. 컴파일 타입 시 크기를 알 수 없는 타입 중 하나가 재귀적 타입이다.
    재귀적 타입의 예제로 cons list를 알아보자.

Cons List

cons 함수는 두개의 인자를 받아 새로운 한 쌍을 생성하는데, 이 인자는 보통 단일 값과 또 다른 쌍이다. 이 쌍들을 담고 있는 쌍들이 리스트를 형성한다.
“to cons x onto y”는 약식으로 요소 x를 새로운 컨테이너에 집어넣고, 그다음 컨테이너 y를 넣는 식으로 새로운 컨테이너 인스턴스를 생성하는 것을 의미한다.
cons list 내의 아이템은 두개의 요소를 담고 있다. 현재 아이템의 값과 다음 아이템이다.
리스트의 마지막 아이템은 Nil 이라 불리는 값을 담고 있는다.
cons list는 cons 함수를 재귀적으로 호출함으로 만들어진다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Message의 값을 할당하기 위해 얼마나 많은 공간이 필요한지 결정하기 위해, 러스트는 어떤 variant가 가장 많은 공간을 필요로하는지 알기 위해 각각의 variant를 본다. (어차피 단 하나의 값으로만 할당되기 때문에, 가장 큰 variant의 값을 보면 된다)

let list = Cons(1, Cons(2, Cons(3, Nil))); // 비재귀적인 variant 값인데, 무한한 사이즈의 List를 갖기 때문에 아래와 같은 버그가 발생한다.
//help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable

아래와 같은 상황에서 Box를 이용해 정의한다면, 이는 포인터이기 때문에 위 버그를 해결할 수 있다.

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

Rc == 참조 카운팅 스마트 포인터

복수 소유권을 가능하게 하기 위해, 러스트는 Rc<T>라 불리는 타입을 가지고 있다. 이는 어떤 값이 계속 사용되는지 혹은 그렇지 않은지를 알기위해 해당 값에 대한 참조자의 갯수를 추적하는 것이다. 참조자가 0개라면 그 값은 어떤 참조자도 무효화하지 않고 메모리 정리될 수 있다.

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

각 Cons variant는 어떤 값과 List를 가리키는 Rc를 갖게 된다. b를 만들때, a의 소유권을 얻는 대신 a를 가지고 있는 Rc를 클론할 텐데 이는 참조자의 갯수를 하나에서 둘로 증가시키고 a와 b가 Rc안에 있는 값을 공유하게 한다.
c를 만들때도 참조자의 갯수를 둘에서 셋으로 늘린다. Rc::clone을 호출하는 때마다 Rc가 가지고 있는 데이터에 대한 카운트는 증가할 것이고 그 데이터의 참조자가 0개가 되지 않으면 메모리는 정리되지 않는다.
불변 참조자를 통하여, Rc는 읽기 전용으로 우리 프로그램의 여러 부분 사이에서 데이터를 공유하도록 허용해줍니다. Rc내부의 데이터를 변경해야 하는 경우, 데이터를 Cell 또는 RefCell로 래핑해야한다. 만일 Rc가 또한 복수개의 가변 참조자도 갖는 것을 허용한다면, 데이터 레이스 및 데이터 불일치를 야기할 수 있다. 내부 가변성 패턴과 불변성 제약이 함께 동작하기 위해 Rc와 같이 결합하여 사용할 수 있는 RefCell 타입은 나중에.. 알아보자. :)
멀티 스레드인 경우 Arc 를 이용하고, 참조 카운트를 확인하려면 Rc::strong_count를 호출하여 확인할 수 있다.

728x90

'Rust' 카테고리의 다른 글

[7] 모듈과 파일시스템  (0) 2023.03.02
[5] 흐름제어 [for, loop, while ...]  (0) 2023.03.02
[Code] Struct & Enum 연습문제  (0) 2023.02.23
[4] 구조체(Struct)와 열거형(Enum)  (0) 2023.02.23
[3] 라이프 타임과 참조자 유효화  (0) 2023.02.16
Comments