Rust Chapter9 : Struct

    들어가기 전

    이 글은 프로그래밍 러스트 2판을 공부하며 작성한 글입니다. 

     


    9. 스트럭트 (Struct)

    • Struct 자바, 파이썬에서 제공하는 클래스와 거의 유사한 개념이다.
    • Struct는 크게 다음 세 종류가 존재한다.
      • 필드형 Struct : 주로 많이 사용함.
      • 튜플형 Struct : 타입 검사를 좀 더 Strict 하게 하기 위해 NewType을 생성할 때 주로 사용.
      • 유닛형 Struct : 주로 Trait과 함께 사용할 때 유용하다고 한다.

     


    필드형 Struct

    • 필드는 일부만 public, private으로 만들 수 있음.
      • private 필드 : 같은 모듈 내에서는 참조 가능.
      • public 필드 : 외부 모듈에서도 참조 가능.
      • Struct가 private 필드로만 구성되어있으면, 외부 모듈에서는 직접 Struct를 생성할 수 없다. 따라서 관습적으로 Struct를 생성하기 위해 new() 메서드를 구현 및 제공해야한다.
    • .. EXPR
      • 구조체 생성 과정에서 이름 있는 필드 뒤에 .. EXPR이 오면, 나머지 필드들이 Assign 된다.
      • 소유권이 있는 필드는 소유권이 이동, Copy Trait을 구현한 필드는 Copy, 레퍼런스는 레퍼런스가 복사된다.
    pub struct GrayscaleMap {
        // 필드를 private으로 하는 경우.
        pixels: Vec<u8>,
        size: (usize, usize)
    
        // 필드를 public으로로 하는 경우
        // pub pixels: Vec<u8>,
        // pub size: (usize, usize),
    }
    
    impl GrayscaleMap {
        // 필드가 private인 경우에는 Struct를 생성조차 할 수 없다.
        // 따라서 관습적으로 New() 메서드를 제공한다.
        fn new(pixels: Vec<u8>, size: (usize, usize)) -> GrayscaleMap {
            assert_eq!(pixels.len(), size.0 * size.1);
            GrayscaleMap { size, pixels }
        }
    }

    위 코드는 필드가 private 일 때, 외부에서 구조체 표현식으로 구조체를 생성할 수 없어서 new() 메서드가 추가되는 것을 설명한다. 

    struct Broom {
        name: String,
        height: u32,
        health: u32,
        position: (f32, f32, f32),
        intent: BroomIntent
    }
    
    ...
    
    fn chop(broom: Broom) -> (Broom, Broom) {
        let mut broom1 = Broom { height: broom.height/2, ..broom };
        let mut broom2 = Broom { name: broom1.name.clone(), ..broom1 };
    
        broom1.name.push_str(" I");
        broom2.name.push_str(" II");
        (broom1, broom2)
    }

    위 코드는 .. EXPR을 이용해 새로운 구조체를 생성하는 것을 보여준다.

    1. broom1이 생성될 때, broom의 필드 name이 broom1로 Move한다. 따라서 broom은 무효화된다. 
    2. broom2를 생성할 때, name은 따로 지정하고 broom1의 값을 .. EXPR로 전달했다. 이 때, 각 타입은 Copy Trait을 구현했기 때문에 broom1은 무효화되지 않고 broom2가 생성된다. 

     


    튜플형 스트럭트

    • 튜플형 스트럭트는 튜플과 유사한 형식으로 데이터를 받는다.
    • 튜플형 스트럭트의 필드 역시 private, public을 지정할 수 있다. 
    • struct.0, struct.1 같은 방식으로 필드에 접근할 수 있다. 
    • 튜플은 필드의 이름이 없기 때문에 의미가 혼동될 수 있다. 따라서 튜플형 스트럭트보단 이름있는 필드를 가진 스트럭트를 많이 사용한다.
      • 튜플형 스트럭트는 New Type에서 주로 사용된다. 이는 타입 검사의 Scope을 좁히고, 코드에 문맥을 추가하는 것을 의미한다. 
    struct Bounds(usize, usize);
    
    // 필드 일부만 public
    // struct Bounds(pub usize, usize);
    
    // 필드 전체 public
    // struct Bounds(pub usize, pub usize);
    
    
    fn t() {
        let image_bounds = Bounds(1024, 768);
        assert_eq!(image_bounds.0, 1024);
        assert_eq!(image_bounds.1, 768);
    }
    • 위 코드는 튜플형 스트럭트의 필드를 public, private을 사용할 수 있음을 보여준다.
    • 위 코드는 튜플형 스트럭트의 각 필드에 struct.0, struct.1 방식으로 접근할 수 있음을 보여준다. 
    struct Ascii(Vec<u8>);
    
    fn t1() {
        let v1: Vec<u8> = vec![0, 1, 2, 3];
        // 이 코드만으로는 Vec<u8> 타입이 뭘 의미하는지 알 수 없다.
        let vec1: Vec<u8> = vec![0, 1, 2, 3];
    
        let v2: Vec<u8> = vec![0, 1, 2, 3];
        // Ascii 타입이란 것을 알려주면서, 이 값이 무엇을 의미하는지 더 명확해졌다.
        let ascii = Ascii(v2);
    }
    • 위 코드는 튜플형 스트럭트를 이용해 NewType을 선언한 것을 보여준다.
      • Vec<u8> 타입 자체는 어떤 문맥도 나타낼 수 있다.
      • 그러나 Ascii<Vec<u8>> 타입은 Vec<u8>이 아스키 문자라는 문맥을 보여준다. 또한, Ascii 타입인지를 검사할 수도 있으므로 타입 검사 Scope이 Vec<u8>에서 좀 더 줄어드는 효과를 가져온다.

     

    유닛형 스트럭트

    • 유닛형 스트럭트는 어떠한 필드도 가지지 않는 스트럭트를 의미한다. 
    • 유닛형 스트럭트는 Trait과 같이 쓸 때 유용하다.
    // 유닛형 스트럭트.
    struct Onesuch;
    
    let o = Onesuch;

     


    스트럭트 레이아웃

    • Struct의 필드는 모두 스택 메모리에 저장된다.
    • Struct의 필드가 여러 개 있을 때,  스택 메모리에 어떤 순서로 저장되는지는 정의되어있지 않다.

    위 이미지는 아래 Struct가 메모리에 어떻게 저장되는지를 보여준다.

    1. pixels, size는 각각 스택 프레임에 저장된다.
    2. pixels는 힙 메모리에 저장된 데이터를 가리키는 포인터, 그리고 길이를 스택 프레임에 저장한다.
    pub struct GrayscaleMap {
        pixels: Vec<u8>,
        size: (usize, usize)
    }
    
    let size = (1024, 956);
    let pixels = vec![0; size.0 * size.1];
    let image = GrayscaleMap { size: size, pixels: pixels };

     


    Impl로 메서드 정의하기

    • struct 내에서는 필드만 정의할 수 있다. struct의 메서드는 Impl 블록 내에서 구현해야한다.
    • Impl 블록에 구현된 메서드는 아래 형태가 올 수 있다. 
      • 아무것도 없는 경우 : new() 함수 같은 것들 만들 때
      • self : 소유권 자체를 이동하는 경우 (소유권이 이동하기 때문에 Struct 자체는 무효화 될 것이다)
      • &self : 내부 필드를 참조만 하는 경우
      • &mut self : 내부 필드의 변경이 필요한 경우
    • 만약 메서드가 첫번째 인자로 &mut self를 원한다고 하더라도, 호출하는 쪽에서 (&mut self).method(...) 같은 형식으로 호출하지 않아도 된다. 자동으로 변환해주기 때문이다.
    pub struct Queue {
        older: Vec<char>,
        younger: Vec<char>,
    }
    
    impl Queue {
    
        // 아무것도 오지 않는 경우
        pub fn new() -> Queue {
            Queue { older: vec![], younger: vec![]}
        }
    
        // self - 메서드가 자기 자신의 소유권을 가져야 하는 경우
        pub fn split(self) -> (Vec<char>, Vec<char>) {
            (self.older, self.younger)
        }
    
        // &mut self - 내부 필드 수정이 필요한 경우
        pub fn push(&mut self, c: char) {
            self.younger.push(c);
        }
    
        pub fn pop(&mut self) -> Option<char> {
            ...
            self.older.pop()
        }
    }
    
    fn he() {
        let mut q = Queue::new();
        // push()는 &mut self를 원한다. self.push()를 이용하더라도, 자동으로 수정해서 전달한다.
        // 여기서 사용자가 굳이 (&mut q).push()를 호출하지 않아도 된다.
        q.push('a');
        q.push('b');
    
        assert_eq!(q.pop(), Some('a'));
        assert_eq!(q.pop(), Some('b'));
    
        // 여기서 q는 미초기화 상태가 됨. split()이 self를 받기 때문임.
        let (older, younger) = q.split();
    }
    • 위 코드는 impl 블록 내에 메서드를 구현하는 것을 보여준다.
    • 위 코드는 메서드에 첫번째 인자로 올 수 있는 타입을 각각 보여준다.
    • 메서드를 호출할 때, &mut self 같은 인자가 필요하더라도 호출부에서 굳이 (&mut self).push(...)로 호출하지 않아도 되는 것을 보여준다.

     

    타입 연관 함수

    • impl 블록 내에 선언된 함수이지만, self 인수를 받지 않는 함수를 의미한다. 주로 new() 같은 생성자 함수를 의미한다.
    • 메서드는 아니지만, 그 타입과 연관된 함수이기 때문에 타입 연관 함수라고 부른다.
    pub struct Queue {
        older: Vec<char>,
        younger: Vec<char>,
    }
    
    impl Queue {
        // 타입 연관 함수 new()
        pub fn new() -> Queue {
            Queue { older: vec![], younger: vec![]}
        }
    }

     

    타입 연관 상수

    • 자바에서는 클래스 내부에 static final을 이용해 타입 연관 상수를 선언할 수 있다.
    • Impl 블록 내부에 const 키워드를 이용해 타입 연관 상수를 선언할 수 있다.
    pub struct Vector2 {
        x: f32, y: f32
    }
    
    impl Vector2 {
        const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 };
        const UNIT: Vector2 = Vector2 { x: 1.0, y: 1.0 };
        const NAME: &'static str = "Vector2";
        const ID: u32 = 18;
    }
    • 위 코드는 impl 블록 내부에 const 키워드를 이용해 상수를 선언한 것을 보여준다.

     


    Generic Struct

    • Generic 매개변수를 이용해 Generic Struct를 구현할 수 있다. 
    • struct, impl에 각각 제네릭 매개변수를 사용하면 된다. 아래 예시에서는 제네릭 매개변수 T를 이용했다.
    • 대문자 Self 키워드를 사용할 수 있음.
      • 반환 타입으로 Queue<T> 대신 Self를 사용할 수 있음.
      • 구조체 생성 시, Queue { ... } 대신 Self { ... } 를 사용할 수 있음.
    pub struct Queue<T> {
        older: Vec<T>,
        younger: Vec<T>,
    }
    
    impl<T> Queue<T> {
    
        // Queue<T> 대신 Self 타입을 사용할 수 있음.
        pub fn new() -> Self {
            // Queue 대신 Self를 사용할 수 있음. 
            Self { older: vec![], younger: vec![]}
        }
    
        pub fn split(self) -> (Vec<T>, Vec<T>) {
            (self.older, self.younger)
        }
    
        pub fn push(&mut self, c: T) {
            self.younger.push(c);
        }
    
        pub fn pop(&mut self) -> Option<T> {
            if self.older.is_empty() {
                if self.younger.is_empty() {
                    return None;
                }
    
                use std::mem::swap;;
                swap(&mut self.older, &mut self.younger);
                self.older.reverse();
            }
            self.older.pop()
        }
    }

     

     

    Lifetime을 가지는 Generic Struct

    • Struct가 레퍼런스를 필드로 가지면, 레퍼런스의 유효 라이프타임이 반드시 표현되어야 한다. 
    struct Extrema<'elt> {
        greatest: &'elt i32, 
        least: &'elt i32,
    }
    

     


    상수 매개변수를 가지는 Generic Struct

    • 상수 제네릭 매개변수를 Generic으로 가지는 Generic Struct도 지원된다.
      • 상수 제네릭 매개변수에는 연산이 허용되지는 않는다. (예시 : N+1)
    • 아래 코드에서는 new()에 주어진 coefficients의 길이 N을 보고, 생성된 Polynomial의 타입이 추론된다. 
    • Struct가 여러 종류의 제네릭 매개변수를 받을 때는 다음 순서로 배치되어야 한다.
      • Lifetime
      • 일반 제네릭 타입매개변수
      • Const 제네릭 타입 매개변수
    struct Polynomial<const N: usize> {
        coefficients: [f64; N]
    }
    
    impl<const N: usize> Polynomial<N> {
        
        pub fn new(coefficients: [f64; N]) -> Polynomial<N> {
            Self { coefficients }
        }
    }
    
    fn t() {
        let coefficients = [0.0; 6];
        let poloynomial = Polynomial::new(coefficients);
    }
    • 위 코드에서 coefficients의 길이는 6이다. 따라서 생성된 struct의 타입은 Polynomial<6>으로 추론된다.
    struct LumpOfReferences<'a, T, const N: usize> {
        the_lump: [&'a T; N],
    }
    • 위 코드에서는 다양한 제네릭 매개변수가 왔다. 이 경우, 라이프타임 - 타입 제네릭 - const 제네릭 순서로 와야한다.

     

     

    Struct 타입에 공통 Trait 구현하기

    Struct끼리 같은 값을 가지는지는 어떻게 비교할 수 있을까? 혹은 Struct끼리의 대소비교는 어떻게 할 수 있을까? 이것과 관련된 주제가 공통 Trait이다.

    • Rust는 Debug, PartialEq, PartialOrder 같은 Common Trait을 제공함. 
    • #[derive()]에 구조체가 구현하고자 하는 Common Trait을 명시하면, 자동으로 구현됨. 이 때, 구조체의 각 필드는 해당 Common Trait을 구현하고 있어야 함. 
    • #[derive()]로 명시하지 않는 한, 각 Trait API는 private이다. 이 정책이 default가 된 것은 기본적으로 public API라면 확장이 어렵기 때문이다.
    #[derive(PartialEq, Debug)]
    struct Point {
        x: i32,
        y: i32,
    }
    
    
    fn equal() {
        let a = Point{ x: 123, y: 456 };
        let b = Point{ x: 123, y: 456 };
        let eq = a==b;
    }
    
    

     

     

     

    내부가변성 (Interior Mutability)

    • 외부에서는 불변의 값처럼 보이길 원하지만, 내부에서는 가변적인 동작이 필요한 경우가 있다. 이 때 사용하는 것이 내부가변성이다.
    • 예를 들어 외부에서 메서드 호출 시, &self (Shared Reference)를 얻어서 외부에서 바라봤을 때는 불변이라고 생각할 수 있다. 그러나 내부 메서드에서는 값이 수정되기를 원하는 경우를 의미한다.
    • 주로 다음 스마트포인터를 이용해서 내부가변성을 구현한다.
      • Cell<T> : 값을 복사해서 받아오고, 갈아끼우는 형태로 동작한다.
      • RefCell<T> : 참조값을 빌려오고 업데이트하는 형태로 동작한다.
    • 일반적인 참조자는 Borrow Checker를 통해서 컴파일 타임에 에러를 검출한다.
    • 내부가변성에 사용되는 Cell, RefCell은 참조자 규칙을 지키지 않을 경우 런타임에 패닉을 발생시킨다. (불변 + 가변 참조자가 동시에 존재한다거나 하는...) 
    struct Cache {
        store : RefCell<Option<i32>>
    }
    
    impl Cache {
    
        fn new() -> Cache {
            Cache { store: RefCell::new(None) }
        }
        
        // &self를 반환하기 때문에 외부에서는 불변으로 기대함.
        fn get_or_default(&self, compute: impl FnOnce() -> i32) -> i32 {
            if self.store.borrow().is_none() {
                let result = compute();
                // 그러나 내부에서는 가변으로 동작함.
                *self.store.borrow_mut() = Some(result); 
            }
            self.store.borrow().unwrap()
        }
    }
    
    
    fn main() {
        let cache = Cache::new();
    
        let result = cache.get_or_default(|| {
            println!("Computing...");
            42
        });
        println!("{}", result);
    }

     

     

     

     

     

     

     

     

     

     

     

    '프로그래밍 언어 > Rust' 카테고리의 다른 글

    Rust Chapter11: Trait  (1) 2025.01.25
    Rust : 레퍼런스(Reference)  (1) 2024.12.21
    Rust : 소유와 이동  (2) 2024.12.14

    댓글

    Designed by JB FACTORY