[RUST] 5. 구조체를 활용한 관련 데이터의 구조화

    구조체(struct or structure)는 서로 관련이 있는 여러값을 의미 있는 하나로 모으고 이름을 지정해 접근할 수 있는 사용자 정의 데이터 타입이다.

    구조체 정의와 인스턴스 생성

    구조체를 정의하려면 struct 키워드 다음에 구조체에 부여할 이름을 지정해주면 된다. 이후 중괄호 안에 구조체가 저장할 데이터의 타입과 이름을 나열한다. 이 데이터들을 필드(field)라고 한다. 구조체를 정의한 후 이를 사용하려면 각 필드에 저장할 값을 명시에서 구조체의 인스턴스를 생성해야 한다. 정의 순서와 같을 필요는 없다. 구조체에서 원하는 값을 읽으려면 마침표(.)를 이용하면 된다.

    struct User {
        username: String,
        email: String,
        sign_in_count: u64,
        active: bool,
    }
    
    let mut user1 = User {
        email: String::from("someone@example.com");
        username: String::from("someusername123");
        active: true,
        sign_in_count: 1,
    };
    
    user1.email = String::from("anotheremail@example.com");

    러스트는 구조체의 몇몇 필드만을 가변 데이터로 표시하는 것을 지원하지 않는다.

    fn build_user(email: String, username: String) -> User {
        User {
            email: email,
            username: username,
            active: true,
            sign_in_count: 1,
        }
    }

    매개변수 이름을 구조체의 필드 이름과 똑같게 정의하여 코드를 쉽게 이해할수 있지만 같은 이름이 반복되는 단점이 있다.

    같은 이름의 필드와 변수를 편리하게 활용하기

    필드 초기화 단축 문법(field init shorthand syntax)을 이용해 변수 이름을 일일이 번복해서 입력하지 않아도 같은 결과를 얻을 수 있도록 작성할 수 있다.

    fn build_user(email: String, username: String) -> User {
        User {
            email,
            username,
            active: true,
            sign_in_count: 1,
        }
    }

    기존의 인스턴스로부터 새 인스턴스 생성하기

    구조체 갱신 문법(struct update syntax)은 이미 존재하는 인스턴스에서 몇가지 필드의 값만 수정할 때 사용하면 편리하다.

    let user2 = User {
        email: String::from("another@example.com"),
        username: String::from("anotherusername567"),
        active: user1.active,
        sign_in_count: user1.sign_in_count,
    }
    
    //email과 username만 새로운 값을 대입하고 나머지는 user1의 필드값을 대입
    let user2 = User {
        email: String::from("another@example.com"),
        username: String::from("anotherusername567"),
        ..user1
    }

    이름 없는 필드를 가진 튜플 구조체로 다른 타입 생성하기

    튜플 구조체(tuple structs)는 필드에 이름을 부여하지 않고 타입만 지정하는 구조체를 의미한다.

    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);
    
    let black = Color(0,0,0);
    let origin = Point(0,0,0);

    필드가 없는 유사 유닛 구조체

    유사 유닛 구조체(unit-like structs)는 필드가 하나도 없는 구조체를 뜻한다. ()과 유사하게 동작한다. 관련 트레이트를 구현해야 활용할 수 있는데 10장에서 알아보도록 하자

    구조체를 사용하는 예제 프로그램

    우선 사각형을 구하는 프로그램을 작성해보자

    fn main() {
        let width1 = 30;
        let height1 = 50;
    
        println!(
            "사각형의 면적: {} 제곱 픽셀",
            area(width1, height1)
        );
    }
    
    fn area(width: i32, height: i32) -> u32 {
        width * height
    }

    튜플을 이용한 리팩토링

    fn main() {
        let rect1 = (30,50);
    
        println! (
            "사각형의 면적: {} 제곱 픽셀",
            area(rect1)
        );
    }
    
    fn area(dimensions: (u32, u32)) -> u32 {
        dimensions.0 * dimensions.1
    }

    튜플로 인해 하나의 인수만 전달할수 있도록 변경되었으나, 함수에서 튜플의 각요소의 의미가 명확하지 않은 단점이 있다. 높이와 너비를 뒤바꿔 참조하더라도 문제가 없지만 원소 순서별 의미를 기억해야한다.

    구조체를 이용한 리팩토링: 더 많은 의미 반영하기

    데이터에 이름을 부여해서 의미를 반영하기 위해 구조체를 사용한다.

    struct Rectangle {
        width: u32,
        height: u32,
    }
    
    fn main() {
        let rect1 = Rectangle {width: 30, height: 50};
    
        println!(
            "사각형의 면적: {} 제곱 픽셀",
            area(&rect1)
        );
    }
    
    fn area(rectangle: &Rectangle) -> u32 {
        rectangle.width * rectangle.height
    }

    트레이트를 상속해서 유용한 기능 추가하기

    struct Rectangle {
        width: u32,
        height: u32,
    }
    
    fn main() {
        let rect1 = Rectangle {width: 30, height: 50};
    
        println!("rect1: {}", rect1);
    }

    위 코드는 실행하면 에러메시지가 출력된다. 이를 수정하기 위해Rectangle 구조체가 디버깅 정보를 출력할 수 있도록 Debug 트레이트를 상속하는 애노테이션을 추가하였다.

    #[derive(debug)]
    struct Rectangle {
        width: u32,
        height: u32,
    }
    
    fn main() {
        let rect1 = Rectangle {width: 30, height: 50};
    
        println!("rect1: {:?}", rect1);
    }

    수정한 코드는 다음과 같이 출력된다.

    rect1: Rectangle { width: 30, height: 50 }

    메서드 문법

    메서드(method)는 함수와 유사하다. 함수와 마찬가지로 fn키워드를 이용해 정의하며 이름, 매개변수, 리턴 타입을 정의할 수 있다. 하지만 메서드는 함수와 달리 구조체의 컨텍스트(context) 안에 정의하며, 첫 번째 매개변수는 항상 메서드를 호출할 구조체의 인스턴스를 표현하는 self여야한다.

    메서드 정의하기

    #[derive(Debug)]
    struct Rectangle {
        width: u32,
        height: u32,
    }
    
    impl Rectangle {
        fn area(&self) -> u32 {
            self.width * self.height
        }
    }
    
    fn main() {
        let rect1 = Rectangle { width: 30, height: 50 };
    
        println!(
            "사각형의 면적: {} 제곱 픽셀",
            rect1.area()
        )
    }

    Rectangle 구조체의 메서드로 변환하였다. Rectangle 타입의 컨텍스트 안에 함수를 정의하려면 impl 블록을 이용하면 된다.

    더 많은 매개변수를 갖는 메서드

    fn main() {
        let rect1 = Rectangle { width: 30, height: 50 };
        let rect2 = Rectangle { width: 10, height: 40 };
        let rect3 = Rectangle { width: 60, height: 45 };
    
        println!("rect1은 rect2를 포함하는가? {}", rect1.can_hold(&rect2));
        println!("rect1은 rect3를 포함하는가? {}", rect1.can_hold(&rect3));
    }
    
    impl Rectangle {
        fn area(&self) -> u32 {
            self.width * self.height
        }
    
        fn can_hold(&self, other: &Rectangle) -> bool {
            self.width > other.width && self.height > other.height
        }
    }

    메서드에 여러 개의 매개변수를 사용하려면 self 매개변수 이후에 원하는 만큼의 매개변수를 추가하면 된다.

    연관 함수

    impl 블록의 다른 기능은 self 매개변수를 사용하지 않는 다른 함수도 정의할 수 있다. 이런 함수들은 연관 함수(associated functions)라 한다.

    impl Rectangle {
        fn square(size: u32) -> Rectangle {
            Rectangle { width: size, height: size }
        }
    }

    여러개의 ipml 블록

    각 구조체는 여러 개의 impl 블록을 선언할 수 있다. 문법적으로 아무런 문제가 없다.

    impl Rectangle {
        fn area(&self) -> u32 {
            self.width * self.height
        }
    }
    
    impl Rectangle {
        fn can_hold(&self, other: &Rectangle) -> bool {
            self.width > other.width && self.height > other.height
        }
    }

    요약

    • 구조체는 프로그램 안에서 사용자 정의 타입을 선언하기 위한 개념
    • 메서드는 구조체의 인스턴스에 원하는 동작을 부여
    • 연관 함수는 구조체의 인스턴스가 없는 상황에서 구조체에 적용할 수 있는 기능을 구분
    반응형

    댓글

    Designed by JB FACTORY