mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-14 20:44:17 -06:00
finished ch20.3
This commit is contained in:
parent
0c0dcfee81
commit
e73197aa26
26
.obsidian/workspace.json
vendored
26
.obsidian/workspace.json
vendored
@ -21,6 +21,20 @@
|
||||
"title": "Advanced Features"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b24fe202609f5b35",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "Advanced Functions and Closures.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Advanced Functions and Closures"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8efdb57e394f2650",
|
||||
"type": "leaf",
|
||||
@ -74,7 +88,7 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentTab": 2
|
||||
"currentTab": 1
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
@ -217,12 +231,13 @@
|
||||
"command-palette:Open command palette": false
|
||||
}
|
||||
},
|
||||
"active": "74db89a42def0b8b",
|
||||
"active": "b24fe202609f5b35",
|
||||
"lastOpenFiles": [
|
||||
"Advanced Traits.md",
|
||||
"Advanced Types.md",
|
||||
"Unsafe Rust.md",
|
||||
"Advanced Features.md",
|
||||
"Advanced Functions and Closures.md",
|
||||
"Advanced Types.md",
|
||||
"Advanced Traits.md",
|
||||
"Unsafe Rust.md",
|
||||
"Pattern Matching.md",
|
||||
"Places Patterns Can Be Used.md",
|
||||
"Pattern Syntax.md",
|
||||
@ -244,7 +259,6 @@
|
||||
"Smart Pointers.md",
|
||||
"Simultaneous Code Running.md",
|
||||
"Passing Data Between Threads.md",
|
||||
"Leaky Reference Cycles.md",
|
||||
"minigrep/src/lib.rs",
|
||||
"does_not_compile.svg",
|
||||
"Untitled.canvas",
|
||||
|
@ -11,6 +11,6 @@ In this chapter we will cover
|
||||
- [Unsafe Rust](./Unsafe%20Rust.md): How to opt out of some of Rust's guarantees and take responsibility for manually upholding those guarantees
|
||||
- [Advanced traits](./Advanced%20Traits.md): associated types, default type parameters, fully qualified syntax, supertraits, and the new type pattern in relation to traits
|
||||
- [Advanced types](./Advanced%20Types.md): more about the newtype pattern, type aliases, the never type, and dynamically sized types
|
||||
- [Advanced functions and closures](): function pointers and returning closures
|
||||
- [Advanced functions and closures](./Advanced%20Functions%20and%20Closures.md): function pointers and returning closures
|
||||
- [Macros](): ways to define code that defines more code at compile time
|
||||
a
|
1
Advanced Functions and Closures.md
Normal file
1
Advanced Functions and Closures.md
Normal file
@ -0,0 +1 @@
|
||||
# Advanced Functions and Closures
|
@ -1 +1,333 @@
|
||||
# Advanced Types
|
||||
The Rust type system has some features that we have mentioned so far but haven't gone into detail.
|
||||
|
||||
To start we will go into the newtypes in general as we examine why newtypes are useful as types.
|
||||
|
||||
Then we will go onto type aliases, a feature similar to newtypes but slightly different semantics.
|
||||
|
||||
As well we will discuss the `!` and dynamically sized types.
|
||||
|
||||
## Using the Newtype Pattern for Type Safety and Abstraction
|
||||
The newtype pattern are also useful for tasks beyond those discussed already.
|
||||
|
||||
This includes statically enforcing that values are never confused and indicating the units of a value.
|
||||
|
||||
Before we saw an example of using newtypes to indicate units: recall that the `Millimeters` and `Meters` structs wrapped `u32` values in a newtype.
|
||||
|
||||
If we wrote a function with a parameter of type `Millimeters`, we couldn't compile a program that accidentally tired to call function with a value of type `Meters` or a plain `u32`.
|
||||
|
||||
We can also use the newtype pattern to abstract away some implementation details of a type.
|
||||
|
||||
The new type can expose a public API that is different form the API of the private inner type.
|
||||
|
||||
Newtypes can also hide internal implementation.
|
||||
|
||||
Lets say we could provide a `People` type to wrap a `HashMap<i32, String>` that store a person's ID associated with their name.
|
||||
|
||||
Code using `People` would only interact with the public API we provide.
|
||||
|
||||
Like a method to add a name string to the `People` collect: this code wouldn't need to know that we assign an `i32` ID to names internally.
|
||||
|
||||
The newtype pattern is a lightweight way to achieve encapsulation to hide implementation details, which we discussed before in [Ch18](./Characteristics%20of%20OO%20Languages.md#encapsulation-that-hides-implementation-details).
|
||||
|
||||
## Creating Type Synonyms with Type Aliases
|
||||
Rust provides the ability to declare a *type alias* to give an existing type another name.
|
||||
|
||||
We need to use the `type` keyword to do this.
|
||||
|
||||
For example we can create the alias `Kilometers` to `i32` like this.
|
||||
```rust
|
||||
type Kilometers = i32;
|
||||
```
|
||||
The alias `Kilometers` is a *synonym* for `i32`.
|
||||
|
||||
Unlike the `Millimeters` and `Meters` types we created before.
|
||||
|
||||
`Kilometers` is not a separate, new type.
|
||||
|
||||
Values that have the type `Kilometers` will be treated the same as values of type `i32`.
|
||||
```rust
|
||||
type Kilometers = i32;
|
||||
|
||||
let x: i32 = 5;
|
||||
let y: Kilometers = 5;
|
||||
|
||||
println!("x + y = {}", x + y);
|
||||
```
|
||||
Because `Kilometers` and `i32` are the same type, we can add values of both types and we can pass `Kilometers` values to functions that take `i32` parameters.
|
||||
|
||||
However using this method, we don't get the type checking benefits that we get from the newtype pattern discussed earlier.
|
||||
|
||||
In other words, if we mix up `Kilometers` and `i32` values somewhere, the compiler will not give us an error.
|
||||
|
||||
The main use for type synonyms is to reduce repetition.
|
||||
|
||||
As an example, we might have a lengthy type like this.
|
||||
```rust
|
||||
Box<dyn Fn() + Send + 'static>
|
||||
```
|
||||
Writing this lengthy type function signatures and as type annotations all over the code can be tiresome and error prone.
|
||||
|
||||
Just image a project full of code like this.
|
||||
```rust
|
||||
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
|
||||
|
||||
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
|
||||
// --snip--
|
||||
}
|
||||
|
||||
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
|
||||
// --snip--
|
||||
}
|
||||
```
|
||||
A type alias makes this code more manageable by reducing the amount of repetition.
|
||||
|
||||
Here we have introduced an alias named `Thunk` for the verbose type and can replace all uses of the type with the shorter alias `Thunk`.
|
||||
```rust
|
||||
type Thunk = Box<dyn Fn() + Send + 'static>;
|
||||
|
||||
let f: Thunk = Box::new(|| println!("hi"));
|
||||
|
||||
fn takes_long_type(f: Thunk) {
|
||||
// --snip--
|
||||
}
|
||||
|
||||
fn returns_long_type() -> Thunk {
|
||||
// --snip--
|
||||
}
|
||||
```
|
||||
This is much easier to read and write.
|
||||
|
||||
Choosing a meaningful name for a type alias can help communicate your intent as well.
|
||||
|
||||
*Thunk* is a word for code to be evaluated at a later time, this is an appropriate name for a closure that gets stored.
|
||||
|
||||
Type aliases are also commonly used with the `Result<T, E>` type for repetition.
|
||||
|
||||
Consider the `std::io` module in the std library.
|
||||
|
||||
I/O operations often return a `Result<T, E>` to handle situations when operations fail to work.
|
||||
|
||||
This library has a `std::io::Error` struct that represents all possible I/O errors.
|
||||
|
||||
Many of the functions in `std::io` will be returning `Result<T, E>` where the `E` is `std::io::Error`, such as these functions in `Write` trait:
|
||||
```rust
|
||||
use std::fmt;
|
||||
use std::io::Error;
|
||||
|
||||
pub trait Write {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
|
||||
fn flush(&mut self) -> Result<(), Error>;
|
||||
|
||||
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
|
||||
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
|
||||
}
|
||||
```
|
||||
The `Result<..., Error>` is repeated a lot.
|
||||
|
||||
Therefore `std::io` has this type alias declaration
|
||||
```rust
|
||||
type Result<T> = std::result::Result<T, std::io::Error>;
|
||||
```
|
||||
Due to this declaration is in the `std::io` module, we can use the fully qualified alias `std::io::Result<T>`.
|
||||
|
||||
That is a `Result<T, E>` with the `E` filled in as `std::io::Error`.
|
||||
|
||||
The `Write` trait function signatures end up looking like this.
|
||||
```rust
|
||||
pub trait Write {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize>;
|
||||
fn flush(&mut self) -> Result<()>;
|
||||
|
||||
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
|
||||
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
|
||||
}
|
||||
```
|
||||
This type alias helps in two ways:
|
||||
- It makes code easier to write.
|
||||
- *And*
|
||||
- It gives us a consistent interface across all of `std::io`
|
||||
Due to it being an alias, it is just another `Result<T, E>`, this means we can use any methods that work on `Result<T, E>` with it, as well as special syntax like the `?` operator.
|
||||
|
||||
## The Never Type that Never Returns
|
||||
Rust has a special type named `!` that is known in type theory lingo as the *empty type* because it has no values.
|
||||
|
||||
We prefer to call it the *never type* because it stands in the place of the return type when a function will never return.
|
||||
|
||||
Here is an example in use.
|
||||
```rust
|
||||
fn bar() -> ! {
|
||||
// --snip--
|
||||
}
|
||||
```
|
||||
This code should be read as "the function `bar` returns never."
|
||||
|
||||
Functions that return never are called *diverging functions*.
|
||||
|
||||
We can't create values of the type `!` so `bar` can never possibly return.
|
||||
|
||||
What is the use of a type you can never create values for?
|
||||
|
||||
Recall the code from Ch2, part of the number guessing game.
|
||||
|
||||
Here is a sample of that code
|
||||
```rust
|
||||
let guess: u32 = match guess.trim().parse() {
|
||||
Ok(num) => num,
|
||||
Err(_) => continue,
|
||||
};
|
||||
```
|
||||
Before we skipped over some details about this code.
|
||||
|
||||
In ch6 we discussed that `match` arms must all return the same type.
|
||||
|
||||
For example this code will not compile.
|
||||
```rust
|
||||
let guess = match guess.trim().parse() {
|
||||
Ok(_) => 5,
|
||||
Err(_) => "hello",
|
||||
};
|
||||
```
|
||||
The type of `guess` in this code would have to be an integer *and* a string, and Rust requires that `guess` have only one type.
|
||||
|
||||
So what does `continue` return?
|
||||
|
||||
How are we allowed to return a `u32` from one arm and have another arm that ends with `continue`?
|
||||
|
||||
`continue` has a `!` value.
|
||||
|
||||
That is, when Rust computes the type of `guess`, it looks at both match arms, the former with a value of `u32` and the latter with a `!` value.
|
||||
|
||||
Because `!` can never have a value, Rust decides that the type of `guess` is `u32`.
|
||||
|
||||
The formal way to describe this behavior is that expressions of type `!` can be coerced into any other type.
|
||||
|
||||
We are allowed to end this `match` arm with `continue` because `continue` doesn't return a value.
|
||||
|
||||
Instead it moves control back to the top of the loop, so in the `Err` case, we never assign a value to `guess`.
|
||||
|
||||
The never type is useful with the `panic!` macro as well.
|
||||
|
||||
Remember the `unwrap` function that we call on `Option<T>` values to produce a value or panic with this definition:
|
||||
```rust
|
||||
impl<T> Option<T> {
|
||||
pub fn unwrap(self) -> T {
|
||||
match self {
|
||||
Some(val) => val,
|
||||
None => panic!("called `Option::unwrap()` on a `None` value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Here, the same thing happens as in the `match` case form before.
|
||||
|
||||
Rust sees that `val` has the type `T` and `panic!` has the type `!`, so the result of the overall `match` expression is `T`.
|
||||
|
||||
This works because `panic!` doesn't produce a value, it ends the program.
|
||||
|
||||
In the `None` case, we will not be returning a value form `unwarp` so this code is valid.
|
||||
|
||||
One final expression that has the type `!` is a `loop`.
|
||||
```rust
|
||||
print!("forever ");
|
||||
|
||||
loop {
|
||||
print!("and ever ");
|
||||
}
|
||||
```
|
||||
This loop never ends, so `!` is the value of the expression.
|
||||
|
||||
However, this wouldn't be true if we included a `break`, because the loop would terminate when it got to the `break`.
|
||||
|
||||
## Dynamically Sized Types and the `Sized` Trait
|
||||
Rust must know certain details about its types, such as how much space to allocate for a value of a particular type.
|
||||
|
||||
This leaves one corner of its type system a little confusing at first: the concept of *dynamically sized types*.
|
||||
|
||||
Sometimes referred to as *DSTs* or *unsized types*, these types let us write code using values whose size we can know only at runtime.
|
||||
|
||||
Lets look into the details of a dynamically sized type called `str`, which we have been using throughout.
|
||||
|
||||
This does not include `&str`, but `str` on its own, is a DST.
|
||||
|
||||
We can't know how long the string is until runtime, meaning we can't create a variable of type `str`, nor can we make that argument of type `str`.
|
||||
|
||||
Consider this code, which will not compile.
|
||||
```rust
|
||||
let s1: str = "Hello there!";
|
||||
let s2: str = "How's it going?";
|
||||
```
|
||||
Rust needs to know how much memory to allocate for any value of a particular type, and all values of a type must use the same amount of memory.
|
||||
|
||||
If Rust allowed use to write this code, these two `str` values would need to take up the same amount of memory.
|
||||
|
||||
These two have different lengths:
|
||||
- `s1` needs 12 bytes of storage.
|
||||
- `s2` needs 15.
|
||||
This is why it is not possible to create a variable holding a dynamically sized type.
|
||||
|
||||
So what should we do?
|
||||
|
||||
We should make the types of `s1` and `s2` a `&str` rather than a `str`.
|
||||
|
||||
Recall from the "String Slice" section from Ch4, that the slice data structure just stores the starting position and the length of the slice.
|
||||
|
||||
Even though a `&T` is a single value that stores the memory address of where the `T` is located, a `&str` is *two* values.
|
||||
|
||||
The address of the `str` and its length.
|
||||
|
||||
We can know the size of a `&str` value at compile time: it's twice the length of a `usize`.
|
||||
|
||||
This means we always know the size of a `&str`, no matter how long the string it refers to is.
|
||||
|
||||
Generally this is the way in which dynamically sized types are used in Rust, they have an extra but of metadata that stores the size of the dynamic information.
|
||||
|
||||
The golden rule of dynamically sized types is that we must always put values of dynamically sized types behind a pointer of some kind.
|
||||
|
||||
We can combine `str` with all kinds of pointers.
|
||||
|
||||
For example `Box<str>` or `Rc<str>`.
|
||||
|
||||
In fact we have seen this before but with a different dynamically sized type: traits.
|
||||
|
||||
Every trait is a dynamically sized type we can refer to by using the name of the trait.
|
||||
|
||||
In Ch18 in ["Using Trait Objects That Allow for Values of Different Types"](./Characteristics%20of%20OO%20Languages.md#encapsulation-that-hides-implementation-details), we mentioned that to use trait as trait objects, we must put them behind a pointer, such as `&dyn Trait` or `Box<dyn Trait>` (`Rc<dyn Trait>` would work as well).
|
||||
|
||||
To work with DSTs, Rust provides the `Sized` trait to determine whether or not a type's size is known at compile time.
|
||||
|
||||
This trait is automatically implemented for everything whose size is known at compile time.
|
||||
|
||||
Additionally Rust implicitly adds a bound on `Sized` to every generic function.
|
||||
|
||||
That is, a generic function definition like this:
|
||||
```rust
|
||||
fn generic<T>(t: T) {
|
||||
// --snip--
|
||||
}
|
||||
```
|
||||
This is actually treated as though we had written this:
|
||||
```rust
|
||||
fn generic<T: Sized>(t: T) {
|
||||
// --snip--
|
||||
}
|
||||
```
|
||||
By default, generic functions will work only on types that have a known size at compile time.
|
||||
|
||||
However, you can use the following special syntax to relax this restriction.
|
||||
```rust
|
||||
fn generic<T: ?Sized>(t: &T) {
|
||||
// --snip--
|
||||
}
|
||||
```
|
||||
A trait bound on `?Sized` means "`T` may or may not be `Sized`".
|
||||
|
||||
This notation overrides the default that generic types must have a known size at compile time.
|
||||
|
||||
The `?Trait` syntax with this meaning is only available for `Sized`, not any other traits.
|
||||
|
||||
Note that we switched the type of the `t` parameter from `T` to `&T`.
|
||||
|
||||
Because the type might not be `Sized`, we need to use it behind some kind of pointer.
|
||||
|
||||
Here we have chosen to use a reference.
|
Loading…
x
Reference in New Issue
Block a user