Little things I love about Rust: atomic object creation

In Rust there are no half-initialized structs. I miss it sooo much in other languages I have to work with (hence this quick post).

A struct either exists or not

This line:

let foo = Foo {
   a,
   b
};

just creates Foo. From this line onward foo exists. Before that line it didn’t - only parts a and b existed. A “constructor” in Rust is just any method that has such line somewhere inside it and returns the created object.

It is easy to overlook, but it makes a huge difference in how people actually write code.

In a language like Java or C++ constructor starts with some 0-initialized (or somewhat uninitialized) object, and has to “fill in the blanks”. This lures developers into writing helper methods that work on partially initialized-object, mutating it until it is “good to go”.

What if there’s an exception? Is this object fully created or not? Should the destructor run or not? Were all the parts properly destructed? Was the code written in a way that handles all the ways the object might have been half-initialized?

Complications like C++'s member initializer lists have to invented to avoid half-initialized state - forcing developers to write weird initialization code.

Sure, it can be written correctly in other languages… but it is just sooo much harder to do in practice. And after working in Rust, you begin to see the potential pitfalls and lack of robustness everywhere.

In Rust? You write regular code. You can call any functions, handle errors etc. until you have all the parts and are ready to constructor the end struct. It’s always clear when the destructor will run, and when it won’t.

15 Likes

I’d like to add that constructors for tuple structs and enums tuple variants are actual functions:

struct Foo(String, usize, isize);
enum Bar<T> {
    A(T),
    B(T, T)
}
fn main() {
    let x: fn(String, usize, isize) -> Foo = Foo;
    let y: fn(String, String) -> Bar<String> = Bar::B;
    let z: fn(Vec<String>) -> Bar<Vec<String>> = Bar::A;
}

[Playground]

3 Likes

Never heard of that. Is there any practical use for it?

Such a constructor can be used in any context where a regular function is expected:

let xs = vec![1,2,3];
let ys : Vec<_> = xs.iter().map(Bar::A).collect();
6 Likes

Rust does not really protect you against putting code that occasionally could fail into your Constructor
as seen in the Learning Project
Error Handling in the Constructor
but the Compiler would not accept Code without correct Error Handling
like:

struct User {
    name: String
}
impl User {
   fn create_from_file() -> User {
       let mut username = std::fs::read_to_string("user.txt")?;
     
       User { name: username }
   }
}

Code like this or similar which is permitted in many other Languages and actually exists in many Applications would not even compile with Rust.

    error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
      --> src/user.rs:20:26
       |
    20 |       let mut username = std::fs::read_to_string("user.txt")?;
       |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `User`
       |
       = help: the trait `std::ops::Try` is not implemented for `User`
       = note: required by `std::ops::Try::from_error`
  
 error: aborting due to previous error

I think this is the interesting thing about Rust.
It teaches you to build robust Applications.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.