7장 - 객체 지향 프로그래밍

OOP란 무엇인가?

OOP(Object-Oriented Programming)은 다음의 특징을 가진 프로그래밍 언어
  • 캡슐화 (encapsulation)객체라 불리는 단일 유형의 개념적 단위에 데이터와 함수를 연결지음.
  • 추상화 (abstraction) - 데이터와 함수를 숨겨 객체의 상세 구현 사항을 알기 어렵게 함.
  • 다형성 (polymorphism) - 다른 기능적 관점에서 객체와 상호작용하는 능력.
  • 상속 (inheritance) - 다른 객체로부터 데이터와 동작을 상속받는 능력.

Rust는 OOP가 아니다

  • Rust는 상속이 불가능

메소드 캡슐화 하기

  • 함수(메소드라고도 하는)가 연결된 struct인 객체라는 개념을 지원
  • 모든 메소드의 첫번째 매개변수는 메소드와 연관된 인스턴스의 참조
    • &self - 인스턴스에 대한 immutable한 참조.
    • &mut self - 인스턴스에 대한 mutable한 참조.
  • 메소드는 impl 키워드 블록 안에 정의
impl MyStruct { ... fn foo(&self) { ... } }
struct SeaCreature { noise: String, } impl SeaCreature { fn get_sound(&self) -> &str { &self.noise } } fn main() { let creature = SeaCreature { noise: String::from("blub"), }; println!("{}", creature.get_sound()); }

선택적 노출을 통한 추상화

  • Rust는 객체의 내부 동작을 숨길 수 있음
  • 필드와 메소드들은 그들이 속한 모듈에서만 접근 가능
  • pub 키워드는 struct의 필드와 메소드를 모듈 밖으로 노출
모듈에 대한 내용은 9장 - 프로젝트 구성과 구조 참고
struct SeaCreature { pub name: String, noise: String, } impl SeaCreature { pub fn get_sound(&self) -> &str { &self.noise } } fn main() { let creature = SeaCreature { name: String::from("Ferris"), noise: String::from("blub"), }; println!("{}", creature.get_sound()); }

다형성과 Trait

  • Rust는 trait 으로 다형성 지원 (타 언어의 interface와 유사)
  1. trait 안에 메소드 원형을 정의
trait MyTrait { fn foo(&self); ... }
  1. struct의 구현된 trait 메소드들은 impl 블록 안에 정의
impl MyTrait for MyStruct { fn foo(&self) { ... } ... }
struct SeaCreature { pub name: String, noise: String, } impl SeaCreature { pub fn get_sound(&self) -> &str { &self.noise } } trait NoiseMaker { fn make_noise(&self); } impl NoiseMaker for SeaCreature { fn make_noise(&self) { println!("{}", &self.get_sound()); } } fn main() { let creature = SeaCreature { name: String::from("Ferris"), noise: String::from("blub"), }; creature.make_noise(); }

Trait에 구현된 메소드

  • trait에 메소드를 구현해 넣을 수 있음
struct SeaCreature { pub name: String, noise: String, } impl SeaCreature { pub fn get_sound(&self) -> &str { &self.noise } } trait NoiseMaker { fn make_noise(&self); fn make_alot_of_noise(&self) { self.make_noise(); self.make_noise(); self.make_noise(); } } impl NoiseMaker for SeaCreature { fn make_noise(&self) { println!("{}", &self.get_sound()); } } fn main() { let creature = SeaCreature { name: String::from("Ferris"), noise: String::from("blub"), }; creature.make_alot_of_noise(); }

Trait 상속

trait은 다른 trait의 메소드들을 상속 받을 수 있음
struct SeaCreature { pub name: String, noise: String, } impl SeaCreature { pub fn get_sound(&self) -> &str { &self.noise } } trait NoiseMaker { fn make_noise(&self); } trait LoudNoiseMaker: NoiseMaker { // NoiseMaker 상속 fn make_alot_of_noise(&self) { self.make_noise(); self.make_noise(); self.make_noise(); } } impl NoiseMaker for SeaCreature { fn make_noise(&self) { println!("{}", &self.get_sound()); } } impl LoudNoiseMaker for SeaCreature {} fn main() { let creature = SeaCreature { name: String::from("Ferris"), noise: String::from("blub"), }; creature.make_alot_of_noise(); }

동적 vs 정적 디스패치

메소드는 다음의 두 가지 방식으로 실행
  • 정적 디스패치 (static dispatch) - 인스턴스의 자료형을 알고 있는 경우, 어떤 함수룰 호출해야 하는지 정확히 알고 있음
  • 동적 디스패치 (dynamic dispatch) - 인스턴스의 자료형을 모르는 경우, 올바른 함수를 호출할 방법을 찾아야 함
&dyn MyTrait은 동적 디스패치를 통해 객체의 인스턴스들을 간접적으로 작동
동적 디스패치는 trait 자료형 앞에 dyn을 붙일 것을 권고
struct SeaCreature { pub name: String, noise: String, } impl SeaCreature { pub fn get_sound(&self) -> &str { &self.noise } } trait NoiseMaker { fn make_noise(&self); } impl NoiseMaker for SeaCreature { fn make_noise(&self) { println!("{}", &self.get_sound()); } } fn static_make_noise(creature: &SeaCreature) { // 실제 자료형을 압니다 creature.make_noise(); } fn dynamic_make_noise(noise_maker: &dyn NoiseMaker) { // 실제 자료형을 모릅니다 noise_maker.make_noise(); } fn main() { let creature = SeaCreature { name: String::from("Ferris"), noise: String::from("blub"), }; static_make_noise(&creature); dynamic_make_noise(&creature); }
💡
메모리 상세
동적 디스패치는 실제 함수 호출을 위한 포인터 추적으로 인해 느릴 수 있음

Trait 객체

