
오늘은 러스트에서 어떻게 객체 지향적인 코드를 짜는지 알아보겠다.
러스트는 전통적인 객체 지향(OOP)은 아니다. 그렇지만 객체 지향적인 코드는 작성할 수 있다.
러스트는 구조체로 객체에서 사용할 필드를 정의할 수 있다.
struct Foo {
name: String
}
구조체로 필드를 정의했으면 이 객체의 고유 동작을 구현(impl)을 통해 정의할 수 있다.
상속이 없는 대신 이 구현 문법이 굉장히 많이 쓰인다.
한 구조체에 여러 개의 구현이 존재할 수 있다. 이런 역할이 상속을 대체하는 것이다.
앞서 트레잇을 구현할 때에도 이 문법이 쓰인다.
impl Foo {
fn new() -> Self {
Foo {
name: "Bar".into()
}
}
}
또, 메소드를 구현할 때 self를 참조하는 방법과 사용하는 방법이 있다.
fn bar(&self) {
println!("{}", &self.name);
}
이렇게 self를 참조하게 될 경우 불변 참조가 만들어지며 self를 소비하지 않는다.
가변 참조로도 만들 수 있다.
fn bar1(&mut self) {
println!("{}", &self.name);
}
self를 소비하지 않고 self 값을 변경할 수 있는 메소드이다.
마지막으로 소비하는 형태의 메소드를 만들 수 있는데,
fn bar2(self) {
// do something
}
이렇게 만들면 소유권이 메소드 스코프로 move 되며 더 이상 밖에서 사용하는 것이 불가능하다.
let foo = Foo::new();
foo.bar1();
foo.bar1();
foo.bar2(); // moved to scope
// not available code
// foo.bar();
트레잇을 간단하게 설명을 하자면
타입이 가져야 할 공유 동작을 정의하는 인터페이스
이다.
이 트레잇은 한 타입에 여러 개가 구현될 수도 있다.
즉 OOP에서 인터페이스 역할을 대신 하는 것이다.
trait Foo {
fn bar(&self) -> bool;
}
impl Foo for Bar {
fn bar(&self) -> bool {
true
}
}
위와 같은 방식으로 객체 지향을 흉내낼 수 있다. 배운 것들을 정리해보면 다음과 같은 코드로 객체 지향을 흉내낼 수 있다. ResponseType은 임의의 enum 타입이라고 하자.
trait Response {
fn response_type(&self) -> ResponseType;
}
struct BasicResponse {
r#type: String,
}
impl Response for BasicResponse {
fn new() -> Self {
BasicResponse {
r#type: "FOO".into()
}
}
fn response_type(&self) -> ResponseType {
ResponseType::Basic
}
}
fn main() {
let response = BasicResponse::new();
// do something
}
다음 글에서는 공유 동작에서 안전하지 않은 행동들, 즉 object safety에 대해서 알아보겠다.