안녕하세요. 오늘은 rust에서 polymorphism을 활용하는 방법을 정리해 보고자 합니다.
cpp 코드를 작성하던 습관 대로 Rust 코드를 작성하다 보면 막히는 부분 중 하나가 interface 클래스(pure virtual class)와 이를 통한 객체 전달 부분입니다. 단순히 trait으로 변환하여 코드를 작성하다 보면 쉽게 컴파일 에러 지옥에 빠지곤 하는데요..
아주 간단한 composite design pattern 예제를 구현해 보면서 rust의 polymorphism 에 대해 알아보도록 하겠습니다. Composite pattern 예제로 많이 사용되는 File, Directory 구조를 표현해 보고자 합니다. File은 이름과 크기를 갖고 있는 객체로, Directory는 이름과 하위 파일 및 디렉토리를 갖는 객체로, 그리고 File, Directory 는 모두 Node 라는 interface를 구현하게 만들고자 합니다.
File, Directory가 공통으로 구현하고 있는 Interface의 각 함수는 다음과 같은 의미를 갖고 있습니다.
get_name: 파일 혹은 디렉토리의 이름 반환.
get_size: 파일의 크기를 반환. 디렉토리의 경우, 갖고 있는 모든 파일 혹은 디렉토리 크기의 합을 반환해야 함.
error[E0277]: the size for values of type`(dyn Node + 'static)` cannot be known at compilation time
--> src/bin/composite.rs:42:13
|
42 | childs: Vec<dyn Node>,
| ^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
=help: the trait `Sized` is not implemented for`(dyn Node + 'static)`note: required by a bound in `Vec` --> /Users/huijeongkim/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:400:16
|
400 | pub struct Vec<T, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global> { | ^ required by this bound in `Vec`For more information about this error, try `rustc --explain E0277`.
dyn Node는 compile time에 크기를 알 수 없기 때문에 Vector에 넣을 수 없다는 것 입니다. 이를 아주 간단하게 해결할 수 있는 방법은, 객체 자체가 아닌 객체를 가리키는 포인터를 Vector에 넣는 것 입니다. dyn Node가 아닌 Heap으로 옮겨 진 Box<dyn Node>의 리스트를 갖도록 수정하면 에러는 사라지고 컴파일 성공하게 됩니다.
여기서 File, Directory 를 clone 하도록 수정해 보겠습니다. #[derive(Clone)] 매크로를 사용하면 손쉽게 Clone trait을 구현할 수 있습니다. 하지만 여기서 다시 한 번 컴파일 에러를 만나게 됩니다.
123456789101112131415
error[E0277]: the trait bound `dyn Node: Clone` is not satisfied
--> src/bin/composite.rs:51:5
|
48 | #[derive(Clone)] | ----- in this derive macro expansion
...
51 | childs: Vec<Box<dyn Node>>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for`dyn Node` |
= note: required because of the requirements on the impl of `Clone`for`Box<dyn Node>`= note: 1 redundant requirement hidden= note: required because of the requirements on the impl of `Clone`for`Vec<Box<dyn Node>>`= note: this error originates in the derive macro `Clone`(in Nightly builds, run with -Z macro-backtrace for more info)For more information about this error, try `rustc --explain E0277`.
dyn Node에 대한 clone 이 구현되어 있지 않다는 것 입니다. Directory가 clone 가능하려면 그 내부의 모든 멤버도 clone 가능해야 합니다. 따라서 dyn Node의 clone을 구현해 줘야 합니다.
derive 매크로는 struct, enum, union에만 사용 가능하므로 trait Node에는 #[derive(Clone)]을 추가할 수 없습니다. 대신 다음과 같이 구현할 수 있습니다. 참고로 clone 함수의 리턴이 dyn Node가 되면 첫 번째 에러와 동일한 이유, compile time에 size를 알 수 없다는 문제로 에러가 발생하기 때문에 이 또한 Box 로 clone하도록 구현하였고, 이 때문에 Box<dyn Node>에 대한 clone 구현도 추가하였습니다.
error[E0308]: mismatched types
--> src/bin/composite.rs:10:37
|
10 | root_node.add_new_node(Box::new(new_directory));
| -------- ^^^^^^^^^^^^^ expected struct `File`, found struct `Directory` | |
| arguments to this function are incorrect
|
= note: expected struct `File` found struct `Directory<_>`note: associated function defined here
--> /Users/huijeongkim/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/boxed.rs:213:12
|
213 | pub fn new(x: T) -> Self { | ^^^
For more information about this error, try `rustc --explain E0308`.
구체 객체가 run-time에 결정되어야 하는 이런 예제에서는 generic은 적합하지 않은 것이죠. 이 문제를 해결하기 위해서 enum을 사용해 볼 수 있겠습니다.