Return null instead of Object in by trait obligated function

Hello

I have an application to manage cats. I can add cats to the system, delete them, view their info,...
My application is able to save these cats in memory or in a file. Because I used traits I can very easily choose if my application needs to save the info of the cats in memory or in a file.

Now I also want to be able to store the info of my cats in a MySQL Database. But a database of course requires more configuration than a memory-based or file-based system. My application needs to know the name of the database, username and password required to gain authentication.

But this is the problem I'm encountering:
Because memory- and file-based datastoring doesn't need much configuration a constructor-like function new() without parameters sufficed to initialize the structs FileData and MemoryData. This new()-function gets inherited from the generic trait.

But for MySQLData I'd like my new()-function to have parameters, like this: new(database_name : String, database_user : String, database_pass : String). However this is not allowed because new() is defined without parameters in the trait.

I was thinking of just overriding the required new()-function by simply adding a new()-function with parameters and let the other obligated new()-function without parameters return null. I got close but I am not able to let the new() function return null. Because it needs to return a MySQLData-object.

So basically:
How can I let a function with this signature: fn new() -> MySQLData
return a kind of null/nullptr/None so I don't have to implement it?

I wrote a small piece of pseudo-code to visualize my problem better.
Code:

//General interface for all the datamanagers
pub trait DataManager {
    //Constructor
    fn new() -> Self; 

    //Some basic functions to add/delete/... objects
    fn add(&mut self, Object) -> bool;
    fn delete(&mut self, ObjectId : u16) -> bool;
    //...
}

//Keep objects stored in memory and manipulate them there
struct MemoryData {
    //...
}
impl DataManager for MemoryData {
    fn new() -> MemoryData {
        MemoryData {
            //...
        }
    }
}

//Keep objects stored in a file and manipulate them there
struct FileData {
    //Same as MemoryData
    //...
}
impl DataManager for MemoryData {
    //Same as MemoryData
    //...
}

//CODE FOR QUESTION STARTS HERE!!!!!!!!!
//Now I want to be able to also store objects in a database

extern crate mysql;

struct MySQLData {
    conn : mysql::Pool, //Property holding the connection of the database
}
impl MySQLData {
    //Function to initialize connection to MySQL database, returing mysql::Pool (Pooled connection)
    pub fn init_connection(db_name : &String, db_user : &String, db_pass : &String) -> mysql::Pool {
        //calling mysql::Pool::new() etc...
        //Making connection using given parameters
        //...
        
        mysql_pool
    }

    //Function to create the tables needed for this application in the
    //database if they don't already exist
    fn init_tables(&mut self) {
        //Executing few 'create table if not exists' queries
        //...
    }

    //Trying to override the new() function obligated by DataManager
    fn new(database_name : &String, database_user : &String, database_pass : &String) -> MySQLData {
        let mysql = MySQLData {
            conn: MySQLData::init_connection(&String::from("catdatabase"), &String::from("root"), &String::from("toor")).unwrap(),
        };

        mysql.init_tables();
        mysql
        
    }
}

impl DataManager for MySQLData {
    //This is the new() function obligated by DataManager
    //However, I don't want to implement it! I want it to
    //return a nullptr or an error when this one is used
    fn new() -> MySQLData {
        
    }
}

//Using the DataManagers
fn main() {
    //Here I want to intialize a DataManager,
    //because of the trait I am able to let my application switch between
    //storing the data in memory (MemoryData) or in a file (FileData)
    //by editing only this variable.
    
    //Works: let ref mut manager = data_layer::MemoryData::new();
    //Works: let ref mut manager = data_layer::FileData::new();

    //What I want to do now:
    //let ref mut manager = data_layer::MySQLData::new(); Should give some kind of error or do
    //nothing

    //I want this to work:
    let ref mut manager = data_layer::MySQLData::new("database_name", "username", "password");

    //I can execute the options from DataManager
    manager.add(...);
    manager.delete(...);
    //...
}

Possible solutions I thought of myself but didn't like:

  1. Implementing both functions, and use default values in the function without parameters. But than I'd have almost the same code twice... Which I don't like.
  2. Changing the signature of new() -> Self, to new() -> Option or something close so I can return None in the obligated new() function. But than I'd have to change to much code in my application now (unwrapping etc).
  3. The ideal solution would be to be not implement the by the trait obligated new()-function but let it just return null.

Thanks!

It sounds like the new function isn't really part of a common interface shared by all your types. I would simply remove it from your DataManager trait, and have each type implement its own new function with a different signature.

1 Like

Agreed. Constructors, as a general rule, do not belong in your trait. (Exception: the Default trait, conversion traits like From)

3 Likes

Thanks. That solution worked fine!

Also, note that one of the beautiful things about rust is that it doesn't have a null value for arbitrary types. The only way you could do this is by specifying a return value for the trait-required function that allows for empty values, namely Option<Self>. Then you could trivially implement the trait function for your type with a body of None.

But if the return type is Self, you either have to return a value of type Self, or diverge. Diverge is fancy-talk for "don't return normally." Your options then are to panic, loop infinitely, or invoke undefined behavior with something like unsafe { unreachable_unchecked!() }. (NEVER INVOKE UNDEFINED BEHAVIOR).

1 Like