How to quickly customize a collection type? A programmer familiar with some object-oriented languages might write it like this.
Inherit some built-in type (if it exists) and extend on top of that built-in type. But for programmers familiar with Python, this is not a good idea.
Types of ducks
When a bird is seen walking like a duck, swimming like a duck, and quacking like a duck, then the bird can be called a duck.
A simple example.
|
|
See, this custom Collection
class doesn’t inherit from Python’s built-in Iterable
type, but behaves as if it were some built-in iterable type.
These double underscore methods are generally called magic methods, and are not supposed to be called by the user; they are treated by the interpreter as a “protocol” that allows your custom class to have features like indexing, slicing, etc., regardless of whether it inherits from a standard library type, as long as it implements the corresponding protocol .
Python doesn’t care about whether our custom Collection
type is a subclass of a collection type, but whether it has the “ability” to be a collection.
The following example shows that Python
doesn’t even require that the interface be implemented when defining the class.
|
|
By dynamically registering the __iter__
method of the Statement
class directly at runtime (this is called a monkey patch by the community and I personally don’t like it in real projects), you can call the iter
function on this custom type and even isinstance(s, Iterable)
returns a True
result.
Mixin
I gave an example in a sharing session if there is such an inheritance chain in the system.
If a new flying object needs to be introduced in this system, but it represents a seagull, how to do it? Inherit the existing aircraft base class directly? If we do that we will get a steel seagull with fuel quantity information, a unique new species. Or we could extract another common abstract base class that has only the parts common to aircraft and seagulls.
But if we think in terms of duck types and add a new type, why must we be sure that it is a subclass of some type? Whether it’s a helicopter or a seagull, all they need in common is the ability to fly.
Django
is one of the popular web frameworks in Python, and views can be defined in Django
like this.
|
|
Instead of defining a comprehensive parent class, Django
defines multiple Mixin
classes, which are usually translated into Chinese as mixin classes, each of which is responsible for a part of the functionality, for example JSONResponseMixin
is responsible for JSON
responses, which is somewhat similar to CSharp
, Java
’s interface
(interface). The difference is that CSharp
doesn’t support multiple inheritance, but allows multiple interfaces, while Mixin
in Python is just a default convention that everyone follows, and can be written this way because Python supports multiple inheritance, and a child class can inherit from multiple parents. The Mixin
should not affect the functionality of the subclass itself; it should abstract a generic function for extending the subclass, which itself cannot normally be instantiated.
Such code formally inherits from multiple parent classes, but in practical terms it is more like a combination of functionality from different mixin classes. For example, the above code combines the functionality of a JSON
response with a template response, returning different types of responses depending on the request. Mixing duck calls, shapes, and flight patterns gives you a custom “duck”, depending on what functionality you need.
Object-Orientation in ## Rust
Now it’s the turn of Rust, a programming language that supports multiple paradigms, including the object-oriented paradigm. But first, what exactly is object-oriented? To borrow from the official tutorial The Book: If you follow the GOF description of object-oriented Patterns), object-oriented programs consist of objects that package together data and the processes that manipulate it, then Rust
undoubtedly supports object orientation, with Rust
organizing data by enum
and struct
and binding methods to them via impl
.
However, some programmers may object to this statement, as some people believe that only forms with encapsulation, inheritance, and polymorphism count as object-oriented, and Rust doesn’t even have class
, just as some people believe that JS and Python don’t quite count as object-oriented languages either.
Encapsulation, Inheritance, Polymorphism
These three words are really deep, and chances are that every software engineer has heard of them, so here’s a discussion of these three features in Rust.
The main role of encapsulation, in my opinion, is to isolate different levels of abstraction, where the bottom developer is responsible for the implementation details, and the developer at the top is only concerned with the exposed interface. For example, for list
in Python
, we know that it has an interface that lets us get the number of elements inside it without having to go into the internal implementation details, which are the responsibility of the standard library developers. If we encapsulate a minimum stack on top of this object, we can get the minimum value in the list via the min
method, and we are responsible for encapsulating this interface, whether we maintain a separate stack to hold the minimum value or traverse the entire list when we call the interface is an internal detail that users of this type do not need to know.
Of course, for some languages, mechanisms are provided to force properties to be hidden from external callers, and Rust has the pub keyword to restrict accessibility .
Since the foo
field is not identified by the pub
keyword, it is a private field and cannot be accessed directly.
Next is inheritance, and there is no inheritance in Rust . It is not possible to implement a child structure that inherits from a parent structure. Inheritance has two main purposes, one is to reuse code, where the child class automatically gets the properties and methods of the parent class, but code reuse does not necessarily require inheritance; the other is for polymorphism, where a child type can be used where the parent type is needed.
This makes it seem a bit odd to compare the concepts of polymorphism and inheritance. Inheritance becomes a way to implement polymorphism, which is a bit broader.
Since Rust doesn’t have inheritance, how are the two functions inherited above (mainly the latter) going to be implemented in Rust? How would polymorphism be implemented?
Rust can abstract shared behavior through trait
. Take the example of an airplane, where all kinds of airplanes, and seagulls, can fly, but the specific way they fly is a little different.
The syntax impl trait for struct/enum
allows a function to be abstracted and implemented for different types, and in contrast to Python’s Mixin, trait can also be combined to implement multiple traits for a single type. Like Mixin and C# interfaces, traits can have default implementations.
|
|
The core idea of trait
is combination, trait
is an abstraction of behavior, different objects can have similar behavior, objects are a combination of data and behavior. In the previous Django
example, although syntactically it is multiple inheritance, isn’t it also essentially a combination? Compositions are better suited than inheritance to indicate that an object has a certain function or feature, rather than is a certain kind.
Look at the duck type. When a place needs a duck that quacks, as long as we provide an object with the quack of a duck, isn’t that just polymorphism? So how is polymorphism represented in Rust’s type system?
The following code will compile through.
Of course, Rust’s enumeration is a and type on the type system, where Status::Successful
and Status::Failed
are of the same type (Status
), often referred to as variants
, and look at another code example.
The code can be compiled, where I make use of generics, where the custom function takes a parameter of type T
, which is qualified as: the type that implements the trait Fly, which is called trait bounds.
The code has been slightly modified.
|
|
The Debug trait
is implemented here for both structs via the derive
macro. By implementing this trait, you can print out the name of the structure itself. To add this trait to the type qualification of the generic method, the syntax T: trait1 + trait2
can be used to qualify that a type must implement multiple traits.
The printed result is.
Writing code with only one generic function and Rust actually creating separate functions for each different type after compilation is an approach called static distribution which has the disadvantage of making the compiled size larger. Another approach, called dynamic distribution, puts the type determination at runtime, which takes up less space but introduces more runtime overhead.
The code is not changed much, and dynamic distribution can be achieved by &
borrowing or Box
smart pointer wrapper type, and to add dyn
keyword.
Off topic: Generic polymorphism is not just for trait bounds, see reference and other sources.
This is a statically typed “duck type” belonging to Rust, generic_func
needs objects that can fly, not caring whether they are airplanes or seagulls, and not caring whether they have a common parent class.
Summary
The main purpose of this article is to show how object-oriented programming can be implemented in a Rust way. Rust is not completely unique, and the Python example is listed to illustrate this point. In addition, object-oriented is not the same as encapsulation, inheritance, polymorphism, inheritance and polymorphism can not even be considered parallel concepts.
As for the detailed usage of generic and trait
in Rust, it is limited to space, and related materials such as the official documentation are very detailed, so I won’t elaborate.