Rust: Understanding Ownership, Borrowing, References and Lifetimes

Jun 6th, 2022 (edited)
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.23 KB | None
  1. # Understanding Ownership, Borrowing, References and Lifetimes #
  2. Version 0.2
  4. # Intro #
  5. (The part about lifetimes is not yet written)
  7. Hello fellow rustaceans! Having trouble to understand the concepts of ownership, borrowing, references and lifetimes? Still fighting the compiler and not seeing it as your friend? Don't you worry, you are not alone. But while pain is inevitable, suffering is a choice. With the right help, learning these concepts can be a lot easier.
  9. This article tries to offer you a new perspective onto the topics, and hopefully it can help you in your quest and maybe even make it click for you. And I will try to do this while using only very basic parts of Rust. The solutions I present can be done better with more advanced features.
  11. I will assume you have started to play around with Rust already, and may have some experience in a language with garbage collection. I myself am a recreational coder in Python and have no real experience with software development.
  13. # Definitions #
  14. Words are important, have some definitions and aliases:
  16. * variable = owner
  17. Binds to an -> object
  19. * reference = access
  20. Binds to an -> owner and thereby to an - > object.
  22. * value = object = data
  23. Anything that can be stored in memory like a number, struct, enum, vector, ...
  24. Has nothing to do with Object Orientation.
  26. * immutable = read-only = read (access)
  27. * mutable = read-write = write (access)
  28. Just like file properties.
  30. # Comparison to Parallel Access #
  31. Even if you write your code without using any parallel design, it will still have some of its problems. Why? Because your data is lying there in memory, doing nothing on its own, while different parts of your code are accessing it at different times, and you cannot know in what order and if there are conflicting goals about reading and writing.
  33. So treat your code around variables, references and data/object access a bit like your programm would run in parallel. Every object in memory has to be protected from three kinds of bad accesses:
  35. * Access to an object that doesn't exist (anymore)
  36. * External write access while there is a read-only access
  37. * External access of any kind while there is a write access
  39. # How is Safety Guaranteed? #
  40. And now I want you to shift your thinking about variables, references and borrowing. Think of them as owners, temporary access and the process of creating this access. Again for clarity:
  42. variable => owner of an object
  43. reference => temporary access to an object
  45. TODO
  46. There is more to them, and it is important for the way they behave:
  47. * binding vs. reference & ownership
  49. The Rust compiler will now check these rules:
  51. * An object has exactly one owner
  52. * At one given moment, an object can allow many read-only accecces, but no write access
  53. * Or it can allow one write access, but no other access
  55. ### Ownership
  56. When created, an owner will bind to an object. For now, let's assume ownership will not be moved.
  58. a) The owner is created immutable by default, so the object will be unchangable, read-only.
  59. ```
  60. let owner_read_only = vec![0, 5, 15];
  61. ```
  62. b) If it is created mutable, the object will be changable, write.
  63. ```
  64. let mut owner_write = vec![0, 5, 15];
  65. owner_write.push(13);
  66. ```
  67. While an owner can access the object directly, often it only appears like it does: In the last line above, the push method is given write access for the duration of its call.
  69. ### Access Rules
  70. TODO
  71. Access to an object is given by an owner (or by another access):
  72. ```
  73. // provides read-only access
  74. let access_read_only = &owner_read_only;
  76. // provides write access
  77. let mut access_write = & mut owner_write;
  78. access_write.push(3);
  79. ```
  80. TODO
  81. The mut on both sides have different meanings: ...
  83. The kind of access an owner can give is restricted by all accesses it has already given and that are still alive. Looking at the rules above, it should be clear what can and cannot be done:
  84. ```
  85. // allowed
  86. {
  87. let access_read_only_1 = &owner_read_only;
  88. let access_read_only_2 = &owner_read_only;
  89. }
  90. {
  91. let access_read_only_3 = &owner_write;
  92. }
  93. {
  94. let access_write_1 = & mut owner_write;
  95. }
  97. // not allowed
  98. {
  99. let access_read_only_1 = &owner_write;
  100. owner_write.push(7)
  101. }
  102. {
  103. let access_read_only_1 = &owner_write;
  104. let access_write_1 = & mut owner_write;
  105. access_read_only_1.len();
  106. }
  107. {
  108. let access_write_1 = & mut owner_write;
  109. let access_write_2 = & mut owner_write;
  110. access_write_1.push(7);
  111. }
  112. ```
  113. The `{}` brackets are used here to divide the examples into different scopes.
  115. So [another way to think of the standard references]( in Rust is:
  116. * Immutable borrow / reference => shared read-only access
  117. * Mutable borrow / reference => exclusive or unique write access
  119. ----
  121. Do not let yourself be fooled by how similiar the two concepts of owner and access look like in the code, they are very different things and should be used for very different reasons!
  123. Variables and references are looking very similiar in the code, and they may both be used to access and store objects and let different parts of your code know about each other.
  125. **However**, I would advise you to think of them as very different concepts, and (at least in the beginning) to not use any of them to link together objects. If you do, you may enter [a deep rabbit hole...](
  127. In the next chapter, I will offer you an alternative for linking objects.
  128. [ -> #IDs as Substitution for Pointers]
  130. ### Access can be Difficult to Spot
  131. Most of the time, an access will not be created this clearly and explicit. Every time a function is called and takes a parameter, an access may be created. Every time a method is called on an object, it will create an access to this object. A match expression will sometimes create accesses.
  133. Think about this when the compiler gives you errors about conflicting borrowings that you find hard to understand.
  135. # How to Deal with It #
  136. Now all these rules are making it very difficult for me to create code that compiles, especially since I am used to garbage collection and a high level of abstraction. How to deal with them? What I really wish for is a collection of [Rust specific design patterns]( meant for beginners. There are a few easy ones, but most of them are quite advanced.
  138. So I had to come up with my own strategies. At least for the beginning, for education and fun, these should get you going.
  140. ### Avoid References
  141. The more you can manage to simply not create references or pass around owners, the less you can run into any trouble with the borrow checker and lifetimes. Or you use them in a very controlled way.
  143. * Copy or clone your objects. It will be slower, but for small projects it will not matter.
  145. ### Just Follow All the Rules
  146. Easy, right? Well, at least once you got the hang of it, once you really and deeply understand... one of these days... don't give up, but do take a long break if you are stuck.
  148. * Keep the lifetime of every access as short as possible, maybe by using a new scope.
  149. * Use write ownership and access only if you really have to
  150. * Prefer read-only objects
  151. * Object Orientation: Use methods to create access to an object. Every getter method returns a copy of the data it retrieves.
  152. * Simple functions, inspired by [Functional Programming]( Create functions that will copy or clone their inputs and own their outputs.
  154. ### Ease Up the Restrictions
  155. There is a lot of resistance among the more experienced rustaceans against "cheating". I see two important points here:
  157. First, if you are just learning all the other things about Rust, it could be helpful to learn the borrow checker and lifetimes later. It would be useful to have a [tutorial]( that uses other kinds of pointers first, before introducing the safe standards.
  159. Second, if you do not understand these concepts yet, disabling them will not help you to learn them. And you have to learn them, they are what is special about Rust. Many experienced systems programmer love this language for a reason, after all.
  160. > "It is a feature, not a bug!"
  162. * There are pointers and cells with fewer restrictions: [Box, Rc, Ref, RefMut, Arc, Cell, RefCell, Mutex] (
  163. * There is unsafe Rust and raw pointers. But even if you could only use raw pointers, they create their own problems and can be difficult to understand and reason about. I advise against this route.
  164. * To reduce the amount of compiler warnings, put (with an '!') #![allow(dead_code, unused_variables)] or even #![allow(warnings)] into your file. Useful for testing out small pieces of code, do **not** make it a habit.
  166. To be honest, when I hit my personal roadblock of --doom-- understanding, I did not have any mental capacity left to learn more about Rust's features before I --gave-up-- took a break. You're on your own [here] (
  168. ### IDs as Substitution for Pointers
  169. And now for the best part, the solution I came up with for letting different parts of your code know about each other, while avoiding any trouble with the borrow checker: Use an ID for every object you create, and store them all in collections!
  171. I recommend a HashMap that owns its containing objects. If you want to have increasing numbers as keys, simply store a counter in the collection and use it to give out keys. This way, you can remove objects from anywhere in the collection and all keys are staying the same.
  173. Implementing a constructor that does all the work is not an easy task, so I keep it simple here (it means I tried and failed).
  174. ```
  175. #[derive(Debug)]
  176. struct Stuff;
  178. fn main() {
  179. use std::collections::HashMap;
  180. let mut collection_of_stuff = HashMap::new();
  182. my_id: u32 = 1;
  183. collection_of_stuff.insert(my_id, Stuff {});
  185. // `unwrap` is used because `get` returns an Option
  186. println!("{:?}", collection_of_stuff.get(&my_id).unwrap());
  187. }
  188. ```
  189. Someone made [something similiar] (
RAW Paste Data Copied