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