  • trait 객체: &dyn MyTrait 자료형을 가진 매개변수
  • trait 객체는 인스턴스의 올바른 메소드를 간접적으로 호출
  • 인스턴스에 대한 포인터와 인스턴스 메소드들에 대한 함수 포인터 목록을 갖고있는 struct
  • C++에서는 vtable이라고 함

크기를 알 수 없는 데이터 다루기

  • trait은 원본 struct의 원래 크기를 알기 어려움
  • Rust에서 크기를 알 수 없는 값이 struct에 저장될 때는 다음의 두 가지 방법으로 처리
    • generics - 매개변수의 자료형을 효과적으로 활용하여 알려진 자료형 및 크기의 struct/함수를 생성
    • indirection - 인스턴스를 heap에 올림으로써 실제 자료형의 크기 걱정 없이 그 포인터만 저장할 수 있는 간접적인 방법을 제공

Generic 함수

  • Rust의 generic은 trait과 함께 작동
  • 매개변수 자료형 T를 정의할 때 해당 인자가 어떤 trait을 구현해야 하는지 나열
  • 아래 예제에서 T 자료형은 Foo trait을 반드시 구현해야 함
fn my_function<T>(foo: T) where T:Foo // Foo trait을 구현해야함 { ... }
  • generic을 이용하면 컴파일 시 자료형과 크기를 알 수 있음 (정적 자료형 함수 생성)
struct SeaCreature { pub name: String, noise: String, } impl SeaCreature { pub fn get_sound(&self) -> &str { &self.noise } } trait NoiseMaker { fn make_noise(&self); } impl NoiseMaker for SeaCreature { fn make_noise(&self) { println!("{}", &self.get_sound()); } } fn generic_make_noise<T>(creature: &T) where T: NoiseMaker, { // 컴파일 타임에 실제 자료형을 알게 됩니다 creature.make_noise(); } fn main() { let creature = SeaCreature { name: String::from("Ferris"), noise: String::from("blub"), }; generic_make_noise(&creature); }

Generic 함수 줄여쓰기

다음과 같이 줄여쓸 수 있음
fn my_function(foo: impl Foo) { // 타입 앞에 'impl' 키워드 ... }
다음과 동일
fn my_function<T>(foo: T) where T:Foo { ... }
struct SeaCreature { pub name: String, noise: String, } impl SeaCreature { pub fn get_sound(&self) -> &str { &self.noise } } trait NoiseMaker { fn make_noise(&self); } impl NoiseMaker for SeaCreature { fn make_noise(&self) { println!("{}", &self.get_sound()); } } fn generic_make_noise(creature: &impl NoiseMaker) { // whwere -> impl // 컴파일 타임에 실제 자료형을 알게 됩니다 creature.make_noise(); } fn main() { let creature = SeaCreature { name: String::from("Ferris"), noise: String::from("blub"), }; generic_make_noise(&creature); }

Box

  • Box는 stack에 있는 데이터를 heap으로 옮길 수 있게 해주는 데이터 구조
  • Box는 smart pointer로도 알려진 struct이며 heap에 있는 데이터를 가리키는 포인터
  • Box는 크기가 알려져 있는 struct (그저 포인터만 저장하고 있으므로)
  • field의 크기를 알아야 하는 struct에 뭔가의 참조를 저장할 때 종종 사용
  • 많이 씀 Box::new(Foo { ... })
struct SeaCreature { pub name: String, noise: String, } impl SeaCreature { pub fn get_sound(&self) -> &str { &self.noise } } trait NoiseMaker { fn make_noise(&self); } impl NoiseMaker for SeaCreature { fn make_noise(&self) { println!("{}", &self.get_sound()); } } struct Ocean { animals: Vec<Box<dyn NoiseMaker>>, } fn main() { let ferris = SeaCreature { name: String::from("Ferris"), noise: String::from("blub"), }; let sarah = SeaCreature { name: String::from("Sarah"), noise: String::from("swish"), }; let ocean = Ocean { animals: vec![Box::new(ferris), Box::new(sarah)], }; for a in ocean.animals.iter() { a.make_noise(); } }

Generic 구조체 다시 보기

  • generic struct는 trait으로 제한된 매개변수 자료형 가질 수 있음
struct MyStruct<T> where T: MyTrait { foo: T ... }
  • 매개변수 자료형은 generic structure의 구현 블록 안에 표시
impl<T> MyStruct<T> { ... }

The Rust Programming Language 링크

의문점

  • generic 함수에서 where 절의 타겟 trait을 여러개 설정 가능한데, impl로 줄여서 표현은 어떻게?