Rust Generics and Traits: Define Common Struct Behaviors πŸ¦€

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys my name is Trevor Sullivan and welcome back to my video channel thank you so much for joining me for yet another video on the rust programming language now the topic that we're going to be covering in this particular video actually expands on the topics that we covered in the last couple of videos if you're following along with my rust programming tutorial playlist and those two videos covered the creation of custom userdefined data structure in Rust and the last one the most recent one actually covered how to implement methods or behaviors on your custom structures now something else that's really important to understand in Rust is the ability to use something known as generics the documentation does a pretty comprehensive job of describing generics as well as a corresponding feature of rust which is known as traits so generics and traits work very closely hand inand to allow you to Define common sets of behaviors across the different data structures that you're defining within your application now data generics or gener struct generics I should say can be used in a variety of different places here so we can Define them on structs themselves and we can also use them as function parameters as well as output argument declarations as well so there's a variety of different places inside of your rest code where you can specify the use of generics now I like to kind of demonstrate the behavior of traits before we go into generics because understanding traits first actually helps you to better understand why generics are necessary so let's go ahead and actually talk about traits how to define a trait and then how to implement a particular or trait for a type like a struct that you've defined in your code now if you're coming from some other programming languages then traits might be familiar to interfaces but unlike C interfaces that allow you to declare properties and methods traits inside of rust only allow you to Define method signatures that you want to associate to your data structures it's the same with go as well if you're coming from the go programming language then you'll probably know that interfaces in go only work with methods they don't allow you to Define common Properties or Fields across your different data structures so essentially what a trait is is it's an object that we can Define in our code in our modules in Rust and we can specify certain methods or functions that should be associated with our type that should exist as an implementation on that particular type now if if you recall from our previous video we had created a couple of different types of structs here we had a vehicle struct for example and then we created this implementation block here where we implemented an instance level method that allows us to paint an existing vehicle so if the vehicle starts out as white and we want to paint it blue we can simply call this Associated function on the vehicle type and that will affect the current instance of the vehicle that we are dealing with and the same thing applies to other type so we have like a person type here and we can Implement a method on the person type that says if we call walk meters on a person it causes that specific instance of a person like me or you or someone else to actually advance that particular distance as an attribute of the struct itself right so what we can actually do is take these different methods that we are defining on our data structures and Implement them as a common interface of behaviors now why is this important well let's take a look at an example for starters here so I'm actually going to bring in a new code file into our project here let's just Define a new file called test traits. RS and inside of here after I import this into my main function let's comment out our test create person from our previous videos and let's declare Pub mod and test traits and so that will allow us to get the rust analyzer fully uh working with this particular file so now let's open up this test traits file and we are going to define a couple of data structures right so let's do a struct called a dog okay and I'm not going to add any fields in this for now we're just going to have a dog type and then let's create another struct called a cat type so a dog and a cat are two very common types of pets that you might come across and each of those different animals makes their own unique type of sound right dogs bark cats meow that's just how it is Tigers maybe make a roaring sound Bears that's another type of animal maybe they make a roaring sound as well and so that roaring or barking or meowing is a common set of behaviors across different types of animals within our application right so rather than just going off and implementing a method here like Implement dog and then say let's create a function here called bark for a dog so what we'll do is just say bark and then put in our body and just say print line bark and up here of course we would pass in self right so normally what this would do is it would just go to the dog struct and it would associate this bark method or function it would associate it with the dog type so that when we create an instance of a dog basically referred to as Ampersand self right here a pointer to self then it'll just run the code inside of this Associated function but rather than just having this unique bark method on a dog where we have to call Bark on a dog and we have to call a meow method on a cat wouldn't it be easier if we just had a generic function called make sound or something like that that we could call on any type of animal so if it's a bear the Mak sound function is going to Roar if it's a tiger Mak sound is going to Roar if it's a cat then the Mak sound function will meow and if it's a dog then the Mak sound function will bark right and so this gives us a common interface to the different data types that we declare inside of our application if you take the character approach in a game let's say that we had a struct like character okay so this is your in-game character and let's say that you have had a character that was a mage or an Archer or a warrior well your structure here is probably going to have some kind of field like hit points that keeps track of the number of hit points that the character currently has and this is u16 not uint so every character regardless of what character type it is is going to have hit points and maybe a warrior has a different number of hit points than a mage or an Archer but if a character is damaged by an attacker character like some kind of enemy like maybe a a wizard or maybe another Warrior or a ghost or something like that then the hit points are going to take some damage right so rather than implementing a unique damage method for every single character type out there we can instead just declare that common Behavior as a trait and then we can implement the specific imp impementation for each of those different specific character types so hopefully that makes a little bit of sense I know it's kind of wordy but hopefully this will make a little bit more sense as we continue here so in any case let's say for example that we have a struct person here and the struct person maybe you at home maybe you have a pet right maybe you have a pet dog maybe you have a pet cat we happen to have three dogs of our own so uh let's just say that a person can only have one pet though right and so we want to give them a pet property here and we want them to have a pet type as the pet right so what we could do is say pet is going to be of type dog because we have a dog struct down here but now if a person owns a cat then we can't assign a cat instance to this dog pet property right because it's a type dog it's not of type cat or if somebody had a pet bear or a pet tiger or a pet antelope or whatever it may be then this struct right here is too specific right it's only allowing us to have a dog so instead we would have to Define other types of structs and this just complicates our code so what we could do is say person with cat and then change the pet type to cat we could paste it again and then say pet is type of bear which we haven't created yet we could say person with bear and this one maybe we could say person with with dog and so as you can see we've got several different structs here each depending on what type of pet that particular person has right well as we expand the number of animals we have many many different permutations of our data structures here and all of them have to be implemented uniquely right so generics allow us to actually declare a pet Field on a generic person type so that we don't have to duplicate all of this code and declare different data structures for each of these permutations so rather than declaring the type of pet as a dog what we can actually do is say something like animal okay now we haven't defined animal yet but animal is going to become a trait which is a kind of Declaration of certain behaviors of data structures inside of rust so if you think about it here a dog is an animal a cat is also an animal and we could go ahead and add other types of structs in here like a bear maybe you have a pet bear I hope not but maybe you do uh we'll do a struct of tiger here as well and so each of these four different types of animals are animals right I don't know why I said that but you get the idea here so what we can do is use traits to declare all of these different animals as animals so what we'll do is say uh trait animal and we don't even have to put anything inside of the body of the trait itself self so what you're going to see is that we can implement this trait called animal on each of these structures down here and once we implement this particular trait that will allow us to use it as a generic type on our person object here and then a person can have any type of animal as a pet that it wants right and again just keep in mind that for this example we're just referring to a person having a singular pet we're not going to get into arrays or vectors or slices or anything like that we're just going to keep things relatively simple and just have a scalar value for the pet Field on our person type so what we need to do in order to fix this issue here you can see it says trait objects must include the D keyword well what we need to do is declare this trait down here as a generic type for person and the way that we do that is with these angle brackets here so less than and greater than and inside of these brackets right here we just specify a term so we could do something like pet type and so this is a generic declaration that we can then use inside of our application here so what we're going to do is say pet type and then in order to specify what the pet type actually can be because right now this generic can accept literally any type of data as input it could accept an unsigned 8bit integer it could accept a floating Point 64-bit value it could accept a string value it really doesn't matter at this point because we have no boundary as far as what types we can assign to this generic so what we do is we Implement something known as trait boundaries where we can restrict the pet type generic type to be only certain trait implementations and so what we want to do is use our animal trait right down here in order to specify that the pet type right here has to be implementing the animal trait and so what you're going to see is that at the moment let's just get rid of this implementation block down here so at the moment none of our animals that we have right over here implement the animal trait and so right now we would not be able to assign an instance of a dog or a cat or a bear or an tiger to the pet Field on a person instance so let's actually try that out and see what happens so let's declare a public function here so we can call it from our main application entry point and we're going to say create person and inside of this create person method right here or function right here we are going to create a person for starters and let's just give a person a first name just so we can uniquely identify the person so we'll just make that a string value and then we'll say let's create a person so let's do let P1 equal person semicolon and right inside of here we specify our Fields so first name will be trevor. twring and then our pet Field right here is going to be a pet now we want to go ahead and construct that pet so that we can pass it into this data type when we construct it the person type right because right now you're going to see that the person type has an unknown generic because we haven't specified anything for the pet type yet right so what we need to do is declare our pet let's do let pet one equal a dog and then if we try to pass in pet one right here into our function you're going to see that we get this error right here so let's try to do cargo run and you're going to see that the person right here has a specification that requires a bound and so what we're saying is that the animal trait must be implemented for this pet type here so in order to fix this we actually have to implement the animal trait on the dog type because we tried to assign a dog to a generic type that accepts an animal so what we can do for this trait here is just go right under our dog struct here and we're going to create an implementation block here and normally you might just say imple dog well in this case we need to specifically implement the animal trait for the dog struct type so what we do is we say imple animal for dog and so when we use this syntax right here imple trait for type that basically Associates that trait with the type and that requires that this type like dog for example Implement any function sign signatures that have been declared in the trait at the moment our animal trait doesn't actually specify that any function signatures must be satisfied so we can literally just have an empty implementation block for the animal trait on the dog type and that satisfies the trait so now that the dog type has implemented the animal trait you should see that that error disappears here on the line where we construct the person and if we do a cargo run let's actually make sure that we call that from our main function here so we'll go into test traits and call the create person function that we publicly exposed from this module right here let's just do a cargo run and everything works perfectly fine here now of course we do have some warnings here from the rust analyzer just because we are not actually using any of these structs and so obviously you want you don't want to have just dead code sitting in your project so since we specifically want to allow these we'll just do allow dead code and we can copy that attribute down to each of these types so that we can get rid of that warning all right so at the moment we have a struct we've declared a pet type of the animal trait and we have a pet Field here that takes the animal trait and allows anything that implements the animal trait to be used right down here so now what we can do is Implement animal for these other types down here so we need to implement animal for cat right actually let's just comment this out and I'll show you what happens if we don't do that let's comment these out and then let's try to change this down here let pet 2 equal cat so we have an empty Cat Here and Now what we're going to do on the person when we instantiate the person we're going to change pet to pet 2 which is a cat not a dog and as you can see we're back to square one with our trait because the trait animal is not implemented for the cat type so that's what you're going to see if you try to pass in a generic but you have the trait boundary that specifies that this trait must be implemented on whatever data type is being passed in for this type here so what we want to do is implement this for cat we'll implement it for bear and we'll implement it for Tiger so we'll save that and that should automatically fix our issue here because pet 2 is a cat cat now implements the animal trait and so that works perfectly fine and we could just duplicate this code a couple more times do pet three set it to a bear and then we'll do pet four and we'll set it to a tiger all right so now we have four different pets and regardless of which pet we want to use let's try pet number four which is a tiger you can see that that compiles and executes just fine uh we could do pet number three which is a bear again hopefully you don't actually have a bear as a pet I don't either uh probably not many people do except for zoos uh but as you can see that compiles and runs perfectly fine as well all right so that's how we can declare the generic type on our struct but at the moment our trait right here doesn't have any functions that are required to be implemented on each of these types down here so what we do inside of the trait body here is we specify a function that we want to be common behavior on any type that implements the animal trait so as we talked about before all of these different four types of animals make different types of sounds a bear is going to Roar a tiger's going to Roar a dog is going to bark and a cat is going to meow so what we want to do is make sure that we Implement some kind of generic make sound behavior on all of these different types right and then we'll implement the actual specific behavior for each of the different types in our implementation blocks so what we'll do is create a function here called make sound and we're going to accept self as a parameter because this is going to be an instance level method that we call on each instance of the type so we don't want to just call it on dog generally because dogs as a concept don't bark specific dogs bark so what we're going to do is just go ahead and return nothing so we'll just return a unit type there and we'll just put a semicolon to terminate it so this is all we have to do for a trait all we do is we declare the signature of the function which includes the name of the function the input arguments here and any return types as well but then the actual implementation of this particular function or in this case method because it's Associated to types is to go down to the implementation block for each type that implements animal and you can now see that it says not all trait items are implemented and it specifically even tells you this is the great thing about rust is it's very specific and tells you that the Mak sound function is missing from the implementation for a dog so all we have to do is just start typing here and automatically from the trait definition right up here the signature of the function is autocom completed by the rust analyzer extension for Microsoft Visual Studio code and it automatically knows the name of the function the input arguments here if applicable and the return type of the function so we can just autocomplete the definition of the function and then we'll just do print line dog Bart so what we're going to do is essentially just copy this implementation here and use that similar implementation for each of our animal trait implementations so we'll go down here and say cat meow and then we'll copy that again come down here to the bear implementation and say bear roar and I think we're going to use the same thing for Tiger I'm not really sure if there's a difference between a tiger Roar and a bear roar but we're just going to say tiger roar so as soon as we save this project right here you can see that all of our errors disappear everything should compile just fine here because all of our different data structures that implement the animal type are now implementing the Mak sound function with the appropriate input and output types so now when we specify something like pet 3 which is a be we should be able to do cargo run and of course we need to call that method so let's do p1. pet dot make sound because whatever the pet is for the person regardless of what type it is we want to make that sound for it right so what we're going to do is do cargo run and because pet number three is a bear we have bear Ro or if we change the pet to let's say pet number four you can see tiger W and if we change the pet to pet number two we can see cat meow and if we change the pet to pet number one we can see that the dog has barked so by creating a single struct type here to represent a person and by using rust's generic type argument along with what's known as a trait boundary here to specify what valid input types are allowable for the pet type we were able to just use a single person struct rather than defining separate structures for each person for each animal type that we Implement because if we implemented 50 different types of animals into our rust application we would have to have 50 different types of person structures that each allowed that specific type of animal but using generics we only need one implementation of person and that satisfies any type of animal thanks to the animal trait that we declared right here so of course you could feel free to implement other behaviors on your trait here you can Define other function names and signatures that you want to be implemented across all of your different animal types and what's cool is that you can even combine traits together so let's say that certain types of animals are not dangerous okay so let's say that we had a dog here and we have a trait called not dangerous and we're just going to assume that all dogs are not dangerous obviously some breeds are more dangerous than others but we're just going to assume for the sake of example that all dogs are not dangerous so we're going to come right after our struct dog here and we're going to say imple not dangerous and we're just going to do an empty set of curly braces there so now the dog struct has the not dangerous trait along with the animal trait so it's an animal and it's not dangerous same thing for a cat let's implement the not dangerous trait for a cat here as well of course a bear and a tiger are both dangerous types of creatures so we probably don't want to have those as pets right so if we come back to our person type right up here right now we are allowing any animal to be a pet of the person but if we only want to allow and go further restrictive with our trait boundaries here we can actually add in the not dangerous trait in order to restrict which types of animals are allowed and the way that we do that is with the plus sign so we're basically adding multiple traits together so that they're being uh kind of conglomerated together so basically all of these traits must be satisfied in order to use that type for pet type so we'll do animal plus not dangerous and just autoc complete that and we should be able to implement that here of course I just realized that I totally forgot to say for dog and for cat so we need to make sure that we implement the trait for these specific types so don't forget to do that as well say four dog and four cat and of course we don't have that implementation of not dangerous for the bear and tiger types all right so now that we have this trait boundary that says animal and not dangerous what would happen if we try to run our application well dog barked because a dog is not a dangerous animal let's try substituting the pet number two which is a cat and it says cat meow right because it's not dangerous so let's try pass passing in maybe pet number three and check it out we get this exception here because it says that the boundary of not dangerous hasn't been implemented for the pet type generic argument and therefore we cannot use it as a pet on the person type and the same thing will apply for pet 4 which is a tiger once again you're going to see this exception it says that only the cat and dog implement the not dangerous trait so therefore cat and dog are the only valid value so again the rust compiler is being extremely helpful it's trying to inform us exactly what's wrong with our code and it's even telling us the valid inputs that we could use for this particular argument during our person construction so this is kind of how generics work it's a really powerful feature and hopefully this example of using people pets and specific types of animals has been informative and educational to help you understand exact how this concept works now there is an alternative syntax that you can use for trait boundaries right up here so right now we just have the trait boundary after this colon but it's directly after the generic declaration here and often times you'll see generics just referenced as t for type but you can actually use a name like pet type if you want to just to make your generics more readable I don't really like using single characters because it just makes your code look really confusing type T type Z Type U type P whatever it is it just gets kind of confusing but when I look at a person type here and see a generic called pet type it's pretty obvious to me that that's referring to a specific type of pet so what we can do is just let's copy the or cut out the animal and not dangerous traits here we'll get rid of that colon right there after pet type the pet type generic and then what we can do is say where and we can do pet type colon and then specify these over here so this helps this is just a different syntax that you can use but this helps to make things a little bit more readable here so if you did specify multiple generics which you also can do then over here we can just kind of separate out the trait boundaries from the actual Declarations of our generic input arguments so if we wanted to we could add other fields down here that do have other types of generics here so for example we could say pet number two could be a pet type two so these are two different generics that we're declaring here and of course as soon as we save that you'll see that we get this error because we haven't declared pet type 2 as a generic so let's go up here and say pet type 2 and let's say that for pet type two we only want to allow dangerous animals so your first pet can be a not dangerous animal but your second pet can be a dangerous animal so what we'll do is specify that pet type two can be an animal and it has to be dangerous but of course we haven't actually implemented the dangerous trait yet so what we'll do is just say trait dangerous and right down here we'll Implement dangerous for bear and then we'll also Implement dangerous for the tiger struct as well and then down here on this line where we are instantiating our person we of course need to now specify pet two and pet two if we try to make it a P1 for example remember that P1 is a dog right so we have dog right up here so pet number one is going to be pet number two which is a cat pet number two is pet number whoops let's do pet one here and so as you can see the dog does not satisfy the dangerous trait only the bear and the tiger implement the dangerous trait so therefore only the bear or the Tiger can be used as pet number two so if I try pet two that's the cat so once again the cat does not implement the dangerous trait but if I use pet three which is a bear then you can see that that compiles and runs just fine and if I use pet 4 as the second dangerous pet then we can run that and it compiles just fine as well and the reason that you're not seeing any output from the bear or the Tiger here even though it's compiling is because we are only calling make sound on the first pet not the second pet so we could just duplicate this line reference the pet 2 field and then call the make sound function or method because the animal trait that declares the Mak sound method has been implemented on both not dangerous and dangerous animals so anyways that's all I've got for this particular video for now hopefully it this gives you a better understanding of how generics work and how traits work inside of rust and thanks for joining me again please leave a like if you learned anything new from this video leave a comment down below let me know what you thought of this video and please subscribe to the channel to help me grow this community because I am an independent content creator and any support that you can provide would be extremely helpful thanks again for watching and we'll see you soon
Info
Channel: Trevor Sullivan
Views: 4,554
Rating: undefined out of 5
Keywords: rust, rustlang, rust developer, rust programming, rust software, software, open source software, systems programming, data structures, rust structs, rust enums, rust coding, rust development, rustlang tutorial, rust videos, rust programming tutorial, getting started with rust, beginner with rust programming, rust concepts
Id: XKbOVFt3UNY
Channel Id: undefined
Length: 32min 22sec (1942 seconds)
Published: Mon Aug 28 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.