I have the following trait defined:
pub trait TItem {
// some functions
}
I have two structs TArray and TObject with:
impl TItem for TArray {
// some functions
}
impl TItem for TObject {
// some functions
}
I would like to implement a writer like this:
impl MyWriter {
pub fn write_item(&mut self, item: &TItem) {
if item.is_array() {
write_array(item)
return
}
if item.is_object() {
write_object(item)
return
}
// do something else that's never an error
}
pub fn write_array(&mut self, item: &TArray) { /*code*/ }
pub fn write_object(&mut self, item: &TObject) { /*code*/ }
}
I can implement this type of functionality in C, C++ and CSharp fairly easily but being relatively new to Rust I've not found a way to do this and it seems like it should be easy but from my research it doesn't seem to be. How can this be done?
In idiomatic rust, an alternative to standard OOPs concepts like in C/C++ or C# is using enums
.
The example provided by you can be written using enums
as:
pub struct TArray {
// some fields ...
}
pub struct TObject {
// some fields ...
}
pub enum TItem {
Array(TArray),
Object(TObject),
}
struct MyWriter;
impl MyWriter {
pub fn write_item(&mut self, item: &TItem) {
match item {
TItem::Array(array) => self.write_array(array),
TItem::Object(object) => self.write_object(object),
}
}
pub fn write_array(&mut self, item: &TArray) {
println!("Called write array")
}
pub fn write_object(&mut self, item: &TObject) {
println!("Called write object")
}
}
fn main() {
let array_item = TItem::Array(TArray {});
MyWriter.write_item(&array_item);
}
Here the TItem
enum is equivalent to the interface
TItem
and the structs TArray
and TObject
can be considered to be the classes
implementing the interface
.
Note: While this can be much faster and easier to implement, it is not extendable from outside the crate like traits. Any new subclass (variant) has to be added to the enum directly.
Now to be more un-idiomatic and provide examples in OOPs concept, there are crate that provide downcasting functionality to rust like downcast
or downcast-rs
.
As an example, using downcast-rs
, the code would be like:
use downcast_rs::{Downcast, impl_downcast};
trait TItem: Downcast {}
impl_downcast!(TItem);
pub struct TArray {
// some fields ...
}
impl TItem for TArray {}
pub struct TObject {
// some fields ...
}
impl TItem for TObject {}
struct MyWriter;
impl MyWriter {
pub fn write_item(&mut self, item: &dyn TItem) {
if let Some(array) = item.downcast_ref() {
self.write_array(array);
} else if let Some(object) = item.downcast_ref() {
self.write_object(object);
} else {
// handle other cases
}
}
pub fn write_array(&mut self, item: &TArray) {
println!("Called write array")
}
pub fn write_object(&mut self, item: &TObject) {
println!("Called write object")
}
}
fn main() {
let array_item = TArray {};
MyWriter.write_item(&array_item);
}
You can consider the crate allowing you to use dyncamic_cast
of C++
like functionality, or the is
check of C#
Note 1: This method can be much slower than the enum
one as it has the overhead of dynamic dispatch/vtable.
Note 2: downcast-rs
is not available in the rust playground so the playground link could not be provided
Edit:
The trait
functions can be added to the enum
like so:
pub struct TArray {
// some fields ...
}
impl TArray {
fn foo(&self) {
println!("Called foo from 'TArray'")
}
fn bar(&self, param1: usize, param2: usize) -> Result<(), String> {
println!("Called bar from 'TArray'");
Ok(())
}
}
pub struct TObject {
// some fields ...
}
impl TObject {
fn foo(&self) {
println!("Called foo from 'TObject'")
}
fn bar(&self, param1: usize, param2: usize) -> Result<(), String> {
println!("Called bar from 'TObject'");
Ok(())
}
}
pub enum TItem {
Array(TArray),
Object(TObject),
}
impl TItem {
// A function `foo` which is already implemented for `TArray` and `TObject`
pub fn foo(&self) {
match self {
Self::Array(arr) => arr.foo(),
Self::Object(obj) => obj.foo()
}
}
// A function `bar` which is already implemented for `TArray` and `TObject`
pub fn bar(&self, param1: usize, param2: &str) -> Result<(), String> {
match self {
Self::Array(arr) => arr.bar(param1, param2),
Self::Object(obj) => obj.bar(param1, param2)
}
}
}
struct MyWriter;
impl MyWriter {
// ...
}
This may become very verbose for large number of enum
variants or large number of trait
(or shared) methods. The boilerplate code can be reduced by using the crate enum_dispatch
.
Using enum_dispatch
the above code can be reduced to:
use enum_dispatch::enum_dispatch;
#[enum_dispatch]
trait ItemTrait {
fn foo(&self);
fn bar(&self, param1: usize, param2: usize) -> Result<(), String>;
}
pub struct TArray {
// some fields ...
}
impl ItemTrait for TArray {
fn foo(&self) {
println!("Called foo from 'TArray'")
}
fn bar(&self, param1: usize, param2: usize) -> Result<(), String> {
println!("Called bar from 'TArray'");
Ok(())
}
}
pub struct TObject {
// some fields ...
}
impl ItemTrait for TObject {
fn foo(&self) {
println!("Called foo from 'TObject'")
}
fn bar(&self, param1: usize, param2: usize) -> Result<(), String> {
println!("Called bar from 'TObject'");
Ok(())
}
}
#[enum_dispatch(ItemTrait)]
pub enum TItem {
Array(TArray),
Object(TObject),
}
struct MyWriter;
impl MyWriter {
// ...
}
fn main() {
let item: TItem = TArray {}.into();
item.foo();
}
Note: While this uses the trait
, it does not have any of the overhead of dynamic dispatch or box redirection as it uses the trait members to create the methods in the enum similar to what was done earlier during the compilation.