rust 实例化动态对象

在功能开发中,动态创建或获取某个对象的情况很多。在前端JS开发中,可以使用工厂函数,通过给定的类型标识创建不同的对象实例;还可以通过对象映射来实现动态创建对象。

Rust中,我们也可以使用这两种方式去创建对象实例,但实现书写的方式可能略有不同;rust还可以通过序列化JSON数据时进行枚举类型匹配。

我们定义好需要测试的数据结构体、方法。小狗、小猫有自己的字段、方法,它们有相同的字段name,也有相同的方法say

rust 复制代码
use serde_derive::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug)]
struct Dog {
    name: String,
    work: String,
}
impl Dog {
    fn new(name: String, work: String) -> Dog {
        Dog { name, work }
    }
    fn say(&self) {
        println!("{} say wangwang", self.name);
    }
}

#[derive(Deserialize, Serialize, Debug)]
struct Cat {
    name: String,
    age: i32,
}

impl Cat {
    fn new(name: String) -> Cat {
        Cat { name: name, age: 0 }
    }
    fn say(&self) {
        println!("{} say miamiamia", self.name);
    }
}

序列化serde

我们在拿到JSON格式数据进行序列化时,在rust中是需要确定具体数据类型的,但是我们并不知道具体类型,因为现在有两种类型,要合为一种类型,就需要归集,使用枚举enum来定义可能的类型:

rust 复制代码
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
enum Animal {
    Dog(Dog),
    Cat(Cat),
}

对于JSON格式和rust 结构体的互相转换,可以使用serde库。也正好利用JSON转结构体这一过程,利用转换机制来实现动态创建对象。

安装相关的库:

sh 复制代码
cargo add serde serde_derive serde_json

我们定义一个JSON格式数据,使用serde库进行反序列化,并使用match进行匹配:

rust 复制代码
fn main() {
    let data = r#"
    {
        "name":"admin",
        "age":2
    }
    "#;
    let animal = serde_json::from_str(data).unwrap();
    match animal {
        Animal::Dog(dog) => {
            dog.say();
        }
        Animal::Cat(cat) => {
            cat.say();
        }
    };
}

测试运行,正常输出了cat say wangwang。我们修改JSON格式数据

rust 复制代码
let data = r#"
{
    "name":"admin",
    "work":"play"
}
"#;

测试运行,正常输出了dog say wangwang,说明没有逻辑没有问题。

待优化的地方在于我们使用了match,如果我们需要在多个地方使用animal,那么这段匹配逻辑就无处不在了。当有很多方法时,无法控制具体调用哪个方法,就需要不停的去匹配。

我们可以将它们需要调用公共方法在枚举类型Animal定义一下,内部逻辑根据不同类型在调用各自的方法。

rust 复制代码
impl Animal {
    fn say(&self) {
        match self {
            Animal::Cat(cat) => cat.say(),
            Animal::Dog(dog) => dog.say(),
        }
    }
}

Animal定义公共方法say,然后在序列化JSON数据格式时,我们必须要指定数据类型:

rust 复制代码
fn main() {
    let data = r#"
    {
        "name":"admin",
        "age":2,
        "work":"play"
    }
    "#;
    let animal: Animal = serde_json::from_str(data).unwrap();

    animal.say();
}

明确指定了animal: Animal,因为没有其他逻辑帮助rust推断出具体的类型是什么。也可以这么写let animal = serde_json::from_str::<Animal>(data).unwrap();

注意

需要注意的是:匹配的不同对象结构体的字段不能一致,否则会匹配到枚举的第一个;如果出现包含的情况,我们需要把被包含的结构体放在前面。

比如小猫也有work字段了:

rust 复制代码
#[derive(Deserialize, Serialize, Debug)]
struct Cat {
    name: String,
    age: i32,
    work: String,
}

这是我们再去匹配JSON格式数据,因为数据里有age,我们希望的是匹配小猫Cat,但是它里面完全包含了小狗的字段Dog,而且枚举Animal种小狗在前,所以会直接匹配小狗:

