SoFunction
Updated on 2025-04-08

Detailed explanation of the example of instantiating dynamic objects in Rust

In functional development, there are many cases where an object is created or obtained dynamically. In front-end JS development, you can use factory functions to create different object instances through a given type identification; you can also create objects dynamically through object mapping.

existRustIn this article, we can also use these two methods to create object instances, but the implementation of writing may be slightly different;rustIt can also be serializedJSONEnumeration type matching is performed when data is performed.

We define the data structure and method that needs to be tested. Puppies and kittens have their own fields and methods, they have the same fieldsname, there is also the same methodsay

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", );
    }
}

#[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", );
    }
}

Serialize serde

We're getting itJSONWhen the format data is serialized,rustThe specific data type needs to be determined, but we do not know the specific type, because there are two types now. To combine into one type, we need to collect and use enumeration.enumTo define possible types:

#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
enum Animal {
    Dog(Dog),
    Cat(Cat),
}

forJSONFormat andrustThe mutual conversion of structures can be usedserdelibrary. Just use itJSONThe process of transforming structures uses the transformation mechanism to achieve dynamic creation of objects.

Install related libraries:

cargo add serde serde_derive serde_json

We define a JSON format data, usingserdeThe library is deserialized and usedmatchMaking a match:

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

The test runs and the output is normalcat say wangwang. We modify the JSON format data

let data = r#"
{
    "name":"admin",
    "work":"play"
}
"#;

The test runs and the output is normaldog say wangwang, it means there is no logic and no problem.

What is to be optimized is that we use itmatch, if we need to use it in multiple placesanimal, then this matching logic is everywhere. When there are many methods, you cannot control which method to call, so you need to constantly match it.

We can put them in the enumeration typeAnimalDefine that internal logic calls their own methods according to different types.

impl Animal {
    fn say(&self) {
        match self {
            Animal::Cat(cat) => (),
            Animal::Dog(dog) => (),
        }
    }
}

forAnimalDefine public methodssay, and then when serializing the JSON data format, we must specify the data type:

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

    ();
}

It has been clearly specifiedanimal: Animal, because there is no other logic to help rust infer what the specific type is. It can also be writtenlet animal = serde_json::from_str::<Animal>(data).unwrap();

Notice

It should be noted that the fields of the matching different object structures cannot be consistent, otherwise the first one of the enumeration will be matched; if there is a case of inclusion, we need to put the included structure in front.

For example, there are kittens, tooworkFields are here:

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

This is how we match JSON format data, because there isage, what we want is to match the kittenCat, but it contains all the puppy fieldsDog, and enumerationAnimalBreeding puppies is the first, so they will match puppies directly:

let data = r#"
{
    "name":"admin",
    "age":2,
    "work":"play"
}
"#;

This cannot achieve the result we want, so we need to pay attention to adjusting the order of enum values ​​and putting complex data structures in front. WillCatPut it in front and it will work normally.

#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
enum Animal {
    Cat(Cat)
    Dog(Dog),
}

Dynamic type matching

The previous method is that we get the JSON data of the specific object, and then obtain the corresponding object instance through serialization. If we only know a certain type, we need to initialize the specific instance object according to the type.

We enumerate the type of the instance object and define the method of converting string to enumeration type:

#[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"),
        }
    }
}

CallAnimalTypeGet the enum type and instantiate the object by matching the type, which is the serialization aboveJSONThe format subsequent processing method is consistent.

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(),
            };
            ();
        }
        AnimalType::Cat => {
            let cat = Cat {
                name: "admin".to_string(),
                age: 2,
                work: "play".to_string(),
            };

            ();
        }
    }
}

Trait trait

traitIt is a type unique to rust, which can define the behavior of an object and can then be implemented by other objects. The object that implements it can have the same behavior, but can have different internal logic.

This ensures that we dynamically obtain different object instances and ensure that the methods exist when calling their methods. When creating a dynamic object, you need to use it because you don’t know the specific size.Box<dyn Trait>Define dynamic objects.

trait AnimalTrait {
    fn say(&self);
}

Then implement it in various typesAnimalTraitand implement public methodssay

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

Definition types are implementedAnimalTraitYou can use it with confidenceBox<dyn AnimalTrait>The dynamic object is provided.

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"),
        }
    }
}

methodstr_to_animalThe corresponding instance object is obtained through type matching. Now we no longer need to call the method directly in the match. We get the dynamic object and use which method we want to call.

fn main() {
    let names = "dog";
    let animal = AnimalType::str_to_animal(names);
    ();
}

This makes it very convenient to pass dynamic objects. We don’t need to care which method to call or whether we need to import the specified method. rust byBox<dyn AnimalTrait>The appropriate implementation will be called automatically.

From/Into type strong rotation

We definedAnimalTraitStandardize the behavior of dynamic objects, and they are implementing themAnimalTraitAfter that, it can call its public method based on the dynamic object.

But when creating dynamic objects based on type, enums are still definedAnimalTypeMethodstr_to_animaland call to match the corresponding dynamic object.

We can also useFromtrait, by letAnimalTraitaccomplishFromtrait, so use it directlyintoMethod to convert string type into dynamic object.

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"),
        }
    }
}

This implementation can reduce the display function calls when creating dynamic objects, which we call directly when usinginto()Method:

fn main{
    let dog: Box<dyn AnimalTrait> = "dog".into();
    ();
}

HashMap mapping type

All the above implementation solutions are inevitably usedmatchMaking matches, and the implementation of mapping objects we mentioned earlier can avoidmatchmatch.

passHashMapInitialize type map structure object, through custom methods when usinggetPass in the specified type to get the dynamic type.

struct AnimalFactory {
    map: HashMap<String, Box<dyn Fn() -> Box<dyn AnimalTrait>>>,
}

We define a structureAnimalFactory, which contains oneHashMapFields of typemap, used to store the mapping relationship between types and creation functions.

NotedHashMapThe value of the closure function is not a direct dynamic type, if defined directlyHashMap<String, Box<dyn AnimalTrait>>,We must instantiate the object instance when initializing, which leads to only one instance of the specific object, and the issue of ownership cannot be avoided. If we need to pass ownership, we must useArcNow.

Defines the factory structureAnimalFactory, define the initialization functionnew:

impl AnimalFactory {
    fn new() -> Self {
        (
            "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>>,
        );
        (
            "cat".to_string(),
            Box::new(|| Box::new(Cat::new("test".to_string()))),
        );

        AnimalFactory { map }
    }
}

becauseHashMapWe need to define a specific type, we are inserting the typeDogUnable to match the definedBox<dyn Fn() -> Box<dyn AnimalTrait>>This leads to an error, which requires us to manually force the type.

To simplify type writing, we define a type substitution:

type AnimalDynType = Box<dyn Fn() -> Box<dyn AnimalTrait>>;

We have initialized the mapping table and defined a method to obtain dynamic objects based on specific types:

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

When using it, first create an AnimalFactory object, then call the get method, pass in the specific type name, and get the corresponding dynamic object.

fn main() {
    let animal = AnimalFactory::new();

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

at last

These implementation methods have certain usage scenarios, and choose the appropriate method according to actual needs.

This is the end of this article about the detailed explanation of the examples of instantiating dynamic objects in Rust. For more relevant content on instantiating dynamic objects in Rust, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!