Generic type (Any) - how to improve?

Hello. How can this code be improved?

I often have to repeat this piece of code:

match self
{
   DbVar::U8(var_type) => {  
   // any code...  
   },
   DbVar::BOOL(var_type)  => { 
   // any code... 
   },
   _  => { panic!("not supported type") },
};

How can it be improved?

Code:

use core::any::Any;
use core::fmt::Display;
 
use serde_derive::{Serialize, Deserialize};

pub type GenericVarType = Box<dyn Any + Send + Sync>;

//Generic variable
#[derive(Debug)]
pub struct GenericVar<T: Any + Display>
{
    pub name: String,
    pub value: T,
    // any fields...
}

//Generic variable as enum
#[derive(Debug)]
pub enum DbVar
{
    BOOL(GenericVarType),
    U8(GenericVarType),
    U16(GenericVarType),
    U32(GenericVarType),
    U64(GenericVarType),
    I8(GenericVarType),
    I16(GenericVarType),
    I32(GenericVarType),
    I64(GenericVarType),
    F32(GenericVarType),
    F64(GenericVarType),
    STRING(GenericVarType),
}

impl DbVar
{   
    fn set_value<T: Any + Display>(&mut self, new_value: T)
    {
        match self
        {
            DbVar::U8(var_type) => { 
                let var = var_type.downcast_mut::<GenericVar<T>>().expect("Cannot cast to U8");
                var.value = new_value;
            },
            _  => { panic!("not supported type") },
        };
    }
    
    pub fn to_json(&self) -> String
    {
        #[derive(Serialize, Deserialize)]
        struct VarJson {
            name: String,
            value: String,
        };
        
        fn convert<T: Any + Display>(var: &GenericVar<T>) -> VarJson
        {
            let value = var.value.to_string();
            VarJson{name: var.name.clone(), value}
        }
        
        let var_json = match self
        {
            DbVar::U8(var_type) => { 
                let var = var_type.downcast_ref::<GenericVar<u8>>().expect("Cannot cast to U8");
                convert(var)
            },
            DbVar::BOOL(var_type)  => { 
                let var = var_type.downcast_ref::<GenericVar<bool>>().expect("Cannot cast to U8");
                convert(var)
            },
            _  => { panic!("not supported type") },
        };
        
        return serde_json::to_string(&var_json).expect("Cannot convert to JSON");
    }
    
    pub fn do_something(&self)
    {
        match self
        {
            DbVar::U8(_var_type) => { 
                //...any code
            },
            DbVar::BOOL(_var_type)  => { 
              //..any code
            },
            _  => { panic!("not supported type") },
        };
        
    }
}

pub type DbVarVec = Vec<DbVar>;

fn main() 
{
    let mut var1 = DbVar::U8(Box::new(GenericVar::<u8>{name: "var1".to_string(), value: 123}));
    let var2 =  DbVar::BOOL(Box::new(GenericVar::<bool>{name: "var2".to_string(), value: true}));
    
    var1.set_value(243 as u8);
    
    let mut db = DbVarVec::new();
    db.push(var1);
    db.push(var2);
    
    for elm in db.iter()
    {
        println!("element: {:?}", elm);
        println!("json: {}", elm.to_json());
    }

}

Playground

It feels like half your problems come because you use a Box<dyn Any> for each variant and then have to go through the hassle of downcasting it. Why don't you use the type directly?

#[derive(Debug)]
pub enum DbVar
{
    BOOL(bool),
    U8(u8),
    U16(u16),
    U32(u32),
    U64(u64),
    I8(i8),
    I16(i16),
    I32(i32),
    I64(i64),
    F32(f32),
    F64(f64),
    STRING(String),
}

Or you could drop the DbVar enum altogether and just pass around a Box<dyn Any>. That would get rid of the match+downcast code.

I'd also suggest implementing From for the various types. That way you can call into() and use the normal conversion traits to simplify things.

Not using GenericVarType (Box<dyn Any>) for the variant contents also lets you #[derive(Serialize, Deserialize)] and get exactly what you expect.

#[derive(Debug, Serialize, Deserialize)]
pub enum DbVar
{
  BOOL(bool),
  STRING(String),
  ...
}

impl From<bool> for DbVar {
  fn from(b: bool) -> Self { DbVar::BOOL(b) }
}

impl From<String> for DbVar {
  fn from(s: String) -> Self { DbVar::STRING(s) }
}

impl DbVar
{   
  fn set_value<T: Into<DbVar>>(&mut self, new_value: T)
  {
    *self = new_value.into();
  }

  fn to_json(&self) -> String {
    serde_json::to_string(self).unwrap()
  }
}
2 Likes

Thx for reply.
Your solution does not have a - name field:

#[derive(Debug)]
pub struct GenericVar<T: Any + Display>
{
    pub name: String,
    pub value: T,
    // any fields...
}

In a real solution, I have a few other fields.
New solution:


use serde_derive::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
pub enum DbValue
{
  BOOL(bool),
  U8(u8),
  STRING(String),
}

impl From<bool> for DbValue {
  fn from(b: bool) -> Self { DbValue::BOOL(b) }
}

impl From<String> for DbValue {
  fn from(s: String) -> Self { DbValue::STRING(s) }
}

impl DbValue
{   
  fn set_value<T: Into<DbValue>>(&mut self, new_value: T)
  {
    *self = new_value.into();
  }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct GenericVar
{
    pub name: String,
    pub value: DbValue,
    // any fields...
}

pub type GenericVarVec = Vec<GenericVar>;

fn main() 
{
    let var1 = GenericVar{name: "var1".to_string(), value: DbValue::BOOL(true)};
    
    let mut db = GenericVarVec::new();
    db.push(var1);

    for elm in db.iter()
    {
        println!("element: {:?}", elm);
        let json = serde_json::to_string(elm).unwrap();
        println!("json: {}", json);
    }

}

Will such a solution be good?

What is the GenericVar representing? If it is just a named value then how about something like this?

/// A named value (previously called `GenericVar`).
pub struct Variable {
  pub name: String,
  pub value: Value,
  ...
}

/// All values that can be represented (was called `DbVar` in the 
/// previous example).
#[derive(Debug)]
pub enum Value {
  Boolean(bool),
  String(String),
  U8(u8),
  ...
}

If your GenericVar can have different types at runtime then it shouldn't be using compile-time generics (the T: Any + Display bit).

It looks like your use case has a well-known finite set of types, so you shouldn't need to reach for Any at all.

I dunno. Does it let you do what you wanted?

This solution meets my expectations (at the moment). :wink:

The main purpose is to store variables (GenericVar) together with values (DbValue). The values are to be generic (of various types - u8, bool ... etc.).