rust 复制代码
let data = r#"
{
    "name":"admin",
    "age":2,
    "work":"play"
}
"#;

这样达不到我们想要的结果,所以需要注意调整枚举值的顺序,可以将复杂数据结构放到前面。将Cat放到前面就可以正常工作了。

rust 复制代码
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
enum Animal {
    Cat(Cat)
    Dog(Dog),
}

动态类型匹配

上一个方式是我们拿到了具体对象的JSON数据,然后通过序列化,获取到对应的对象实例。如果我们只知道某个类型,需要根据类型初始化具体实例对象。

我们枚举实例对象的类型,定义字符串转枚举类型的方法:

rust 复制代码
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
enum AnimalType {
    Dog,
    Cat
}
impl AnimalType {
    fn str_to_animal_type(str: &str) -> AnimalType {
        match str {
            "dog" => AnimalType::Dog,
            "cat" => AnimalType::Cat,
            _ => panic!("unknown type"),
        }
    }
}

调用AnimalType获取到枚举类型,然后通过匹配类型来实例化对象,这跟上面的序列化JSON格式后续处理方式一致。

rust 复制代码
fn main() {
    let names = "dog";

    match AnimalType::str_to_animal_type(names) {
        AnimalType::Dog => {
            let dog = Dog {
                name: "admin".to_string(),
                work: "play".to_string(),
            };
            dog.say();
        }
        AnimalType::Cat => {
            let cat = Cat {
                name: "admin".to_string(),
                age: 2,
                work: "play".to_string(),
            };

            cat.say();
        }
    }
}

Trait 特质

trait是rust中特有的类型,它可以定义对象的行为,然后可以被其他对象实现。实现它的对象可以拥有相同的行为,但是可以拥有不同的内部逻辑。

这可以保证我们在动态获取到不同的对象实例,调用它们的方法时保证方法存在。在创建动态对象时,因为不知掉具体大小,需要使用Box<dyn Trait>定义动态对象。

rust 复制代码
trait AnimalTrait {
    fn say(&self);
}

然后在各个类型中实现AnimalTrait,并实现公共方法say

rust 复制代码
impl AnimalTrait for Dog {
    fn say(&self) {
        println!("{} say wangwang", self.name);
    }
}

impl AnimalTrait for Cat {
    fn say(&self) {
        println!("{} say miamiamia", self.name);
    }
}

定义类型都实现AnimalTrait的方法,就可以放心的使用Box<dyn AnimalTrait>提供的动态对象了。

rust 复制代码
impl AnimalType {
    fn str_to_animal(str: &str) -> Box<dyn AnimalTrait> {
        match str {
            "dog" => Box::new(Dog::new("admin".to_string(), "play".to_string())),
            "cat" => Box::new(Cat::new("test".to_string())),
            _ => panic!("unknown type"),
        }
    }
}

方法str_to_animal通过类型匹配获取到对应的实例对象,现在我们不需要再匹配里直接调用方法了。我们拿到动态对象,想调用那个方法就用哪个。

rust 复制代码
fn main() {
    let names = "dog";
    let animal = AnimalType::str_to_animal(names);
    animal.say();
}

这样就很方便的进行动态对象的传递,我们不需要关心该调用哪个方法,是否需要导入指定的方法。rust通过Box<dyn AnimalTrait>会自动调用合适的实现。

From/Into 类型强转

我们定义了AnimalTrait规范了动态对象的行为,它们在实现了AnimalTrait后,就可以根据动态对象调用它的公共方法了。

但在根据类型创建动态对象时,仍然定义了枚举AnimalType的方法str_to_animal并调用从而匹配到对应的动态对象。

我们还可以使用Fromtrait,通过让AnimalTrait实现Fromtrait,从而直接使用into方法让字符串类型转为动态对象。

rust 复制代码
impl From<&str> for Box<dyn AnimalTrait> {
    fn from(value: &str) -> Self {
        match value {
            "dog" => Box::new(Dog::new("admin".to_string(), "play".to_string())),
            "cat" => Box::new(Cat::new("test".to_string())),
            _ => panic!("unknown type"),
        }
    }
}

