DataScience
Published 2023. 5. 10. 11:15
Rust 구조체 Rust
728x90

러스트는 객체지향 프로그래밍보다는 함수형 프로그래밍에 더 가깝습니다. 단적인 예로 러스트 코드는 이터레이터와 클로저를 적극적으로 사용합니다. 이러한 이유에서 클래스가 존재하지 않습니다. 대신 비슷한 역할을 구조체 struct를 통해서 구현 할 수 있습니다.

구조체

구조체의 정의

구조체 선언

먼저 파이썬에서 클래스를 하나 정의해 보겠습니다. Person 클래스는 객체화 시 name, age 두 변수를 파라미터로 받고, self.name, self.age 라는 인스턴스 프로퍼티에 할당됩니다.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

러스트에서 구조체를 선언하기 위해서는 struct 키워드 뒤에 구조체 이름을 명시하면 됩니다. 구조체 안에서는 필드명: 타입명 으로 필드를 적어줍니다. 필드를 통해 변수를 구조체에 묶어둘 수 있습니다. 여기서 #[derive(Debug)] 는 미리 정의되어 있는 기능으로(derived trait 라고 합니다), 구조체의 내용을 보기 위해서 필요합니다.

#[derive(Debug)] // derived traits
struct Person {
    name: String,
    age: i32,
}

이제 각각 인스턴스를 생성해 보겠습니다.

jane = Person("jane", 30)
jane.age += 1
print(jane.name, jane.age)
print(jane.__dict__)

그리고 프로퍼티를 변경하고 출력해 보겠습니다. 마지막으로 __dict__ 를 이용해 인스턴스를 딕셔너리로 출력해 볼 수도 있습니다.

#[derive(Debug)] // derived traits
struct Person {
    name: String,
    age: i32,
}

fn main() {
    let mut jane = Person {
        name: String::from("Jane"),
        age: 30
    };
    jane.age += 1;
    println!("{} {}", jane.name, jane.age);
    println!("{:?}", jane);
}

인스턴스를 생성할 때는 구조체 이름 뒤에서 {필드명: 값} 문법으로 값들을 넣어주면 됩니다. 프로퍼티는 파이썬과 동일하게 접근과 변경이 가능합니다. 그런데 여기서 인스턴스 jane 이 mutable로 선언되었습니다. 왜냐하면 jane.age 의 값을 변경하고 있기 때문에 구조체 자체가 mutable로 선언되어야 합니다.

 

마지막으로 jane을 출력하는데 "{:?}" 가 사용되었는데, 이 문법은 디버그 출력이라고 합니다. 원래 러스트에서 어떤 값을 출력하려면 그 값은 Format이 정의되어 있어야 하는데, Person 구조체는 정의되어 있지 않기 때문에 디버그 출력을 이용해 간편하게 내용을 확인할 수 있습니다.

메소드

만일 파이썬의 Person 클래스를 객체화할 때, alive = True 라는 프로퍼티를 추가하고 싶다면 아래와 같이 할 수 있습니다.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.alive = True

함수는 메소드를 통해 구조체에 묶을 수 있습니다. impl 키워드 뒤에 구조체 이름을 명시해서 해당 구조체에 속한 메소드를 선언할 수 있습니다. 파이썬의 생성자와 마찬가지로, 객체화할때 사용되는 new 라는 함수를 정의할 수 있습니다. 메소드가 아닌 "함수"도 구조체 정의에 포함시킬 수 있는데, 이 경우는 연관 함수(Associated function)이라고 부르고, 파라미터에 self가 들어있지 않습니다. 먼저 구조체 정의에 alive: bool', 을 추가합니다.

#[derive(Debug)] // derived traits
struct Person {
    name: String,
    age: i32,
    alive: bool,
}

impl Person {
    fn new(name: &str, age: i32) -> Self {
        Person {
            name: String::from(name),
            age: age,
            alive: true,
        }
    }
}

new 함수의 리턴 타입이 Self인데, 자신이 속한 구조체 타입인 Person 클래스를 리턴한다는 의미입니다. 물론 -> Person으로 써도 동일하지만, Self 가 더 권장되는 방법입니다. 그리고 이 함수 안에서 구조체를 alive: true 값을 넣어서 생성하고 있는 것을 알 수 있습니다.

인스턴스를 생성하는 메소드 말고 일반적인 메소드도 추가가 가능합니다. 먼저 파이썬에서는

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.alive = True
        
    def info(self):
      	print(self.name, self.age)

    def get_older(self, year):
        self.age += year

러스트에서는 아래와 같습니다.

impl Person {
    fn new(name: &str, age: i32) -> Person {
        Person {
            name: String::from(name),
            age: age,
            alive: true,
        }
    }

    fn info(&self) {
        println!("{} {}", self.name, self.age)
    }

    fn get_older(&mut self, year: i32) {
        // if we don't borrow the ownership, ownership will be moved to the
        // function and the variable will be dropped
        // self must be passed as mutable reference
        self.age += year;
    }
}

이때 self 가 borrowed 되면서 mutable 인 것에 주의합니다. 왜냐하면 인스턴스 프로퍼티가 변경되기 때문에 self가 mutable이어야 합니다. 여기서 info 메소드의 &self를 self로 바꾸면 어떻게 될까요?

다시 인스턴스를 생성하고 메소드를 호출해 보겠습니다.

john = Person("john", 20)
john.info()
john.get_older(3)
john.info()

get_older 메소드를 통해 age가 3 증가합니다. 러스트에서도 동일합니다.

