In this section, we introduce some of the more advanced type features, including the newType schema, type aliases, never, and dynamic size types described in the previous section

Use the NewType schema for type safety and abstraction

In the previous section, we skipped the orphan rule by using the newType schema. We can also use the newType schema to encapsulate certain details of a type. For example, newType can expose a public API that is different from the internal private type, thus limiting the functionality that users can access. Here is a MyVec structure that can only add members:

struct MyVec<T>(Vec<T>);
impl<T> MyVec<T> {
  fn new() - >Self {
    MyVec(vec![])}// Only implement add, do not provide delete method, so cannot delete
  fn push(&mut self, item: T) {
    self.0.push(item)
  }
}
let mut arr = MyVec::new();
arr.push(1);
arr.push(2);
arr.push(3);
Copy the code

The newType pattern can also be used to hide internal implementations. For example, we could provide the People type to encapsulate a HashMap

that stores the person ID and its name. Users of type People can only use the public API provided by us, such as a method that adds a name string to the People collection. The code that calls this method does not need to know that we have given the name a corresponding ID internally. In the future, we can change the ID generation rules at will without affecting the user:
,string>

use std::collections::HashMap;
struct People(HashMap<u32.String>);
impl People {
  fn new() - >Self {
    People(HashMap::new())
  }
  fn add(&mut self, name: String) {
    // Simply generate an ID based on the name
    let id: u32 = name.as_bytes().iter().map(|&x| x as u32).sum();
    // Save to HashMap
    self.0.insert(id, name); }}let mut people = People::new();
people.add(String::from("xiaoming"));
// {860: "xiaoming"}
Copy the code

Create a synonym type using a type alias

Those of you who have used TS know that the type keyword can be used to generate an alternative name for an existing type:

type Kilometers = u32;
let x: u32 = 5;
let y: Kilometers = 6;
println!("{}", x + y);
/ / 11
Copy the code

The primary use of type aliases is to reduce code character duplication:

type Thunk = Box<dyn Fn(a) >;// 1. Thunk as a parameter
fn takes_long_type(f: Thunk) {
  f()
}
let f: Thunk = Box::new(|| println!("hi"));
takes_long_type(f); // "hi"

// 2. Thunk as the return value
fn returns_long_type() -> Thunk {
  Box::new(|| println!("hello"))}let f2 = returns_long_type();
f2(); // "hello"
Copy the code

For Result

types we often use type aliases to reduce code duplication, such as the STD :: IO module method that returns Result

in the return value fails to handle:
,>
,>

use std::io::Error;
use std::fmt;

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>;
}
Copy the code

We use the type alias to handle the Result<… , the Error > :

// Since all E are STD :: IO ::Error,
// Different methods return different types of T,
// So we just need to pass in type T as the type argument
type Result<T> = std::result::Result<T, std::io::Error>;

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< > (); }Copy the code

The Never type that Never returns

Rust has a name called! The term in the type system is empty type because it does not have any value. We prefer to call this type never because it acts as a return value type in functions that never return, such as the following function bar that never returns:

fn bar() - >! {}Copy the code

So is the return type of continue! :

let mut x = 0;
loop {
  let y: u32 = if x == 1 {
    x
  } else {
    x += 1;
    continue;
  };
  println!("y: {}", y);
}
Copy the code

The code above is stuck in an infinite loop. That’s not the point. We see that y is of type U32, x in the if branch is of type correct, and continue returns! The key here is type! Expression can be cast to any other type, so allow u32 to be the type of y, otherwise an error will be reported.


In addition, the panic! The macro is implemented using the never type, using the unwrap function Option

as an example:

impl<T> Option<T> {
  pub fn unwrap(self) -> T {
    match self {
      Some(val) => val,
      None= >panic!("called `Option::unwrap()` on a `None` value"),}}}Copy the code

In the code above, the compiler knows that val is of type T, panic! Is!!! Type, here! Is cast to T, so the result of the entire match expression is T.


The return type of loop is! :

let x / *! * / = loop {
  print!("loop");
};

// If there is a break in the loop, then the type of x is empty: ()
let x / * () * / = loop {
  break;
};
Copy the code

The type inferred by the compiler is shown in the comments after the x variable, which can be seen in the vscode editor. You can try it out

Dynamic size types and Sized traits

Whereas RUST must know the size of all types at compile time, there is a concept in the type system of dynamic type sizes that are known only at run time

STR type

STR is a dynamic size type. The length of a string can only be determined at runtime, so you cannot create a variable of type STR, or use STR as an argument to a function:

let s1: str = "abc"; // The size of s1 was not known at compile time
let s2: str = "abcd"; // the size of s2 was not known at compile time
fn foo(s3: str) {  // The size of s3 was not known at compile time
}
Copy the code

Rust allocates memory by type at compile time. If each STR had the same amount of memory, s1 and S2 would have the same amount of memory, but in fact the two characters are different in length. We usually use Pointers to solve STR problems:

let s1: &str = "abc";
let s2: &str = "abcd";
fn foo(s3: &str) {}Copy the code

Changing STR to the reference type & STR will compile because each reference has a fixed size, each containing a reference to the starting location of the data in memory and the length of the data


In addition to & references, smart Pointers can also be used to determine the size at compile time:

use std::rc::Rc;
let b: Box<str> = Box::from("abc");
let r: Rc<str> = Rc::from("abc");
Copy the code

Sized trait

Rust also provides a special Sized trait to determine whether a type’s size is known at compile time, which is automatically implemented by a type that calculates the size at compile time, and rust implicitly adds a Sized constraint for each generic function:

fn generic<T>(t: T) {
}
/ / the compiled
fn generic<T: Sized>(t: T) {
}
Copy the code

By default, generic functions can only be used with types whose size is known at compile time. Sized can be preceded by? To relax this restriction:

fn generic<T: ?Sized>(t: &T) {
}
Copy the code

? Sized means: not sure whether T has been Sized. This syntax can only be used for Sized traits, not for other traits. In addition, the parameter t type is changed from t to &t. Because the t type may not have been Sized, we need to place it after some kind of pointer. References are used above, but smart Pointers can also be used.

Cover: Follow Tina to draw America

Read more about the latest chapter on our official account