- In the previous section,
we saw how to define associated functions for a structure.
So here's a quick recap before we move on.
I've defined a public structure Point3D,
I've defined the implementation block.
It has a public function, I've called it new by choice.
It doesn't have a self parameter,
therefore it's an associated function,
effectively a static function in other languages.
You invoke this method on the name of the structure
you'd say Point3D, colon, colon, new
and it would create presumably,
and return a Point3D object initialized
with the X, Y, and Z parameters that you pass in.
So associated functions pertain
to the entire structure type, not to an instance.
You invoke them on the structure type,
typically like here as a factory function
to create a new instance and to return it.
You can also define associated data,
associated data pertains to the entire structure,
not to any particular instance.
So it's effectively shared by all instances,
similar to the idea of static data
in other object-oriented languages
Right, well, you don't there are two ways
to define associated data as it's called,
you can either define associated data which is constant
and doesn't change and no net compile time,
that's the key point.
Or you can define associated data,
which isn't constant, which could change.
Okay, so two different techniques.
If you want to have per structure data that is fixed,
then you can declare it as constant.
If you have per data structure that can change,
then you need to find it as static.
All right, so we can look
at these two different techniques in the next few minutes.
So the easiest approach is
when you have constant associated data,
data that's fixed and is kind of shared by all instances,
like static constant in other languages.
So what you do is you define a constant field
inside the implementation block.
You optionally make it public and then you can access it
anywhere in your methods.
Okay, so let's have a look here.
I've got a type called Employee.
I've defined it in the file mytypes/employee.rs.
So the structure template, if you like,
will define the data,
and then in the implementation block for employee
as well as having functions, I can define a constant field.
The maximum salary that any employee is allowed
to earn is fixed for all employees,
it's the same maximum salary anyone can earn and it's fixed.
So I've made a constant.
Remember when you declare constant,
you have to specify the type explicitly.
So it's an unsigned 64 bit.
Turns out that the maximum salary
that anyone can earn is 99,000 units.
Right, so if you define a constant in your structure,
then you can use it via its structuring.
So here's a payrise method.
You would take an employee, like employee1,
employee1.payrise, you'd pass in the amount
that you're hoping to give this employee as a pay rise.
It adds the amount onto that person's salary.
And then it checks to see whether we've overflowed,
if the person's salary is no greater
than the maximum salary defined at the structural level.
So you can use self to access per instance data
and then you use the class name or the structure name
to access data that's defined
at the entire structural level here.
Okay, so it'll access the maximum salary,
which is defined at the structural level.
It's the same maximum salary for everybody.
And it's a constant here,
so if the person's salary has now exceeded
the maximum salary, we just reset it to the maximum.
So you can't earn more than 99,000 in my example.
So this is how you can define shared data that's constant
inside the implementation block.
You define the constant field like that
and then you use the name of the structure type
in order to access it here.
Okay, so that's when you have constant data.
If you have non-constant data,
it's quite a different approach.
What you do is you define a static variable in the module,
not actually inside the implementation block,
but outside it's still in the same file,
but outside the implementation block,
you define a static variable,
and then use it in your functions.
Okay, so here's an example.
I've got a static defined in the module,
but outside the implementation block, okay?
So this static,
you don't define it inside the implementation block.
You define it outside, it's still part of the module,
but it's not actually inside the implementation block.
So what I've got here is kind of like a, you know,
when you insert data into a database,
the database has an auto increment mechanism.
Every time you create a new row,
it automatically increments the ID.
I've got some kind of similar mechanism here.
NEXT_ID is kind of like a semi global counter
called NEXT_ID, remember, you've gotta use capital letters
for statics and you've gotta specify the type.
You've gotta be really careful as well
when you have statics that can change about thread safety,
we talked about this earlier.
You've gotta either use some kind of mutex
or some kind of lazy initialization.
You've gotta be careful when you have multiple statics.
So I've used an atomic data type here, AtomicI32,
an integer type that can be modified
in a thread safe manner.
So we we're not gonna get into thread safety issues.
So this is a thread safe 32 bit integer,
which I initialize to 0.
And then every time I create a new employee,
it'll create a new employee
and it'll give that employee a different ID.
The ID for each employee will be, well have a look here,
it's gonna get the next ID, this atomic integer.
It'll fetch the value, it'll add one to it, okay,
it's basically increment the value
and that incremented value will be the ID for this employee.
So if I call the new function five times,
every time I call new,
it'll basically increment the global NEXT_ID.
So each instance will have a different ID,
ID 1, ID 2, ID 3, ID 4.
It's like a self increment mechanism
that you get in a relational database.
Right, so that's how you implement shared data
or associated data that can vary,
define a static field outside the structure
and then you can use it here, okay, so here,
notice that we didn't specify the structure type
as a prefix because NEXT_ID is defined in the module
but it's not actually defined inside the scope of employee,
it's defined at the file level, if you like,
so we access it as a file level property.
We don't specify the class name
or the structure name as a prefix.
Right, let's have a look at an example in our project.
So we'll start off with main.rs
which defines a sub bunch of submodules in mytypes.
And in particular there's a submodule called employee,
which is defined in employee.rs,
that's where our structure is gonna be.
And then we'll have the usage of it in demo_associated_data
and then we'll run the application.
Okay, so from the top in main.rs,
define mytypes as a module.
Let's not forget uncomment the code
we're gonna be using.
When we define a mytypes module, as we know,
it'll look for mytypes.rs, and in mytypes.rs,
I define a second division module called employee.
Okay, so as we've said before,
if you are in a file called mytypes,
I need to find a submodule, then it look
in the folder called mytypes to find that submodule,
and here it is.
So here's my employee submodule,
got some things being introduced into scope,
and here's my employee structure.
Notice the data now is private,
it's only accessed inside this file,
which is the correct approach, encapsulated,
the structure itself is public,
so I can access the employee name, the data type,
but I can't access the fields externally.
Only in this file can I access the fields.
So that's encapsulation,
that's the equivalent of private data in other languages.
Here's my variable associated data, that's only visible,
that's not public either.
I could have made it public actually if I wanted to,
but I haven't, it's just an internal implementation detail,
so that can only be accessed in the current file.
My implementation for employee defines
constant associated data.
And let's see what we've got here.
The to_string function, I don't think there's anything
particularly interesting there.
It receives an immutable reference to self,
it returns a string.
The string will contain, oh, notice the ID here.
So it'll return the ID inside square brackets
just to make it look nice followed by the person's name,
how much the person earns, and their full-time status.
Okay, so that's the data there.
There's a mutable payrise method
where you attempt to give the person a salary increment
but if the salary now exceeds the maximum salary,
max salary was the constant defined
inside my employee scope.
Okay, so inside my employee block, I defined max salary.
The value will just be effectively copy and pasted in
at the compile time.
So if the salary is more than 99 K,
then reset the salary to 99 K.
Here instead of using the name
of the structure type employee, you can always use Self
as in with a capital S as an alias for the type
of the current structure.
So I could have said Self, look in my own type
to find the maximum salary,
look in my own type to find maximum salary there.
Okay, so not something that you'd probably want to do
at this point, but like I said, when we get on to looking
at traits and generics later on, being able to self-identify
like that is actually quite a useful sentence.
So self with a lowercase s is a reference
to the actual object, Self for the capital S is an alias
for the current structure type.
So I could have actually just said employee
if I preferred to make it more explicit.
Okay, and then finally we have our associated function, new,
build a function, takes in the person's name
and salary and boolean, returns an employee,
it uses the associated static data here,
okay, which is kind of a global scope in this file.
So not prefixed by the structure type.
NEXT_ID fetch the current value and add one.
So initially it's 0, so it'll fetch the value 0
and increment it, so that would be 1,
and that would be the ID.
And that's the ID for the first employee.
So the first time I call new,
the ID will be 1 for that employee
and obviously the second time I call new
for the next employee, its ID will be 2,
and then the third time the ID will be 3, and so on.
Okay, so let's look at the actual usage
of that demo_associated_data.
So I create three employees.
Employee one is Matthew, who earns 10,000,
and employee two is Mark and he earns 20,000.
Luke earns 30,000, and John earns 40,000.
And John is mutable and subjected to a handsome pay rise.
He earns it, you know, so Matthew, Mark, Luke, and John,
you know those names, let's print those details out.
I hope this example works because this is the last example
now in the lesson, it would be nice if it actually worked.
Okay, so remember that the ID was output
in square brackets just for readability.
So the NEXT_ID mechanism that we had here,
this NEXT_ID was rolled over each time.
Each time we created a new employee,
the NEXT_ID was incremented
in a thread safe kind of way to keep the compiler happy.
So the first employee has ID 1, oh, and ID 0,
and then ID 1, and then ID 2, and then ID 3, okay,
plus the details for each employee like so.
So that wraps up the lesson.
Just a very quick reminder of what we've covered.
Defining structures with fields.
We knew that already.
Defining the implementation block for a structure,
which is a kind of a strange concept
when you're first getting into this.
The fact that you define the data in the structure block
and the implementation in another block takes a bit
of getting used to but it is the way to do it.
And then in the implementation block,
functions can be defined as public,
if you want them to be publicly visible or not.
Most functions will be methods.
They have a self reference, either mutable or not.
You can have associated functions
that basically like statics,
they don't have a self pointer, typically they access
or they create an instance, then return it.
And then finally we talked about the idea
of having static data, effectively static data
that's constant, you define inside the implementation block
as constant and then you access it by the class name
or the structure name.
Static data that can vary,
you define outside the implementation block
with a static keyword, and then you can access those
without the class or the structure prefix here.
Okay, so now that we've seen structures,
what we're gonna look at
in the next lesson is how to define interfaces or traits.