fn main() {
    let mut john = Person::new("john", 20);
    john.info();
    john.get_older(3);
    john.info();
}

정리하자면, 구조체 안에는 self 파라미터를 사용하지 않는 연관 함수와 self 파라미터를 사용하는 메소드 모두 정의될 수 있습니다.

튜플 구조체(Tuple struct)

튜플 구조체는 구조체 필드가 이름 대신 튜플 순서대로 정의되는 구조체입니다. 필드 참조 역시 튜플의 원소를 인덱스로 참조하는 것과 동일합니다.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    println!("{} {}", black.0, origin.0);
}

 

트레이트(trait)

트레이트로 메소드 공유하기

파이썬은 클래스를 상속해 공통된 메소드를 사용할 수 있지만, 러스트는 구조체의 상속이 되지 않습니다.

먼저 파이썬에서 다음과 같이 Person 을 상속하는 새로운 클래스 Student 를 선언합니다.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.alive = True

    def say_hello(self):
        print("Hello, Rustacean!")

    def get_older(self, year):
        self.age += year


class Student(Person):
    def __init__(self, name, age, major):
        super().__init__(name, age)
        self.major = major

    def say_hello(self):
        print(f"Hello, I am {self.name} and I am studying {self.major}")

Student 클래스는 새로운 프로퍼티 major 를 가지고 있고, Person  say_hello 메소드를 오버라이드하고 있습니다.

Rust는 하나의 struct를 상속하는 방법이 존재하지 않습니다. 즉 필드와 메소드를 다른 struct에 전달할 수 없습니다. 하지만 서로 다른 struct 타입들이 메소드를 공유할 수 있는 하나의 속성을 정의할 수 있는데, 바로 trait입니다. 러스트에서는 구조체에서 공유하는 메소드를 구현하기 위해 트레이트를 먼저 선언해야 합니다. 트레이트에서는 공유할 메소드의 원형을 선언합니다.

trait Greet {
    fn say_hello(&self) {}
}

이렇게 선언하면, say_hello는 아무것도 실행하지 않는 빈 함수이기 때문에, 실제 내용을 각 구조체의 메소드에서 구현해야 합니다. 혹은 say_hello의 기본 구현형을 트레이트를 선언할 때 정의할 수도 있습니다.

이제 파이썬과 동일하게 러스트 코드를 작성해 보겠습니다.

struct Person {
    name: String,
    age: i32,
    alive: bool,
}

impl Person {
    fn new(name: &str, age: i32) -> Person {
        Person {
            name: String::from(name),
            age: age,
            alive: true
        }
    }

    fn get_older(&mut self, year: i32) {
        self.age += year;
    }
}

impl Greet for Person {}


struct Student {
    name: String,
    age: i32,
    alive: bool,
    major: String,
}

impl Student {
    fn new(name: &str, age: i32, major: &str) -> Student {
        Student {
            name: String::from(name),
            age: age,
            alive: true,
            major: String::from(major),
        }
    }
}

impl Greet for Student {
    fn say_hello(&self) {
        println!("Hello, I am {} and I am studying {}", self.name, self.major)
    }
}

이제 메인 함수에서 Person과 Student 구조체의 인스턴스를 만들고 say_hello 메소드를 각각 호출해 보겠습니다.

fn main() {
    let mut person = Person::new("John", 20);
    person.say_hello(); // 🫢
    person.get_older(1);
    println!("{} is now {} years old", person.name, person.age);

    let student = Student::new("Jane", 20, "Computer Science");
    student.say_hello();
}

실행 결과

John is now 21 years old
Hello, I am Jane and I am studying Computer Science

person.say_hello()는 trait Greet의 메소드를 그대로 사용하기 때문에 아무것도 출력되지 않는 것을 알 수 있습니다.

다시 트레이트 선언으로 돌아가보면 say_hello 함수는 파라미터로 &self 를 받고 있지만, 트레이트에 정의되는 함수는 인스턴스 프로퍼티에 접근할 수 없습니다. 만일 여기서 다음과 같이 함수의 원형을 수정하고 컴파일해보면 에러가 발생합니다.

trait Greet {
    fn say_hello(&self) {
        println!("Hello, Rustacean!");
    }
}

파생(Derive)

컴파일러는 #[derive] 트레이트을 통해 일부 특성에 대한 기본 구현을 제공할 수 있습니다. 보다 복잡한 동작이 필요한 경우 이러한 특성은 직접 구현할 수 있습니다.

다음은 파생 가능한 트레이트 목록입니다:

  • 비교: Eq, PartialEq, Ord, PartialOrd.
  • Clone, 복사본을 통해 &T에서 T를 생성합니다.
  • Copy, '이동 시맨틱' 대신 '복사 시맨틱' 타입을 제공합니다.
  • Hash, &T에서 해시를 계산합니다.
  • Default, 데이터 타입의 빈 인스턴스를 생성합니다.
  • {:?} 포매터를 사용하여 값의 형식을 지정하려면 Debug.
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1); // 🤯
}
error[E0277]: `Rectangle` doesn't implement `Debug`
  --> src/main.rs:12:31
   |
12 |     println!("rect1 is {:?}", rect1); // 🤯
   |                               ^^^^^ `Rectangle` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
}

'Rust' 카테고리의 다른 글

Rust 제네릭  (88) 2023.05.12
Rust 모듈과 크레이트  (98) 2023.05.11
Rust 데이터 구조와 이터레이터  (77) 2023.04.22
Rust 소유권  (89) 2023.04.21
Rust 반복문  (83) 2023.04.20
profile

DataScience

@Ninestar

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!