finished ch20.3

This commit is contained in:
darkicewolf50 2025-04-16 15:44:05 -06:00
parent 0c0dcfee81
commit e73197aa26
4 changed files with 354 additions and 7 deletions

View File

@ -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",

View File

@ -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

View File

@ -0,0 +1 @@
# Advanced Functions and Closures

View File

@ -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.