What object oriented programming characteristic allows you to create a class that is a specialized version of another class?
Everyone comes from somewhere, and the chances are good that your previous programming language implemented Object-Oriented Programming (OOP) in a particular way:
Object-oriented design is then about identifying the classes (the 'nouns') and the methods (the 'verbs') and then establishing relationships between them, is-a and has-a. There was a point in the old Star Trek series where the doctor would say to the captain, "It's Life, Jim, just not Life as we know it". And this applies very much to Rust-flavoured object-orientation: it comes as a shock, because Rust data aggregates (structs, enums and tuples) are dumb. You can define methods on them, and make the data itself private, all the usual tactics of encapsulation, but they are all unrelated types. There is no subtyping and no inheritance of data (apart from the specialized case of Deref coercions.) The relationships between various data types in Rust are established using traits. A large part of learning Rust is understanding how the standard library traits operate, because that's the web of meaning that glues all the data types together. Traits are interesting because there's no one-to-one correspondence between them and concepts from mainstream languages. It depends if you're thinking dynamically or statically. In the dynamic case, they're rather like Java or Go interfaces. Consider the example first used to introduce traits: # #![allow(unused_variables)] # #fn main() { trait Show { fn show(&self) -> String; } impl Show for i32 { fn show(&self) -> String { format!("four-byte signed {}", self) } } impl Show for f64 { fn show(&self) -> String { format!("eight-byte float {}", self) } } #}Here's a little program with big implications: fn main() { let answer = 42; let maybe_pi = 3.14; let v: Vec<&Show> = vec![&answer,&maybe_pi]; for d in v.iter() { println!("show {}",d.show()); } } // show four-byte signed 42 // show eight-byte float 3.14This is a case where Rust needs some type guidance - I specifically want a vector of references to anything that implements Show. Now note that i32 and f64 have no relationship to each other, but they both understand the show method because they both implement the same trait. This method is virtual, because the actual method has different code for different types, and yet the correct method is invoked based on runtime information. These references are called trait objects. And that is how you can put objects of different types in the same vector. If you come from a Java or Go background, you can think of Show as acting like an interface. A little refinement of this example - we box the values. A box contains a reference to data allocated on the heap, and acts very much like a reference - it's a smart pointer. When boxes go out of scope and Drop kicks in, then that memory is released. # #![allow(unused_variables)] # #fn main() { let answer = Box::new(42); let maybe_pi = Box::new(3.14); let show_list: VecThe difference is that you can now take this vector, pass it as a reference or give it away without having to track any borrowed references. When the vector is dropped, the boxes will be dropped, and all memory is reclaimed. For some reason, any discussion of OOP and inheritance seems to end up talking about animals. It makes for a nice story: "See, a Cat is a Carnivore. And a Carnivore is an Animal". But I'll start with a classic slogan from the Ruby universe: "if it quacks, it's a duck". All your objects have to do is define quack and they can be considered to be ducks, albeit in a very narrow way. # #![allow(unused_variables)] # #fn main() { trait Quack { fn quack(&self); } struct Duck (); impl Quack for Duck { fn quack(&self) { println!("quack!"); } } struct RandomBird { is_a_parrot: bool } impl Quack for RandomBird { fn quack(&self) { if ! self.is_a_parrot { println!("quack!"); } else { println!("squawk!"); } } } let duck1 = Duck(); let duck2 = RandomBird{is_a_parrot: false}; let parrot = RandomBird{is_a_parrot: true}; let ducks: Vec<&Quack> = vec![&duck1,&duck2,&parrot]; for d in &ducks { d.quack(); } // quack! // quack! // squawk! #}Here we have two completely different types (one is so dumb it doesn't even have data), and yes, they all quack(). One is behaving a little odd (for a duck) but they share the same method name and Rust can keep a collection of such objects in a type-safe way. Type safety is a fantastic thing. Without static typing, you could insert a cat into that collection of Quackers, resulting in run-time chaos. Here's a funny one: # #![allow(unused_variables)] # #fn main() { // and why the hell not! impl Quack for i32 { fn quack(&self) { for i in 0..*self { print!("quack {} ",i); } println!(""); } } let int = 4; let ducks: Vec<&Quack> = vec![&duck1,&duck2,&parrot,&int]; ... // quack! // quack! // squawk! // quack 0 quack 1 quack 2 quack 3 #}What can I say? It quacks, it must be a duck. What's interesting is that you can apply your traits to any Rust value, not just 'objects'. (Since quack is passed a reference, there's an explicit dereference * to get the integer.) However, you can only do this with a trait and a type from the same crate, so the standard library cannot be 'monkey patched', which is another piece of Ruby folk practice (and not the most wildly admired either.) Up to this point, the trait Quack was behaving very much like a Java interface, and like modern Java interfaces you can have provided methods which supply a default implementation if you have implemented the required methods. (The Iterator trait is a good example.) But, note that traits are not part of the definition of a type and you can define and implement new traits on any type, subject to the same-crate restriction. It's possible to pass a reference to any Quack implementor: # #![allow(unused_variables)] # #fn main() { fn quack_ref (q: &Quack) { q.quack(); } quack_ref(&d); #}And that's subtyping, Rust-style. Since we're doing Programming Language Comparisons 101 here, I'll mention that Go has an interesting take on the quacking business - if there's a Go interface Quack, and a type has a quack method, then that type satisfies Quack without any need for explicit definition. This also breaks the baked-into-definition Java model, and allows compile-time duck-typing, at the cost of some clarity and type-safety. But there is a problem with duck-typing. One of the signs of bad OOP is too many methods which have some generic name like run. "If it has run(), it must be Runnable" doesn't sound so catchy as the original! So it is possible for a Go interface to be accidentally valid. In Rust, both the Debug and Display traits define fmt methods, but they really mean different things. So Rust traits allow traditional polymorphic OOP. But what about inheritance? People usually mean implementation inheritance whereas Rust does interface inheritance. It's as if a Java programmer never used extend and instead used implements. And this is actually recommended practice by Alan Holub. He says:
So even in Java, you've probably been overdoing classes! Implementation inheritance has some serious problems. But it does feel so very convenient. There's this fat base class called Animal and it has loads of useful functionality (it may even expose its innards!) which our derived class Cat can use. That is, it is a form of code reuse. But code reuse is a separate concern. Getting the distinction between implementation and interface inheritance is important when understanding Rust. Note that traits may have provided methods. Consider Iterator - you only have to override next, but get a whole host of methods free. This is similar to 'default' methods of modern Java interfaces. Here we only define name and upper_case is defined for us. We could override upper_case as well, but it isn't required. # #![allow(unused_variables)] # #fn main() { trait Named { fn name(&self) -> String; fn upper_case(&self) -> String { self.name().to_uppercase() } } struct Boo(); impl Named for Boo { fn name(&self) -> String { "boo".to_string() } } let f = Boo(); assert_eq!(f.name(),"boo".to_string()); assert_eq!(f.upper_case(),"BOO".to_string()); #}This is a kind of code reuse, true, but note that it does not apply to the data, only the interface! An example of generic-friendly duck function in Rust would be this trivial one: # #![allow(unused_variables)] # #fn main() { fn quack(q: &Q) where Q: Quack { q.quack(); } let d = Duck(); quack(&d); #} |