Rust Bite - Data Types

types, initialization, ownership, borrowing

This page walks the Rust type system through several sections. The "Page Controls: sect list" button shows a dropdown menu with links to each section and stays in view throughout the page. The bottom menu bar's "Sections" does the same thing.

Synopsis:

This page demonstrates simple uses of the most important Rust types, quickly building familiarity with each type and its role.
  • Rust divides types into two categories: copy and move. Copy types occupy a single contiguous block of stack memory. Move types keep control blocks in stack memory that manage resources on the heap.
  • Primitive types and aggregates of primitive types are copy. Construction, assignment, and pass-by-value copy the source's value to the destination.
  • All other types are move. Construction, assignment, and pass-by-value move ownership of the value's heap resources to the destination. That leaves the source invalid - using a "moved" variable is a compile error.
  • The source and destination of copy and move operations must have exactly the same type.
  • Rust Bite - Data Operations covers copy and move operations in more detail, including diagrams and code examples.
  • Most move types provide clone operations for making copies, but the calling code must invoke clone() explicitly.
  • Rust supports fixed references to both copy and move types. A reference may point to instances in a function's stack memory, in the native heap, or in the program's static memory.
  • Rust ownership rules constrain references to guarantee memory and data race safety by construction. Every value in memory has a single owner responsible for its creation, access, and deallocation.
  • A reference borrows ownership from the owner. Program code may create an arbitrary number of non-mutable references to a variable but only a single mutable reference, which borrows exclusive access.
  • Rust's type system plays a dominant role in providing memory and data race safety.
  • Here we begin to see significant differences between the languages, especially between statically typed languages like C++, Rust, and C# and dynamically typed languages like Python and JavaScript.
Rust Types Details 

Table 1.0 Rust Types

Type Comments Example
-- Integral types ----
bool values true and false let b = true;
i8, i16, i32, i64, isize, u8, u16, u32, u64, usize signed and unsigned integer types let i = 42i8;
-- Floating point types ----
f32, f64 values have finite precision, and may have approximate values let f:f32 = 3.14159;
-- literal string types --
&str A literal string let ls = "literal string";
let second = ls.chars().nth(1);
&str Slice of a literal string let slice = &ls[1..5];
-- Aggregate types ----
[T; N] An array of N elements all of type T let arr = [1, 2, 3, 2, 1];
let first = arr[0];
&[T] Slice of array of elements of type T let arrs = &arr[1..3];
let second = arrs[1];
Tupl collection of heterogeneous types accessed by position let tu = (42i32, 3.14159f64, 'z');
let third = tu.2;
Result<T,E> Result enum holds result Ok(t:T) or Err(e:E) fn doOp1(..args) -> Result<r:R, e:Err>
Option<T> Option enum holds optional value Some(t:T) or None fn doOp2(..args) -> Option<T>
-- Std::library types ----
String Expandable collection of utf-8 characters allocated in the heap let strg = "a utf-8 String".to_string();
Vec<T> Expandable generic collection of items of type T let v = Vec::<f64>::new(), v.push(1.5); ...
VecDeque<T> Expandable double-ended generic collection of items of type T let vd = VecDeque::<f64>::new(),
v.push_front(1.5); ...
HashMap<K,V> Associative container of Key-Value pairs, held in a table of bucket lists. let map = HashMap::<&str, int>::new(),
map.insert("zero", 0); ...
LinkedList, BTreeMap, HashSet, BTreeSet, BinaryHeap, ... The Rust std::library defines types for threading and synchronization,
reading and writing to streams, anonymous functions, ...
Crate std
-- User-defined Types --
User-defined types Based on structs and enums, these will be discussed in the next Bit.
Rust Type System Details 

Table 2. Rust Copy and Move Operations

Operation Example Copy type Move type
Construction let t:T = u:T u's value is copied to t,
u is still valid
u's value is moved to t,
u now invalid
Assignment t:T = u:T u's value is copied to t,
u is still valid
u's value is moved to t,
u now invalid
Pass-by-value fn doOp(t:T) t's value is copied to doOp stack frame,
t is still valid
t's value is moved to doOp stack frame,
t is now invalid

Table 3. Rust Copy and Move Types

Property Members Notes
Primitive types integers, floats, literal strings These are all copy types
Aggregate types arrays, slices of arrays, tuples, Result<T,E>, Option<T> These are copy types if their members are copy, otherwise they are move types
std::library collection types String, Vec<T>, VecDeque<T>, HashMap<K,V>, ... These are all move types

