Constructor question

Hi all,
I'm a Rust newbie so go easy! I'm currently reading through classes and instances in Rust and I want to include a check in a class constructor to ensure that a constructor argument is non-negative. That's all pretty straightforward (although there are probably some more elegant ways of doing than I have done), but my question is how to handle a negative number being passed to the constructor. I don't want to assign a default value, so I guess I need to return an error. What would folks recommend when it comes to handling errors and exceptions in Rust? Is there a pattern or standard way of doing so, or is it just the same as it is in Java i.e. throw an exception?

Thanks in advance,

Angus

There's a few things I want to clarify:

  • Constructors in Rust are kind of a fuzzy line; there's the constructor syntax functions which use the constructor syntax which are colloquially called the constructor function(s). I will assume you're talking about a constructor function.
  • There are no classes in Rust; there's only structs which do not implicitly live under a reference like a class does.

You can restrict the behaviour in a few ways:

  • You can return an Option<Self>:
    struct Foo(number: isize);
    impl Foo {
        fn new(x: isize) -> Option<Foo> {
            if x < 0 {
                None
            else {
                Some( Foo(x) )
            }
        }
    }
    
  • Make your numeric type not support negative values. All of the following are unsigned and therefore can't represent unsigned values: u8, u16, u32, u64, u128, usize.
  • In the case it is an unrecoverable error not meant for your logic to survive, then panic!.
    Note that panics may seem like exceptions but exceptions can be caught, while conventional Rust prefers you don't do that. Also, panics can be configured to result in aborting the process, and therefore become unrecoverable.

Definitely, an Option or a Result is the correct way to go, and then your next choice would be to restrict the numerical type, and then resort to panicking.

I generally extend the meaning to include where the caller should never be sending such invalid values.
assert! is common way to check and have it perform the panic.

Thanks for your replies, they've been very useful. As a matter of interest do you two work professionally as Rust devs? If so, what sort of products do you develop?

Thanks

Angus

I don't work as a Rust developer professionally, although I plan on doing so at some point.

For now, dedicating most spare time to Rust has gotten me pretty far though!

Going back to the OP, here's what I've implemented following your reply. Any other suggestions?

struct Person {

    age: u32

}

impl Person{

    fn new(initial_Age: u32) -> Option<Person> {

        if initial_Age < 0 {

            None

        }else {

            Some (Person { age: initial_Age})

        }

    }

    fn amIOld(&self) {

    }

    fn yearPasses(&mut self){

    }

}

The next thing is - how do I create an instance from this?

Regards

Angus

i suggest you fix the code blocks.

```
// your code
```
1 Like

You already have! :slightly_smiling_face:

Rust doesn't really have "constructors" in the Python/Java sense. You create an instance of some struct by naming it and populating its fields (e.g. Person { age: initial_Age }).

So "constructors" in rust are just a conventional name for functions that return a struct literal.

The idiomatic way you'd do this in Rust is by using the type system instead of runtime checks. A common saying you'll hear is Making illegal states unrepresentable...

https://twitter.com/jdegoes/status/1089949149628375040?lang=en

For example, if you want to make sure your function isn't passed a negative integer, choose an unsigned integer type (e.g. u32). This makes it impossible to ever get into a situation where your function is given an invalid value, because -2 isn't a valid u32.

1 Like

Hi Michael,
I'm from a Java background so I'm currently scratching my head and thinking 'Where in the hell have I created a new instance?' :smiley:

What I was attempting is this in main.rs

let mut p = Some(Person::new(age));

Not sure if that's correct syntax btw.

Regards,

Angus

Note that since your new function takes an u32 argument, when you compile it you get the following warning:

warning: comparison is useless due to type limits
 --> src/lib.rs:7:6
  |
7 |   if initial_age < 0 {
  |      ^^^^^^^^^^^^^^^

An unsigned u32 value cannot be less than zero, so you, possibly without noticing, implemented what @Michael-F-Bryan writes about above, and the function can very well return just Person instead of Option<Person>.

1 Like

The official term for the bit of source code corresponding to where an instance is constructed is called a struct expression. This is an expression that contains your type's name (e.g. Person) an opening {, the value for fields, then a closing }.

Think of Person::new as just a normal function that returns a value of type Person. Because there's no such thing as inheritance in Rust, the creation of values isn't actually an important process any more. You just choose a spot to place the variable, then copy across the initial value for each field. For example, the expression, Person { age: some_age }, would make sure there's enough space on the stack for a Person, then copy the some_age variable's value onto wherever the Person's age field is. "Creating" an instance of some type is just a case of populating its fields.

That's completely different from Java where you'd first allocate memory for your object, then walk down the inheritance tree calling each class's constructor and initialising that class's fields until you reach the constructor for your child class.

1 Like

I think some expansion on the points other people are making here is warranted:

First, unlike Java, Rust doesn't have a compiler-supported constructor method - we create a function fn new by convention, which acts as a constructor, but it isn't special, the way it is in Java. As @Michael-F-Bryan points out, this is because Rust doesn't have inheritance, and so we don't need to construct any superclass' fields or method tables - there aren't any to construct! As Michael says, the Person { [...] } syntax is the construction syntax in Rust - it's the 'special' syntax the compiler understands.

The reasons we don't use that syntax everywhere are: it isn't able to enforce more invariants (like an age restriction) than the types of the fields, and it exposes the implementation details of the struct. Fortunately, if the struct's fields aren't public (indicated in rust with pub), then outside of the module (sorta like a Java namespace), the fields won't be visible, and so it will be impossible to use the construction syntax to fill them in.

Second, the other key point to understand is that in Rust, creating a struct does not imply an allocation of memory on the heap; unlike Java, all structs are created on the stack by default, and you have to put them on the heap yourself (usually with a standard library type like Box, Rc, or for a collection, Vec). This means creating and destroying objects is usually faster, but it's important to figure out over time what is large enough to be 'worth' putting on the heap, or when it's necessary for other reasons. Fortunately, the compiler has error messages that are pretty helpful with this!

Lastly, I wanted to clarify - Java only has signed integer types; byte for 8-bit, short for 16-bit, int for 32-bit, long for 64-bit. Those types are spelled i8,i16, i32, and i64 in Rust; we also have unsigned integer types, that only include non-negative numbers (but of twice the size), which are u8, u16, u32, and u64. There are also the larger, less-common i128 and u128, and there are usize and isize, which are used for indexing collections, and are the size of a pointer on that system.

I mention all this because as someone pointed out above, if you're using the uXX types, a negative number is impossible - the compiler will not compile it as a constant, and if you subtract a larger number from a smaller one, in debug, the program will panic, and in release the program will underflow (and you'll have a huge, positive number).

Hopefully this helps!
Welcome to the community!

2 Likes

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