Skip to content

Commit af250c5

Browse files
authored
Merge pull request #24 from edilmedeiros/edilmedeiros-chap08-edit
Edit chapter 08
2 parents 3ce32db + bf94b12 commit af250c5

File tree

1 file changed

+71
-24
lines changed

1 file changed

+71
-24
lines changed

08_traits_and_reading_bytes.md

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
11
# Traits and Reading Bytes
22

3-
One way you might consider reading the rest of the data from the transaction is to use various ranges. For example, consider the following code:
3+
One way you might consider reading the rest of the data from the transaction is to use various ranges.
4+
For example, consider the following code:
45

56
```rust
67
let transaction_bytes = hex::decode(transaction_hex).unwrap();
78
let version = u32::from_le_bytes(&transaction_bytes[0..4]);
89
let number_of_inputs = u32::from_le_bytes(&transaction_bytes[5..6]);
910
```
1011

11-
Notice how we're grabbing different ranges of `transaction_bytes`. We have to repeatedly reference `transaction_bytes` and we have to keep track of the start and end indexes for each component. This is not ideal. Transactions are presented in hex format for a reason. They are designed to be serialized as byte streams that can be transmitted over the network and read one byte at a time in order.
12-
13-
One way to read a byte stream is to leverage Rust's standard library's [`Read`](https://doc.rust-lang.org/std/io/trait.Read.html) trait. The slice data type in Rust implements the `Read` trait. What does this mean? Well, as we will see, it gives us a method, `read`, which will read some bytes from the slice and then store that data into a array. When we call `read` again, it will start from where it left off. In other words, it keeps track of where we are in the stream and modifies the pointer as it reads. This means we don't need to keep track of any indexes.
12+
Notice how we're grabbing different ranges of `transaction_bytes`.
13+
We have to repeatedly reference `transaction_bytes` and we have to keep track of the start and end indexes for each component.
14+
This is not ideal because we can easily make mistakes.
15+
16+
*Note: there's an indexing mistake in the code above, can you see what it is?*
17+
<!--
18+
Since vec[0..4] notation is not inclusive to the end of the index, we are skipping one byte: the element at index 4.
19+
-->
20+
21+
Transactions are presented in hex format for a reason.
22+
They are designed to be serialized as byte streams that can be transmitted over the network and read one byte at a time in order.
23+
A better solution would be to use a function that keeps track of the indices and allows us to request the number of bytes we require.
24+
Rust's standard library's [`Read`](https://doc.rust-lang.org/std/io/trait.Read.html) trait allows for exactly this.
25+
The slice data type in Rust implements the `Read` trait.
26+
What does this mean? It gives us a method, `read`, which will read some bytes from the slice and return that data in an array.
27+
When we call `read` again, it will start from where it left off.
28+
In other words, the `read` trait includes the machinery to keep track of the current position we are reading in the stream and to manage the pointer as it proceeds.
29+
This means we don't need to keep track of any indexes ourselves.
1430

1531
Let's walk through how this works at a high level with a quick example and then dive deeper into what traits are and how they work.
1632

17-
In order to use a trait method we have to first bring it into scope with a `use` statement. In this case, we want to bring the `Read` trait into scope with `use std::io::Read`. The next thing we want to do is use the `read` method as intended based on the example from the [documentation](https://doc.rust-lang.org/std/io/trait.Read.html#examples).
33+
In order to use a trait method we have to first bring it into scope with a `use` statement.
34+
In this case, we want to bring the `Read` trait into scope with `use std::io::Read`.
35+
The next thing we want to do is use the `read` method as intended based on the example from the [documentation](https://doc.rust-lang.org/std/io/trait.Read.html#examples).
1836

1937
You can follow along with this example in [Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=627e8e7c10d530819a80f189cedded13).
2038
```rust
@@ -28,28 +46,38 @@ fn main() {
2846
let version = u32::from_le_bytes(buffer);
2947

3048
println!("Version: {}", version);
49+
println!("Bytes slice: {:?}", bytes_slice);
3150
}
3251
```
3352

34-
The `mut` keyword before `bytes_slice` tells Rust the variable is mutable. If we don't provide that keyword in a variable declaration, then the compiler will complain that we're attempting to change an immutable variable, which is not allowed.
53+
The `mut` keyword before `bytes_slice` tells Rust the variable is mutable.
54+
If we don't provide that keyword in a variable declaration, then the compiler will complain that we're attempting to change the value of an immutable variable, which is not allowed.
3555

36-
You might also notice the `&mut` keyword in the argument to the `read` method. This indicates that we're passing in `buffer` as a *mutable reference*. We'll talk more about this means in the next chapter so for now let's not worry about that nuance.
56+
You might also notice the `&mut` keyword in the argument to the `read` method.
57+
This indicates that we're passing in `buffer` as a *mutable reference*.
58+
We'll talk more about this means in the next chapter so for now let's not worry about that nuance.
3759

3860
When we run this, it will print the following:
3961
```console
4062
Version: 1
4163
Bytes slice: [2]
4264
```
4365

44-
And this is what we'd expect. The Version is `1`. And the `bytes_slice` variable has been updated and no longer contains the first 4 bytes.
66+
And this is what we'd expect.
67+
The Version is `1` and the `bytes_slice` variable has been updated and no longer contains the first 4 bytes.
4568

46-
You may notice that the way this works is that you have to first create an array with a fixed size. Calling `read` will then extract the number of bytes equal to the size of the array, store that into a buffer and then update our slice.
69+
You may notice that the way this works is that you have to first create an array with a fixed size.
70+
Calling `read` will then extract the number of bytes equal to the size of the array, store that into a buffer and then update our slice.
4771

48-
## Traits Explanation
72+
## What are traits?
4973

50-
So what are traits exactly? Traits are a way to define shared behavior. You can think of them as a template for a particular set of behaviors. For example, the `Read` trait provides a template for types that want to "read data". It lays out what to expect and what types of functions are available.
74+
Traits are a way to define shared behavior.
75+
You can think of them as a template for a particular set of behaviors.
76+
For example, the `Read` trait provides a template for types that want to "read data".
77+
It lays out an *abstract interface* for a type: what kind of behavior is expected from the type and which functions are available to exercise that behavior.
5178

52-
Let's take a closer look at the `Read` trait [from the documentation](https://doc.rust-lang.org/std/io/trait.Read.html). It has a required method, `read`, which has the following function signature: `fn read(&mut self, buf: &mut [u8]) -> Result<usize>;`.
79+
Let's take a closer look at [the documentation for the `Read` trait](https://doc.rust-lang.org/std/io/trait.Read.html).
80+
It defines a required method, `read`, which has the following function signature: `fn read(&mut self, buf: &mut [u8]) -> Result<usize>;`.
5381

5482
```rust
5583
...
@@ -61,13 +89,20 @@ pub trait Read {
6189
...
6290
```
6391

64-
The `read` method itself is not actually implemented with any logic. You'll notice there's no function body, just the signature. The types that "implement" this trait are expected to provide the function logic for any *required* method, or trait methods that have no implementation.
92+
You'll notice there's no function body, just the signature.
93+
It means the `read` method itself is not actually implemented with any logic in the trait declaration.
94+
We expect the types that "implement" this trait to aactually provide the function logic for any *required* method, or trait methods, that have no implementation.
6595

66-
A trait can also provide other methods that a type can get access to once it has implemented the trait. These are known as *provided* methods and are considered *default* implementations since they can also be overwritten. You'll notice for example that there is a [`read_exact` method](https://doc.rust-lang.org/std/io/trait.Read.html#method.read_exact) which is implemented with a call to the [`default_read_exact`](https://doc.rust-lang.org/src/std/io/mod.rs.html#558) method.
96+
A trait can also provide other methods that a type can get access to once it has implemented the trait.
97+
These are known as *provided* methods and are considered *default* implementations since they can also be overwritten.
98+
You'll notice, for example, that there is a [`read_exact` method](https://doc.rust-lang.org/std/io/trait.Read.html#method.read_exact) which is implemented with a call to the [`default_read_exact`](https://doc.rust-lang.org/src/std/io/mod.rs.html#558) method.
99+
`default_read_exact` by itself is implemented with a call the the `read` method.
67100

68-
As long as a type implements the `Read` trait by providing a `read` method, it will have access to these other *provided* methods. A type can also choose to override some or all of these *provided* methods as well and have its own implementations.
101+
As long as a type implements the `Read` trait by providing a `read` method, it will have access to these other *provided* methods.
102+
A type can also choose to override some or all of these *provided* methods as well and have its own custom implementations (e.g. for performance reasons).
69103

70-
Now if we look at the `slice` type from the documentation, we can see that it [*implements* the `Read` trait](https://doc.rust-lang.org/std/primitive.slice.html#impl-Read-for-%26%5Bu8%5D) and provides the function logic for the `read` method. Let's take a look at [the source code](https://doc.rust-lang.org/src/std/io/impls.rs.html#235-250):
104+
Now if we look at the `slice` type documentation, we can see that it [*implements* the `Read` trait](https://doc.rust-lang.org/std/primitive.slice.html#impl-Read-for-%26%5Bu8%5D) and provides the function logic for the `read` method.
105+
Let's take a look at [the source code](https://doc.rust-lang.org/src/std/io/impls.rs.html#235-250):
71106

72107
```rust
73108
...
@@ -95,14 +130,20 @@ impl Read for &[u8] {
95130
...
96131
```
97132

98-
Don't worry if you don't understand what all of this means just yet! Simply notice how we *implement* a trait with the `impl` keyword. So `impl Read for &[u8]` is the code block that provides the function logic for the trait. The other thing to notice is how the function signature for `read` matches the trait's function signature.
133+
Don't worry if you don't understand what all of this means just yet!
134+
Simply notice how we *implement* a trait with the `impl` keyword.
135+
So `impl Read for &[u8]` is the code block that provides the function logic for the trait.
136+
The other thing to notice is how the function signature for `read` matches the trait's function signature.
99137

100-
The idea here is that different types, not just the `&[u8]` type can implement the `Read` trait by providing the function logic for any required method and then be expected to have similar behavior and get access to the trait's provided methods.
138+
The idea here is that different types, not just the `&[u8]` type can implement the `Read` trait by providing the function logic for any required method and then be expected to have similar behavior and get access to the trait's provided methods.
101139
The function logic itself for each type might differ, but given the template they are expected to take in the same arguments, return the same type and generally do the same thing, which in this case is to read some data and modify `self` and the buffer.
102140

103-
Again, you might notice some patterns in the code above that you are not yet familiar with, such as the `&mut` keyword and asterisk `*` before `self` at the bottom of the function. We'll go into more detail about what these mean in the next lesson.
141+
Again, you might notice some patterns in the code above that you are not yet familiar with, such as the `&mut` keyword and asterisk `*` before `self` at the bottom of the function.
142+
We'll go into more detail about what these mean in the next lesson.
104143

105-
Let's now update our program to print out the version number leveraging the `Read` trait. We can convert the `transaction_bytes` `Vec` to a `slice` type using the `as_slice` method. Here is the modified `read_version` function.
144+
Let's now update our program to print out the version number leveraging the `Read` trait.
145+
We can convert the `transaction_bytes` `Vec` to a `slice` type using the `as_slice` method.
146+
Here is the modified `read_version` function.
106147

107148
```rust
108149
use std::io::Read;
@@ -111,7 +152,7 @@ fn read_version(transaction_hex: &str) -> u32 {
111152
let transaction_bytes = hex::decode(transaction_hex).unwrap();
112153
let mut bytes_slice = transaction_bytes.as_slice();
113154

114-
// Read contents of bytes_slice into a buffer.
155+
// Read contents of bytes_slice into a buffer
115156
let mut buffer = [0; 4];
116157
bytes_slice.read(&mut buffer).unwrap();
117158

@@ -124,19 +165,25 @@ fn main() {
124165
}
125166
```
126167

127-
And voila, this will print `Version: 1` as expected! Great job so far!
168+
And voila, this will print `Version: 1` as expected!
169+
Great job so far!
128170

129-
How do we grab the modified `bytes_slice` and continue decoding the transaction? What we probably want to do is pass in the `bytes_slice` into this function as an argument and continue using it in the `main` function. We'll talk more about that and associated Rust concepts of references and borrowing in the next section.
171+
How do we grab the modified `bytes_slice` and continue decoding the transaction?
172+
What we probably want to do is pass in the `bytes_slice` into this function as an argument and continue using it in the `main` function.
173+
We'll talk more about that and associated Rust concepts of references and borrowing in the next section.
130174

131175
### Quiz
132-
1. *Take another look at the `Read` trait and the implementation of the `Read` trait for a slice in the documentation. What are the required and provided methods for the trait? What provided methods are being overwritten by the slice?*
176+
1. *Take another look at the `Read` trait and the implementation of the `Read` trait for a slice in the documentation.
177+
What are the required and provided methods for the trait?
178+
What provided methods are being overwritten by the slice?*
133179
2. *Consider the following block of code in which we create a Vec and then attempt to print it out:*
134180
```rust
135181
fn main() {
136182
let vec: Vec::<u8> = vec![0, 0, 0, 0, 0];
137183
println!("Vec: {}", vec);
138184
}
139185
```
186+
140187
*The compiler will return an error that the Vec cannot be formatted with the default formatter.*
141188
*1. Which trait is not implemented for the Vec that is required for it to be printed?*
142189
*2. How else can you print out the vector for debugging purposes?*

0 commit comments

Comments
 (0)