这样的实现可以减少在创建动态对象时的显示函数调用,我们在使用的时候直接调用into()方法即可:

rust 复制代码
fn main{
    let dog: Box<dyn AnimalTrait> = "dog".into();
    dog.say();
}

HashMap映射类型

以上实现方案难免都使用了match进行匹配,而我们在之前说的映射对象的实现,则可以避免match的匹配。

通过HashMap初始化类型映射结构体对象,在使用时通过自定义方法get传入指定的类型,得到动态类型。

rust 复制代码
struct AnimalFactory {
    map: HashMap<String, Box<dyn Fn() -> Box<dyn AnimalTrait>>>,
}

我们定义了一个结构体AnimalFactory,其中包含一个HashMap类型的字段map,用于存储类型与创建函数的映射关系。

注意到HashMap的值是一个闭包函数而不是直接动态类型,如果直接定义HashMap<String, Box<dyn AnimalTrait>>,我们在初始化时就必须实例化创建对象实例,这就导致具体对象的实例只有一个而避免不了处理所有权的问题。如果我们需要传递所有权,就必须使用Arc了。

定义了工厂结构体AnimalFactory,定义初始化函数new:

rust 复制代码
impl AnimalFactory {
    fn new() -> Self {
        map.insert(
            "dog".to_string(),
            Box::new(|| {
                Box::new(Dog::new("admin".to_string(), "play".to_string())) as Box<dyn AnimalTrait>
            }) as Box<dyn Fn() -> Box<dyn AnimalTrait>>,
        );
        map.insert(
            "cat".to_string(),
            Box::new(|| Box::new(Cat::new("test".to_string()))),
        );

        AnimalFactory { map }
    }
}

由于HashMap需要定义具体的类型,我们在插入类型Dog时无法匹配定义的Box<dyn Fn() -> Box<dyn AnimalTrait>>导致报错,这就需要我们手动强转类型。

为了简化类型书写,我们定义一个类型替代:

rust 复制代码
type AnimalDynType = Box<dyn Fn() -> Box<dyn AnimalTrait>>;

我们已经初始化了映射表,定义根据具体类型获取动态对象的方法:

rust 复制代码
impl AnimalFactory {
    fn get(&self, name: &str) -> Box<dyn AnimalTrait> {
        match self.map.get(name) {
            Some(create_fn) => create_fn(),
            None => panic!("not found"),
        }
    }
}

在使用时,首先创建一个AnimalFactory对象,然后调用get方法,传入具体的类型名称,即可获取对应的动态对象。

rust 复制代码
fn main() {
    let animal = AnimalFactory::new();

    let dog = animal.get_animal("dog");
    dog.say();
}

最后

这几种实现方式都有一定的使用场景,根据实际需求选择合适的方式。

往期关联文章:

rust 集合、错误处理、泛型、Trait、生命周期、包

并发线程间的数据共享

相关推荐
我是苏苏20 分钟前
C#基础:使用Linq进行简单去重处理(DinstinctBy/反射)
开发语言·c#·linq
小小码农(找工作版)22 分钟前
C#前端开发面试题
开发语言·c#
不爱学英文的码字机器29 分钟前
Python爬虫实战:从零到一构建数据采集系统
开发语言·爬虫·python
我是哈哈hh29 分钟前
【JavaScript进阶】作用域&解构&箭头函数
开发语言·前端·javascript·html
鹿鸣悠悠34 分钟前
Python 类和对象详解
开发语言·python
laocooon52385788639 分钟前
用Python实现的双向链表类,包含了头插、尾插、归并排序等功能
开发语言·python
一只哒布刘1 小时前
第六次作业
开发语言·php
圣心1 小时前
Ollama 快速入门
开发语言·javascript·人工智能
小林熬夜学编程2 小时前
【MySQL】第八弹---全面解析数据库表的增删改查操作:从创建到检索、排序与分页
linux·开发语言·数据库·mysql·算法
汇匠源2 小时前
零工市场小程序利用 Java 连接企业与自由职业者?
java·开发语言