Constructors & static function in Rust
Amit Nadiger
Polyglot(Rust??, Move, C++, C, Kotlin, Java) Blockchain, Polkadot, UTXO, Substrate, Sui, Aptos, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Cas, Engineering management.
Constructors play a vital role in object initialization and provide a convenient way to create instances of a struct with specific initial values. In Rust, we can implement constructors by defining a static function called new on the struct. In this article, we will explore constructors in Rust and learn how to implement the new function for struct initialization.
Overview of Constructors in Rust:
In Rust, constructors are not built-in language features like in some other programming languages like C++,Jaba ,Kotlin , etc . Instead, developers conventionally use a static function named new to create instances of a struct with desired initial values. The new function acts as a factory method and allows for controlled initialization of struct instances.
Syntax and Implementation:
To implement a constructor using the new function, follow these steps:
a. Define a struct with its fields:
struct MyStruct {
? ? field1: Type1,
? ? field2: Type2,
? ? // ...
}
b. Implement the new function on the struct:
impl MyStruct {
pub fn new(arg1: Type1, arg2: Type2, ...) -> Self {
// Initialization logic
Self {
field1: arg1,
field2: arg2,
// ...
}
}
}
c. Use the new function to create instances of the struct:
let instance = MyStruct::new(value1, value2, ...);
In Rust, the convention of using the name new for constructor functions is just a convention, not a language requirement. You can choose any other meaningful name for your constructor function as per your preference or to better reflect the purpose of the struct.
While new is a widely adopted naming convention, it is not enforced by the Rust language itself. The key aspect is to have a clear and descriptive function name that indicates the purpose of creating instances of the struct.
For example, if you have a struct representing a File, you might choose to name your constructor function from_path to indicate that it constructs a File instance from a given file path:
struct File {
? ? // fields and methods
}
impl File {
? ? pub fn from_path(path: &str) -> Result<Self, FileError> {
? ? ? ? // Initialization logic
? ? ? ? // ...
? ? }
}
By using from_path instead of new, you provide a more specific name that indicates how the File instance is being constructed. This can enhance the readability and maintainability of your code.
Remember to document your constructor function appropriately, regardless of the chosen name, to provide clear usage instructions and any specific requirements for the arguments.
How to call the parent constructor from the child constructor during inheritance?
As you know inheritance is not supported in Rust, but similar functionality of code re-use is achieved via composition. (i.e isA relation is not supported , insted HasA relation is supported.)
We can call the constructor of a parent struct when inheriting from it by using the ParentStruct::new() syntax. This allows you to initialize the parent struct's fields before initializing the fields specific to the child struct.
Here's an example that demonstrates calling the constructor of a parent struct when inheriting:
struct ParentStruct {
? ? parent_field: u32,
}
impl ParentStruct {
? ? fn new(parent_field: u32) -> Self {
? ? ? ? ParentStruct { parent_field }
? ? }
}
struct ChildStruct {
? ? parent: ParentStruct,
? ? child_field: u32,
}
impl ChildStruct {
? ? fn new(parent_field: u32, child_field: u32) -> Self {
? ? ? ? let parent = ParentStruct::new(parent_field);
? ? ? ? ChildStruct {
? ? ? ? ? ? parent,
? ? ? ? ? ? child_field,
? ? ? ? }
? ? }
}
fn main() {
? ? let child = ChildStruct::new(10, 20);
? ? println!("Parent field: {}", child.parent.parent_field);
? ? println!("Child field: {}", child.child_field);
}
/*
Op =>
Parent field: 10
Child field: 20
*/
In the example above, we have a ParentStruct with a single field parent_field. It has a constructor new() that initializes the parent_field when creating an instance.
The ChildStruct struct inherits from ParentStruct and also has an additional field child_field. When calling the constructor ChildStruct::new(), we first call ParentStruct::new() to initialize the parent struct's field and then initialize the child_field.
By using the ParentStruct::new() syntax within ChildStruct::new(), we can ensure that the parent struct is properly initialized before initializing the child struct, allowing us to set up the complete state of the inherited structure.
Multiple constructor support:
As we discussed earlier since rust, a struct does not have traditional constructors like in some other languages. Instead, we can use associated functions to achieve similar behavior. struct can have multiple associated functions that act as constructors. These functions can have different names and different parameter signatures to provide different ways of initializing the struct.
In Rust, constructors (associated functions) can return values other than the newly created instance of the struct. They can return any value that is compatible with the return type specified in the function signature. This allows you to perform additional operations or computations within the constructor and return a result based on those computations.
Conversion constructor:
Conversion constructors can provided by implementing the From trait . The From trait allows us to define how an instance of one type can be created from another type. This provides a way to create instances of a struct from different types without directly using associated functions.
Example for conversion constructor :
struct Celsius(f64);
#[derive(Clone,Copy)]
struct Fahrenheit(f64);
impl From<Fahrenheit> for Celsius {
? ? fn from(f: Fahrenheit) -> Self {
? ? ? ? let celsius = (f.0 - 32.0) * 5.0 / 9.0;
? ? ? ? Celsius(celsius)
? ? }
}
fn main() {
? ? let fahrenheit = Fahrenheit(77.0);
? ? let celsius: Celsius = Celsius::from(fahrenheit);
? ? println!("{}°F is equivalent to {}°C", fahrenheit.0, celsius.0);
}
/*
Op =>
77°F is equivalent to 25°C
*/
领英推荐
Advantages of Using Constructors:
a. Improved Readability: Constructors provide a clear and descriptive way to create struct instances, making code more readable and self-explanatory.
b. Encapsulation of Initialization Logic: By encapsulating initialization logic within the new function, you can ensure that the struct is always properly initialized with valid values.
c. Controlled Construction: Constructors allow you to enforce any necessary validations or transformations on the input values before constructing the struct.
Default Values and Optional Fields:
Constructors can also handle default values and optional fields. Here are a few approaches:
a. Using Default Implementations: Implement the Default trait for the struct, and then provide a separate new function that accepts optional parameters or sets default values.
b. Builder Pattern: Implement a builder pattern, where you chain methods to set specific fields and provide a final build method to construct the struct.
Error Handling in Constructors:
Constructors can return a Result type to handle errors during struct construction. This allows you to propagate error information or perform custom error handling logic.
Examples and Use Cases:
a. Creating a Point Struct: Implementing a new function for a Point struct to initialize x and y coordinates.
b. Configuring Database Connections: Defining a constructor to create a database connection struct with connection parameters.
Best Practices:
a. Follow Naming Conventions: It is a convention in Rust to use the new function name for constructors. Stick to this convention to make your code more consistent and easily understandable by other Rust developers. However, you can choose other name also if it makes sense.
b. Document the new Function: Provide clear documentation for the new function, specifying the purpose, usage, and any constraints or requirements for the arguments.
c. Consider Default Implementations: If your struct has default values or optional fields, consider implementing the Default trait alongside the new function to provide flexibility in initialization.
Static functions
In Rust, static functions are associated functions defined on a type rather than on an instance of a type. They are also known as associated functions or simply "functions" for brevity. Unlike regular methods that are called on instances of a type, static functions are called directly on the type itself, without an instance.
Rearding static members , I will try to write a separate article :
Here are some key points to understand about static functions in Rust:
Here's an example that demonstrates the usage of static functions:
struct Rectangle {
? ? width: u32,
? ? height: u32,
}
impl Rectangle {
? ? // Static function to create a new Rectangle instance
? ? pub fn new(width: u32, height: u32) -> Rectangle {
? ? ? ? Rectangle { width, height }
? ? }
? ? // Static function to calculate the area of a Rectangle
? ? pub fn calculate_area(rect: &Rectangle) -> u32 {
? ? ? ? rect.width * rect.height
? ? }
}
fn main() {
? ? // Calling the static function new() to create a Rectangle instance
? ? let rectangle = Rectangle::new(5, 10);
? ? // Calling the static function calculate_area() to calculate the area of the rectangle
? ? let area = Rectangle::calculate_area(&rectangle);
? ? println!("Area: {}", area);
}
/*
Op =>
Area: 50
*/
In the example above, the new() function is a static function that creates a new instance of the Rectangle struct. The calculate_area() function is another static function that calculates the area of a given Rectangle instance.
Static functions provide a way to encapsulate logic associated with a type and are a useful tool for structuring code and organizing related functionality.
Advantages of using static functions in Rust:
Limitations of using static functions in Rust:
Suitable scenarios for using static functions:
It's important to note that the use of static functions should be considered carefully and in line with the principles of good software design. They should be used when they provide clear benefits in terms of code organization, convenience, or encapsulation.
I will talk about the destructors in different article .
Thanks for reading till end , please comment if you have any.