Handling multiple types which share a non-object-safe trait

A typical approach is to write a different, object-safe trait, that offers all the functionality you actually need, implement that trait generically for all T: OriginalNonObjectSafeTrait, and make an object of that trait then.

Like for example, if all you need to be able to is to call serde_yaml::to_string, you can make a trait that hasonly a fn(&self) -> Result<String, serde_yaml::Error> method with that functionality and is object safe.

use serde::Serialize;
use serde_yaml;

trait SerializeYamlString {
    fn serde_yaml_to_string(&self) -> Result<String, serde_yaml::Error>;
}
impl<T: Serialize> SerializeYamlString for T {
    fn serde_yaml_to_string(&self) -> Result<String, serde_yaml::Error> {
        serde_yaml::to_string(self)
    }
}

use k8s_openapi::api::core::v1::{Pod, Service};

fn main() {
    make_resources()
        .iter()
        .for_each(|r| println!("{}", r.serde_yaml_to_string().unwrap()));
}

fn make_resources() -> Vec<Box<dyn SerializeYamlString>> {
    let a = Pod { ..Pod::default() };
    let b = Service {
        ..Service::default()
    };
    vec![a, b] // <- TODO, probably needs some `Box::new` wrapping
}

Sometimes, such a object-safe helper trait can even offer (essentially) the full functionality of the original trait, perhaps just a little less efficiently. E.g. if your ordinary trait has a generic method fn foo_with_callback<F: FnOnce(u8)>(&self, x: F)>, then you could make an object safe trait with a method fn foo_with_dyn_callback<'a>(&self, x: Box<dyn FnOnce(u8) + 'a>) to capture essentially the full API of foo_with_callback, but less efficiently, as new trait objects are involved.


trait OriginalNonObjectSafeTrait {
    fn foo_with_callback<F: FnOnce(u8)>(&self, x: F);
}

trait WrappingObjectSafeTrait {
    fn foo_with_dyn_callback<'a>(&self, x: Box<dyn FnOnce(u8) + 'a>);
}

impl<T: OriginalNonObjectSafeTrait> WrappingObjectSafeTrait for T {
    fn foo_with_dyn_callback<'a>(&self, x: Box<dyn FnOnce(u8) + 'a>) {
        self.foo_with_callback(x)
    }
}

// for convenience we can even go back e.g. for a `Box<dyn …>`
impl OriginalNonObjectSafeTrait for Box<dyn WrappingObjectSafeTrait> {
    fn foo_with_callback<F: FnOnce(u8)>(&self, x: F) {
        (&**self).foo_with_dyn_callback(Box::new(x))
    }
}

In the case of serde::Serialize, there’s also pre-existing object safe traits of this kind, you can find in erased-serde.

(Compared to above approach, that features some improvements, such as avoiding introduction of Boxes by using &mut … references; and implementing the original serde::Serialize for the dyn erased_serde::Serialize type itself. Also for Serialize, the approach had to be taken in multiple steps, recursively also providing object-safe versions of all traits that were used for generic parameters.)

10 Likes