Object-oriented programming
Methods for structs
Structs in Rust can have methods attached to them, just like methods for classes in C++. Methods for structs in Rust are surrounded by an impl block. For example, the following procedure-styled program :
struct Address
{
street: String,
city : String
}
fn printInfo(address : & Address)
{
println!("{} , {}", address.street, address.city);
}
fn main()
{
let address = Address{street : "Street 1".to_owned(), city : "City 1".to_owned()};
printInfo(&address);
}
can be written in object-oriented style like following:
struct Address
{
street: String,
city : String
}
impl Address
{
fn new(street : &str, city : &str) -> Self
{
Self {street : street.to_owned(), city : city.to_owned()}
}
fn printInfo(&self)
{
println!("{} , {}", self.street, self.city);
}
}
fn main()
{
let address = Address::new("Street 1", "City 1");
address.printInfo();
}
Here you will see the keywords self and Self. self is a reference to the struct itself, which is similar to this in C++. Self is the type of the current struct, and in this case it is equivalent to Address.
If a method is attached to a struct, we often see there is a variable self without data type declaration in the method's signature. For example:
fn printInfo(&self)
This is equivalent to :
fn printInfo(self : &Self)
or more explicitly, in the above example, it is equivalent to :
fn printInfo(self : &Address)
Applying this technique, we try rewriting a program similar to the one in Chapter "Lifetime", but in the object-oriented style. Here both C++ & Rust program are shown to help you have a comparison.
C++:
#include <iostream>
#include <string>
using namespace std;
// declaration of class Address
class Address
{
string street;
string city;
public:
Address(const char* street, const char* city);
string toString();
};
// implementation of class Address's methods
Address::Address(const char* street, const char* city) : street(street), city(city)
{
}
string Address::toString()
{
return street + " , " + city;
}
// declaration of class Person
class Person
{
string name;
Address* address;
public:
Person(const char* name, Address* address);
void setNewAddress(Address* newAddress);
string toString();
};
// implementation of class Person's methods
Person::Person(const char* name, Address* address) : name(name), address(address)
{
}
void Person::setNewAddress(Address* newAddress)
{
this->address = newAddress;
}
string Person::toString()
{
return name + " @ " + address->toString();
}
int main()
{
Address address1("Street 1", "City 1");
Address address2("Street 2", "City 2");
Person person("Person 1", &address1);
cout << person.toString() << endl;
person.setNewAddress(&address2);
cout << person.toString() << endl;
return 0;
}
Rust:
// declaration of struct Address
struct Address
{
street: String,
city : String
}
// implementation of struct Address's methods
impl Address
{
fn new(street : &str, city : &str) -> Self
{
Self {street : street.to_owned(), city : city.to_owned()}
}
fn toString(&self) -> String
{
format!("{} {}", self.street, self.city)
}
}
// declaration of struct Person
struct Person<'a>
{
name : String,
address : &'a Address
}
// implementation of struct Person's methods
impl<'a> Person<'a>
{
fn new(name : &str, address: &'a Address) -> Self
{
Self {name : name.to_owned(), address : address}
}
fn setNewAddress(&mut self, newAddress : &'a Address)
{
self.address = newAddress;
}
fn toString(&self) -> String
{
format!("{} @ {}", self.name, self.address.toString())
}
}
fn main()
{
let address1 = Address::new("Street 1", "City 1");
let address2 = Address::new("Street 2", "City 2");
let mut person = Person::new("Person 1", &address1);
println!("{}", person.toString());
person.setNewAddress(&address2);
println!("{}", person.toString());
}
When there is no lifetime annotation in struct, like in the case of struct Address, thing is quite simple. In case there is lifetime annotation in struct, like in struct Person, the impl block also needs to specify lifetime explicitly:
impl<'a> Person<'a>
Here, there is a small notice. Inside this impl block, the Self data type is equivalent to Person<'a> rather than Person. For example, the method:
fn setNewAddress(&mut self, newAddress : &'a Address)
{
self.address = newAddress;
}
if written in procedure style, will be expanded to :
fn setNewAddress<'a>(person: &mut Person<'a>, newAddress : &'a Address)
{
person.address = newAddress;
}
and this is exact the function you saw in Chapter "Lifetime". Methods in object-oriented style often give better readability, while functions in procedure style are more verbose, but they may help you debug messages from the compiler more easily when there is errors with mismatched lifetimes in your programs, especially during your early time getting into Rust.
Trait
Trait is used to define common prototype methods to be implemented for different structs. A trait object only contains the prototypes of methods, and when a struct implements a trait, it will need to define the body of the method on its own. Different structs can implement a same trait. This is a bit similar to the way different children classes in C++ implement an empty virtual function from a parent class. However, in C++, all children classes will inherit all (protected) fields and methods from their common parent, while in Rust, only the methods defined in the trait are shared among structs that implement that trait.
For example, in the program in the previous section, both struct Address and Person have a method called toString. We will apply the trait technique to define a trait object with this common method and then implement it for each struct. Once again, the equivalent C++ program is shown here to help you have a rough comparison
C++:
#include <iostream>
#include <string>
using namespace std;
class Printable
{
public:
virtual string toString() = 0;
};
// declaration of class Address
class Address : Printable
{
string street;
string city;
public:
Address(const char* street, const char* city);
string toString();
};
// implementation of class Address's methods
Address::Address(const char* street, const char* city) : street(street), city(city)
{
}
string Address::toString()
{
return street + " , " + city;
}
// declaration of class Person
class Person : Printable
{
string name;
Address* address;
public:
Person(const char* name, Address* address);
string toString();
};
// implementation of class Person's methods
Person::Person(const char* name, Address* address) : name(name), address(address)
{
}
string Person::toString()
{
return name + " @ " + address->toString();
}
int main()
{
Address address("Street 1", "City 1");
Person person("Person 1", &address);
cout << person.toString() << endl;
return 0;
}
Rust:
trait Printable
{
fn toString(&self) -> String;
}
// declaration of struct Address
struct Address
{
street: String,
city : String
}
// implementation of trait Printable for struct Address
impl Printable for Address
{
fn toString(&self) -> String
{
format!("{} {}", self.street, self.city)
}
}
// declaration of struct Person
struct Person<'a>
{
name : String,
address : &'a Address
}
// implementation of trait Printable for struct Person
impl<'a> Printable for Person<'a>
{
fn toString(&self) -> String
{
format!("{} @ {}", self.name, self.address.toString())
}
}
fn main()
{
let address = Address{street : "Street 1".to_owned(), city : "City 1".to_owned() };
let mut person = Person{name : "Person 1".to_owned(), address : &address};
println!("{}", person.toString());
}