Translator: Matrixtang
The original
As the host of the Rust Subreddit, I often post about developers trying to convert their respective language paradigms into Rust, with mixed results and varying degrees of success. In this guide, I describe some of the problems developers encounter when converting other language paradigms into Rust, and suggest some alternative solutions to help you overcome the limitations of Rust.
Inheritance in Rust
Inheritance is arguably the most asked about missing feature in object-oriented languages. Why doesn’t Rust have one structure (struct) inherit from another?
You can be sure that even in the OO world, the reputation of inheritance isn’t much better, and practitioners generally like composition as much as possible. But you could also argue that allowing types to execute methods differently might improve performance, and therefore be desirable for those particular instances.
Here’s a classic example from Java:
interface Animal { void tell(); void pet(); void feed(Food food); }class Cat implements Animal { public void tell() { System.out.println("Meow"); } public void pet() { System.out.println("purr"); } public void feed(Food food) { System.out.println("lick"); } }// this implementation is probably too optimistic... class Lion extends Cat { public void tell() { System.out.println("Roar"); }}Copy the code
For Rust, the first part can be implemented using traits:
trait Animal { fn tell(&self); fn pet(&mut self); fn feed(&mut self, food: Food); }struct Cat; impl Animal for Cat { fn tell(&self) { println! ("Meow"); } fn pet(&mut self) { println! ("purr"); fn feed(&mut self, food: Food) { println! ("lick"); }}Copy the code
But the second part is not so easy:
struct Lion; impl Animal for Lion { fn tell(&self) { println! ("Roar"); // Error: Missing methods pet and feed // Error: Missing methods pet and feedCopy the code
Obviously, the easiest way is to copy these methods. Yes, repetition is bad. This also makes the code more complex. If you need to reuse code, pull these methods out and call them in Cat and Lion.
But, you might notice, how do you implement the polymorphism part of OO? This is where it gets complicated. Whereas object-oriented languages usually give you dynamic forwarding, Rust gives you a choice between static and dynamic distribution, and either way you lose.
// static dispatch // static dispatch let cat = cat; cat.tell(); let lion = Lion; lion.tell(); // Dispatch via enum AnyAnimal {Cat(Cat), Lion(Lion), }// `impl Animal for AnyAnimal` left as an exercise for the readerlet animals = [AnyAnimal::Cat(cat), AnyAnimal::Lion(lion)]; for animal in animals.iter() { animal.tell(); }// dynamic dispatch via "fat" pointer including vtable let animals = [&cat as &dyn Animal, &lion as &dyn Animal]; for animal in animals.iter() { animal.tell(); }Copy the code
Dynamic distribution see juejin.cn/post/687289… And alschwalm.com/blog/static…
Note that unlike garbage collection languages, in Rust each variable must have a specific type at compile time. Also, in the case of enum, implementing delegate traits using Crates like Ambassador [1] is tedious, but crates like Ambassador [1] can help.
A fairly hacky approach to delegating functions to members is to use the Deref trait for polymorphism so that functions defined by the Deref’s target can be called directly on derefee. Note, however, that this is often considered an anti-pattern.
Finally, you can implement a trait for all classes that implement one of the many other features, but it requires specialization, which is currently a nightly feature (although there is an available solution, Workaround [2], if you don’t want to write all the boilerplate code you need, Pack them in a Macro Crate). Traits are likely to inherit from each other, although they specify behavior, not data.
Linked lists or other cursor-based data structures
Many people who come to Rust from C++ will initially want to implement a “simple” bidirectional linked list, but will soon discover that it is far from simple. This is because Rust wants clear ownership, so two-way lists require fairly complex handling of Pointers and references.
A novice might try writing down the following struct:
struct MyLinkedList<T> {
value: T
previous_node: Option<Box<MyLinkedList<T>>>,
next_node: Option<Box<MyLinkedList<T>>>,
}
Copy the code
When they notice that this method fails, they add Option and Box. But once they try to insert, they are surprised:
impl<T> MyLinkedList<T> { fn insert(&mut self, value: T) { let next_node = self.next_node.take(); self.next_node = Some(Box::new(MyLinkedList { value, previous_node: Some(Box::new(*self)), // Ouch next_node, })); }}Copy the code
Of course, Borrow Checker [3] would not allow this. The ownership of values is completely confusing. Box owns the data it contains, so each node in the list will be owned by the previous and next node in the list. Only one owner is allowed per data in Rust, so this will require at least one Rc or Arc to work. But even this can quickly become cumbersome, not to mention the overhead of reference counting.
Fortunately, you don’t need to write your own two-way linked list, because the standard library already contains a (STD: : collections: : LinkedList). Also, this approach may not give you good performance compared to simple Vecs, so you may need to test accordingly.
If you really want to write a bi-directional Linked list, you can refer to Learning Rust With Entirely Too Many Linked Lists [4], which will teach you how to write Linked Lists and learn a lot about Unsafe Rust in the process.
(Also: Single-column tables can be constructed with a series of boxes. In fact, the Rust compiler includes an implementation.
The same applies to graph structures, although you may need a dependency to work with graph data structures. Petgraph[5] is by far the most popular, providing data structures and some graph algorithms.
Self-referential type
When faced with the concept of a self-referential type, it’s easy to ask, “Who owns it?” Similarly, this is what Borrow Checker does not like to hear about ownership.
This problem occurs when you have ownership and want to store both owned and owned objects in a structure. Try this approach naively, and you’ll have a hard time trying the lifetime.
We can only guess that many Rustacean have turned to the Unsafe Rust, which is subtle and errant. Of course, using plain Pointers instead of references eliminates lifecycle worries, because Pointers don’t have lifetime worries. However, this requires manual responsibility for managing the life cycle.
Fortunately, there are crates that adopt this solution and provide a secure interface, such as Ouroboros [6], Self_Cell [7], and One_self_cell [8].
Globally variable state
Developers from C or C++ (or from dynamic languages) are sometimes used to creating and modifying global states in their code. For example, one Reddit user said, “It’s completely safe, but Rust won’t let you.”
Here’s a slightly simplified example:
#include <iostream> int i = 1; int main() { std::cout << i; i = 2; std::cout << i; }Copy the code
In Rust, this roughly translates to:
static I: u32 = 1; fn main() { print! ("{}", I); I = 2; // <- Error: Cannot mutate global state print! ("{}", I); }Copy the code
Many Rustacans will tell you that you don’t need this global state. Of course, this is true in such a simple case. But for a large number of use cases, globally mutable state is really needed, for example, in some embedded applications.
Of course, there’s one way to do this, using Unsafe Rust. But until then, depending on the scenario, you might just want to use Mutex. Alternatively, if the variable only needs to be used once during initialization, OnceCell or lazy_static can neatly solve this problem. Or, if you really only want integers, the STD ::sync::Atomic* type can also be used.
That being said, having a mutable static variable is often the preferred solution, especially in an embedded world where every byte and resource is usually mapped to memory. So, if you really must do this, write it like this:
static mut DATA_RACE_COUNTER: u32 = 1; fn main() { print! ("{}", DATA_RACE_COUNTER); // I solemny swear that I'm up to no good, and also single threaded. // Unsafe. solemny {DATA_RACE_COUNTER = 2; } print! ("{}", DATA_RACE_COUNTER); }Copy the code
Again, you shouldn’t do this unless you really need to. If you’re asking if this is a good idea, the answer is no.
Initialize an array directly
Newbies may be tempted to declare arrays like this:
let array: [usize; 512]; for i in 0.. 512 { array[i] = i; }Copy the code
This will report an error because the array is never initialized. We then try to assign a value to it without telling the compiler that it won’t even reserve a place on the stack for us to write to. Rust is so picky that it distinguishes between arrays based on their contents. In addition, they need to be initialized before we can read them.
Let array = [0usIZE; 512]; let array = [0usize; 512]; We solve this problem at the cost of double initialization, which may or may not be optimized — or, depending on the type, may even be impossible to achieve. Unsafe Rust: How and When Not to Use it[9]
conclusion
The resources
[1] ambassador: docs. Rs/ambassador /…
[2] workaround: github.com/dtolnay/cas…
[3] borrow the checker: blog.logrocket.com/introducing…
[4] Study Rust With Entirely Too Many Linked Lists: rust-unofficial. Github/over-many-Li…
[5] Petgraph: crates. IO/crates/petg…
[6] oeuroboros: docs. Rs/ouroboros / 0…
[7] self_cell: docs. Rs/self_cell / 0…
[8] one_self_cell: docs. Rs/once_self_c…
[9] the Unsafe Rust: How and when not to use it: blog.logrocket.com/unsafe-rust…