Table 4. Rust Type System Attributes

Static typing The compiler knows all types at compile time; they remain fixed throughout program execution.
Inference The compiler infers types in expressions when programs omit explicit annotations. Inference occasionally fails and explicit annotation is required.
Strong typing The compiler checks types exhaustively and allows very few implicit conversions.
  • Numeric and boolean literals coerce to their corresponding type, e.g., 42 to i32.
  • Variables without type annotations coerce to their minimal types.
  • Values coerce to a more specific type of the same or larger size.
  • Smart pointers like Box, Rc, Arc, ... implement the DeRef trait, supplying a method deref() that returns a reference to their inner values. Applying * to these types automatically calls deref() to return an inner reference.
Algebraic data types Enums and structs create these types. Unlike other languages, Rust enums can hold named discriminants with associated data of arbitrary type. That, combined with Rust's matching operations, simplifies state and error handling.
Examples: Result<T,E> { Ok(T), Err(E), }, Option<T> { Some(T), None, }
Matching:
    match result {
       Ok(value) => { // do something with value },
       Err(error) => { // do something with error }
    }
Generics Generics provide types and functions with unspecified parameters, supporting code reuse and abstraction over types. The call site specifies generic parameters, often bound by constraints, e.g., fn doOp<T:Debug, Clone>(t:T).
doOp compiles only if T satisfies its named trait bounds: Debug supporting the format {:?} and Clone supporting explicit copy with clone().
Traits Traits resemble Java and C# interfaces. They define shared behavior that types can implement, supporting abstraction over behavior. Traits declare trait-specific functions. A trait may, but need not, define its function's contents. GenericRust covers Generics and Traits.

1.0 Initialization

A let statement initializes a Rust type by binding a variable name to a memory location holding the specified value.

1.1 Initializing language-defined types

The Rust language defines scalar types - booleans, integers, and floats - and aggregate types: literal strings and slices, arrays and array slices, tuples, and the enums Result<T,E> and Option<T>. The "Rust Types Details" dropdown above enumerates all type variations, e.g., i8, i16, ... The code block below shows how to initialize each of these and how to access members of an aggregate.
  /*-- initialize language defined types --*/

  let i = 42i8;         // data type specified

  let b = true;         // data type inferred

  let f:f32 = 3.14159;  // variable type specified  

  let c:char = 'z';

  let sl:&str = "a literal string";
  let second = sl.chars().nth(1);

  let st = "an owned string".to_string();
  let fourth = st.chars().nth(3);

  let arr:[i32; 3] = [1, 2, 3];
  let first = arr[0];

  let tp: (i32, f64, char) = (1, 2.5, 'a');
  let second = tp.1;

  let iref: &i8 = &i;

  let aref: &[i32; 3] = &arr;
Programs apply type specifications like i8 and f64 to the variable name or to its initializing data, as shown here. Rust usually infers appropriate types for data, but the developer can specify them explicitly. Without the type annotations in this example, Rust would infer i as i32 and f as f64. The char type occupies 4 bytes of storage, large enough to hold any utf-8 character. The type &str refers to a fixed-size immutable literal string in stack memory. It may hold any utf-8 characters. Its size equals the sum of the actual literal character sizes. For the literal string shown, all characters are 1 byte. Arrays are fixed-size sequences of values of the specified type. They support indexing, and mutable arrays allow changing individual element values. Tuples are sequences of values of heterogeneous types. Position arguments access elements, e.g., tp.1 accesses the second element, a float. References like &arr are fixed pointers to the start of the variable's data storage.
Output
  -------------------------
    create and initialize
  -------------------------

  --- initialize language-defined types ---  

  --- i8 ---
  i, i8
  value: 42, size: 1

  --- bool ---
  b, bool
  value: true, size: 1

  --- f32 ---
  f, f32
  value: 3.15927, size: 4

  --- char ---
  c, char
  value: 'z', size: 4

  --- &str ---
  ls, &str
  value: "literal string", size: 16
  second, core::option::Option<char>
  value: Some('i'), size: 4

  --- alloc::string::String ---
  st, alloc::string::String
  value: "an owned string", size: 24
  fourth, core::option::Option<char>
  value: Some('o'), size: 4

  --- [i32; 3] ---
  arr, [i32; 3]
  value: [1, 2, 3], size: 12
  first, i32
  value: 1, size: 4

  --- (i32, f64, char) ---
  tp, (i32, f64, char)
  value: (1, 2.5, 'a'), size: 16
  second, f64
  value: 2.5, size: 8

  --- &i8 ---
  iref, &i8
  value: 42, size: 8

  --- &[i32; 3] ---
  aref, &[i32; 3]
  value: [1, 2, 3], size: 8
  second, i32
  value: 2, size: 4

  --- &str ---
  lscs, &str
  value: "iter", size: 16
  second, core::option::Option<char>
  value: Some('t'), size: 4

  --- &[i32] ---
  sla, &[i32]
  value: [2, 3], size: 16
  second, i32
  value: 3, size: 4
              
Each type in the code block on the left displays its value, its type - evaluated using std::any::type_name::<T>() - and its size, retrieved using std::mem::size_of::<T>(). The function showType formats that information for display. Section 1.5 below shows its definition in the bits_data_analysis.rs file.
Scalar types occupy a single contiguous block of stack memory. Copies of all scalar types are byte-wise copies of the source data. That produces two unique instances residing in the invoking function's stack frame.
Language-defined aggregate types with scalar members also occupy a contiguous region of stack memory and copy byte-wise. Aggregates whose members use heap allocations like strings and vectors move instead of copy.
Section 1.4 below discusses copies, moves, and clones.
The char type holds any utf-8 character in 4 bytes. Literal strings and instances of the string type are sequences of utf-8 characters. Each utf-8 character occupies 1 to 4 bytes. That supports many different character sets: ASCII, Unicode, Mandarin, Kanji, Arabic, ... Rust strings pack the utf-8 characters, so string elements are not fixed size. That means they are not indexable. Instead, Rust provides the chars iterator, which returns an Option enumeration holding either Some(ch) or None once the iteration reaches the end of the character sequence. Code accesses the character value, ch, by conditionally unwrapping Option. Arrays are fixed-size, indexable collections of values of a single type, set by the array's declaration. Code accesses values by index, and mutable arrays allow modification by index. Tuples are fixed-size collections of values of heterogeneous types. Code accesses elements by position: 0, 1, ...

1.11 Specifying locations

Rust stores data in static memory for constants and static data, in stack memory for function arguments and function-local data, and in the native heap. The code block below illustrates each.
  /*-- static, stack, and heap allocations --*/

  static PI:f64 = 3.1415927;      // static memory  
  // static address: &PI = 0x7ff730eb7668
  ------------------------------------
  let f:f64 = 3.1415927;          // stack memory
  // stack address:   &f = 0x40a1cff8c0
  ------------------------------------
  let s:&str = "3.1415927";       // stack memory
  // stack address: &str = 0x40a1cff910
  ------------------------------------
  let g = box::new(3.1415927f64); // heap memory
  // heap address:   &*g = 0x1b78cc05bd0

            
Variables bind to locations in static memory, stack memory, or heap memory. The compiler allocates static memory at compile time, so its contents live for the duration of the program. The runtime allocates stack memory implicitly for each function call, including main. Stack memory serves as scratch-pad storage for input parameters and locally declared data. It deallocates implicitly when the thread of execution leaves the function's scope. Heap memory allocates explicitly through construction of a smart Box pointer. That allocation returns when the smart pointer, g, goes out of scope.

1.2 Initializing standard library types

  /*-- standard library types --*/

  let v:Vec<i32> = vec![1, 2, 3, 2, 1];
  // size: 24 bytes, control block holds
  // ptr to ints on heap, length, capacity
  ------------------------------------
  let st:String = "an owned string".to_string();
  // size: 24 bytes, control block holds
  // ptr to chars on heap, length, capacity
  ------------------------------------
  let mut vecdeq = VecDeque::<f64>::new();
  vecdeq.push_front(1.0);
  vecdeq.push_front(1.5);
  vecdeq.push_front(2.0);
  vecdeq.push_front(1.5);
  vecdeq.push_front(1.0);
  ------------------------------------
  let mut map = HashMap::<&str,i32>::new();
  map.insert("zero", 0);
  map.insert("one", 1);
  map.insert("two", 2);
            
A call to new initializes Standard Library types; subsequent operations add data. Here, vec! is a std::library macro that initializes variable v by creating a Vec<int> and pushing the elements 1, 2, ... Vecs consist of a control block in stack memory holding a pointer to a resizable array of elements in the heap, a count of the elements currently stored, and a count of the available capacity for data. When the length equals the Vec's capacity and a new element is pushed, the Vec allocates a new heap array of twice the current capacity, copies the elements to the new storage, and deallocates the original heap array. The string, st, initializes by converting a literal string to a String type. Strings are sequences of utf-8 characters, each of which may occupy one to four bytes. That supports characters from non-roman languages - Arabic, Kanji, etc. - as well as emojis and symbols. A String's structure resembles a Vec. It has a control block in stack memory and a collection of characters in the heap. The main difference: vectors hold elements of a fixed size and support indexing. Strings hold utf-8 characters that vary in size from one to four bytes. They do not support indexing but provide an iterator, chars, that steps through the character collection. The statement let mut vecdeq = ... creates a double-ended VecDeque queue. It must be declared mut to allow later modifications with push-front statements. Data resides in a circular buffer on the heap, supporting efficient pushing onto either the front or back of the queue. HashMap is an associative container - in this example Key:&str and Value:i32. The &str type refers to a literal string like "zero". Key-value pairs reside in a table of buckets that are linked lists. Lookup applies a hash function to a unique key to find the table address of the bucket holding that key. The HashMap retrieval API walks the bucket list using the hash, searching for the specified key. The code block below displays some of the output of a demonstration program in Bits/Rust/rust_data, stored in the Bits repository alongside all the code used in the Bits for C++, Rust, C#, Python, and JavaScript. Clone or download the repository and explore these examples in Visual Studio Code.
Output
--- initialize std::lib types ---

vec, alloc::vec::Vec
value: [1, 2, 3, 2, 1], size: 24

st, alloc::string::String
value: "an owned string", size: 24

vecdeq, alloc::collections::vec_deque::VecDeque
value: [1.0, 1.5, 2.0, 1.5, 1.0], size: 32

map, std::collections::hash::map::HashMap<&str, i32>
value: {"one": 1, "zero": 0, "two": 2}, size: 48

              
Each type in the code block on the left displays its value, its type - evaluated using std::any::type_name::<T>() - and its size, retrieved using std::mem::size_of::<T>(). The function showType formats that information for display. Section 1.5 below shows its definition in the bits_data_analysis.rs file.
Language-defined aggregate types with scalar members occupy a contiguous region of stack memory and copy byte-wise. Aggregates whose members use heap allocations like strings and vectors move instead of copy. Section 1.4 below discusses copies, moves, and clones.

2.0 Safety and Ownership

The Rust language design, especially its type system, focuses on memory and data race safety. That means:
  • Read and write operations happen only within program-allocated memory.
  • References always refer to valid targets.
  • Threads share data only within the confines of a lock. Each thread must acquire a lock to access data, then release it so other threads can access it.
Construction guarantees these properties. The compiler will not build code that violates them.
Rust Bites - Safety covers memory and data race safety in more detail.

2.1 Single Ownership Policy

To support safety, Rust enforces a "single ownership" policy. Every value in memory has a single owner that manages its allocation, initialization, deallocation, and lifetime. That means:
  • Binding a name to a value assigns ownership of that value to the named variable. Only the owner has authority to modify, use, or deallocate the value. No other variable can access or modify the value without borrowing that privilege from the owner.
  • A collection like Vec<T> owns its members. In the code segment below, v owns the vector, and the vector owns the elements [1, 2, 3]. Program code accesses them by taking a reference:
      let v = vec![1, 2, 3];
      let rv = &v3[1];
    

2.2 Borrows - Rust References

  • References borrow access to a variable from its owner, e.g., let rv = &v. Rust enforces strict rules for the use of references to guarantee memory and thread safety.
    • Non-mutable references share read-only access to a value. Multiple shared references to a single owned value are valid.
    • Mutable references, often called exclusive references, provide exclusive access to the value. No other references can access the value until the mutable reference's lifetime ends. That usually occurs when the mutable reference goes out of scope.
    • These rules apply to use, not declaration.
  • This strong constraint occasionally affects the way programs are designed. No shared mutability ensures references to a collection, like Vec<T>, don't dangle when the collection reallocates its resource memory to provide more capacity.

    Example: Attempt to mutate collection with active reference

      show_op("attempt to mutate Vec while immutable ref exists");
      let mut v3 = vec![1, 2, 3];
      let rv3 = &v3[1]; // ok to declare
      v3.push(4);  // v3 will reallocate if capacity is 3
      println!("  rv3: {rv3:?}"); // not ok to use
    

    Example: Compiler error message

    C:\github\JimFawcett\Bits\Rust\rust_data
    > cargo run
       Compiling rust_data v0.1.0 (C:\github\JimFawcett\Bits\Rust\rust_data)
    error[E0502]: cannot borrow `v3` as mutable because it is also borrowed as immutable
       --> src\main.rs:417:3
        |
    416 |   let rv3 = &v3[1]; // ok to declare
        |              -- immutable borrow occurs here
    417 |   v3.push(4);  // v3 will reallocate if capacity is 3
        |   ^^^^^^^^^^ mutable borrow occurs here
    418 |   println!("  rv3: {rv3:?}"); // not ok to use
        |                     ------- immutable borrow later used here    
    
    For more information about this error, try `rustc --explain E0502`.   
    error: could not compile `rust_data` (bin "rust_data") due to previous error
    

2.3 Copy, Move, and Clone Operations

  • Copy of primitives and Move operate efficiently, copying only a few bytes. Clone is expensive for a collection because it copies both the collection control block and all its heap resources.
  • Some Rust types implement the Copy trait. All primitives and aggregates of primitives are "copy" types. All other types are "move".
  • For all copy types, construction, assignment, and pass-by-value copy the data without transferring ownership, so the source remains valid.
    Figure 1. Str Copy The str type represents a constant literal string in static memory. All strs are accessed through a reference, &str. The reference &str is copy. Copying an &str produces a second reference pointing to the original address.

    Example: &str copy

      let lstrs = "literal string";  // copy type
      let lstrd = lstrs;
      println!("  source: {lstrs:?}, address: {:p}", lstrs);
      println!("  destin: {lstrd:?}, address: {:p}", lstrd);
      nl();
      println!("  Note: a literal string is a fixed value in memory.
      All access occurs through a reference, so copies just copy
      the reference. Both variables point to the same address." 
      );

    Example: Results of &str copy

    --- direct copy &str ---
      source: "literal string", address: 0x7ff7e742b590
      destin: "literal string", address: 0x7ff7e742b590
    
      Note: a literal string is a fixed value in memory.
      All access occurs through a reference, so copies just copy
      the reference. Both variables point to the same address.
    
  • Figure 2. String Move For all move types, construction, assignment, or pass-by-value as a function argument transfers ownership to another variable. A "moved-from" variable is invalid. Any use of it produces a compile error.
    Move operates very efficiently, copying only a few bytes instead of the entire object. When a copy is needed, the developer explicitly clones the original value.

    Example: String Move

    let s = String::from("a string");
    let addrvec = &s;
    let addrzero = std::ptr::addr_of!(s.as_bytes()[0]);
    println!("address of s: {:p}", addrvec);
    println!("address of first byte of s's char buffer: {:p}\n", addrzero);
    
    let t = s;  // move
    let addrvec = &t;
    let addrzero = std::ptr::addr_of!(t.as_bytes()[0]);
    println!("address of t: {:p}", addrvec);
    println!("address of first byte of t's char buffer: {:p}\n", addrzero);

    Example: Results of String Move

    --- demo_move for String ---
    
    --- let s = String::from("a string") ---
    address of s: 0x25758ff558
    address of first byte of s's char buffer: 0x1725ee5dcd0
    
    --- let t:String = s; // move ---
    address of t: 0x25758ff600
    address of first byte of t's char buffer: 0x1725ee5dcd0
    
    Note: s and t are unique objects
    that share same buffer
    but now, s is invalid
    
  • Figure 3. String Clone Move types usually implement the Clone trait with the function clone(). Calling clone produces an independent copy of the source. The original and clone are independent entities with different owners. They hold the same values immediately after the clone operation but may mutate to different values independently.

    Example: String Clone

    let s_src = String::from("a string");
    let s_src_addr = &s_src;
    let s_src_bufaddr = std::ptr::addr_of!(s_src.as_bytes()[0]);
    println!("  s_src: {:?}, address: {:p}", s_src, s_src_addr);
    println!("  s_src_bufaddr: {:p}", s_src_bufaddr);
    let s_cln = s_src.clone();
    let s_cln_addr = &s_cln;
    let s_cln_bufaddr = std::ptr::addr_of!(s_cln.as_bytes()[0]);
    println!("  s_cln: {:?}, address: {:p}", s_cln, s_cln_addr);
    println!("  s_cln_bufaddr: {:p}", s_cln_bufaddr);
    nl();
    println!("  Note: s_src and s_cln have different addresses
    and their buffers have different addresses.
    So they are unique entities.");
    

    Example: Results of String Clone

    --- string clone ---
    s_src: "a string", address: 0x7610d0f390
    s_src_bufaddr: 0x229473682f0
    s_cln: "a string", address: 0x7610d0f448
    s_cln_bufaddr: 0x22947375d00
    
    Note: s_src and s_cln have different addresses
    and their buffers have different addresses.
    So they are unique entities.
    

2.4 Indexing

The array, array slice, and all std::library sequential collections support indexing. Rust tracks index operations in real time. If an out-of-bounds index is used, the current thread of execution "panics", causing an orderly thread termination before any reads or writes execute. That prevents a memory access vulnerability common to other languages.

2.5 Analysis and Display Functions

The "Output" details show output with detailed formatting, but the corresponding code sections do not show the code that produces it. The code above elides the routines that handle formatting and low-level details like type information. The elided code consists of calls to functions shown in the dropdown below. These functions use language features, like generics, that later Bits cover. The complete code, including all elisions, resides in the Bits Repository.
Analysis & Display Function Code 
  #![allow(unused_mut)]
  #![allow(dead_code)]
  #![allow(clippy::approx_constant)]

  /* rust_data::bits_data_analysis.rs */
  /*-----------------------------------------------
    Note:
    Find all Bits code, including this in
    https://github.com/JimFawcett/Bits
    You can clone the repo from this link.
  -----------------------------------------------*/

  use std::fmt::Debug;

  /*-- show_type --------------------------------------
  Shows compiler recognized type and data value
  */
  pub fn show_type<T: Debug>(t: &T, nm: &str) {
    let typename = std::any::type_name::<T>();
    print!("  {nm}, {typename}");
    println!(
      "\n  value: {:?}, size: {}",
        // smart formatting {:?}
      t, std::mem::size_of::<T>()   
        // handles both scalars and collections
    );
  }
  /*---------------------------------------------------
    show string wrapped with long dotted lines above 
    and below
  */
  pub fn show_label(note: &str, n:usize) {
    let mut line = String::new();
    for _i in 0..n {
      line.push('-');
    }
    print!("\n{line}\n");
    print!("  {note}");
    print!("\n{line}\n");
  }
  pub fn show_label_def(note:&str) {
    show_label(note, 50);
  }
  /*---------------------------------------------------
    show string wrapped with dotted lines above 
    and below
  */
  pub fn show_note(note: &str) {
    print!("\n-------------------------\n");
    print!(" {note}");
    print!("\n-------------------------\n");
  }
  /*---------------------------------------------------
    show string wrapped in short lines
  */
  pub fn show_op(opt: &str) {
    println!("--- {opt} ---");
  }
  /*---------------------------------------------------
    print newline
  */
  pub fn nl() {
    println!();
  }
/*---------------------------------------------------
  Show initialization of Rust's types
*/
fn show_formatted<T:Debug>(t:&T, nm:&str) {
  show_op(std::any::type_name::<T>());
  show_type(t, nm);
}
            
show_type and show_formatted are generic functions that accept any type T that implements the Debug trait. GenericRust covers these in detail. Understanding generics is not required to grasp what they do here.
show_type displays the calling name via parameter nm and the type name of t ε T. It also shows the value and size of t. The print format {:?} is very useful because all language and library types provide formatted views of their values. The next Objects Bit shows how user-defined types do that as well. The function std::mem::size_of::<T>() measures the size of the part of t that resides in the calling function's stack frame. It does not evaluate the size of data members residing in the heap.
show_label displays a block of text preceded and followed by demarcation lines of specified size.
show_note does the same with fixed-length lines and additional spaces.
show_op displays a line of text with short dotted lines as prefix and suffix.
nl is shorthand for println!().
show_formatted combines show_op and show_type to display formatted information about a Rust variable t ε T.

3.0 Epilogue

Bits_ObjectsRust revisits most of the operations for language-defined and std::library types discussed on this page, applied to user-defined types.

4.0 References

Link Comments
Crate std Rust documentation covering primitive and library-defined types.
fat pointers Pointers to slices or trait objects hold an address and a length for these dynamically sized types.
Rust Type System #1 Ownership, aliasing, lifetime
Rust Type System #2 Algebraic types, generic associated types
Rust Type System #3 Generic container types, interior mutability