RustStory Repo

Chapter 2. - Rust Data

Types, type deduction, ownership

2.0  Prologue

Compiler-enforced memory safety is a primary Rust feature, implemented through a strict ownership policy that prevents concurrent aliasing and mutation for any instance of a Rust type.
Definition: Alias Two or more identifiers are bound to part or all of the same memory location.
Example: let mut iden1 = vec![1,2,3]; let iden2 = &iden1[1];
Definition: Mutation Operations on an identifier change its bound value.
Example: iden1.push(4);
Value refers to the state of an identifier's bound instance. For a String, that is the collection of all its characters. Changes to String state may change its memory allocation when the original allocation lacks capacity - leaving all aliases of the changed identifier holding invalid references. When new names bind to an existing value, blittable types copy that value and non-blittable types move it. A move transfers ownership, so the original owner can no longer access that value. References support borrowing - providing access to the owning instance's value while suspending the owner's ability to mutate. When borrowing ends, the owner regains the ability to mutate.
References are borrows
/* attempt to mutate after borrow */

let mut s = String::from("s is owner");
slog(&s);

{
  let rs = &s;  // borrow s
  // statement below fails to compile
  // owner can't mutate after borrow
  // s += " with stuff";
  slog(&rs);
}  // borrow ends here

s += " with stuff";
slog(&s);
Data may have any number of immutable references - often called shared references. Only one mutable reference can be taken to data that has no other references; that is called a unique reference. References are often called borrows, because the reference borrows the ability to view or mutate the referend. When an immutable reference is taken on an instance, that instance cannot be mutated until the borrow ends, as shown in the code block to the left. When a mutable reference is taken, only that reference can mutate the instance until the borrow ends, usually at the end of the scope in which the reference is defined. The Rust compiler enforces all of these rules. When a rule is violated, the compiler emits a useful error message that helps a designer fix the violation.

2.1  Data and its Life Cycle

Rust data comes in two flavors (see basic-types for details):
  1. Blittable types:
    Stored entirely in one contiguous block of stack memory.
    • Basic Types:
      u8, i8, u16, i16, u32, i32, u64, i64, usize, isize, f32, f64, bool, char, str
    • Aggregate Types:
      array, tuple, struct if all their items are blittable
    • User-Defined Types that have all blittable members and are marked Copy with #[derive(Copy)]
  2. Non-Blittable types:
    Control block stored in one contiguous block of stack memory with references to data held in the heap.
    • Std Library Types:
      String, Box, Vec, VecDeque, LinkedList, HashMap, HashSet, BTreeMap, BTreeSet, BinaryHeap
    • Aggregate Types:
      array, tuple, struct if each has at least one non-blittable member
    • User-defined types that have at least one non-blittable member
Figure 1. String Move
Blittable data values are copied:
let x = 3.5; Creates an x:f64 on the stack initialized with the value 3.5.
let y = x; Creates an y:f64 on the stack and copies x's value into y. x is still valid.
Non-Blittable data values are moved:
let s = String::from("a string"); Creates an s:String control block on the stack pointing to continguous heap memory containing the characters "a string"
let t = s; Copies the s:String control block to t (still pointing to s's characters) and marks s as moved.
When x and y go out of scope - when the thread of execution leaves the scope in which they are defined - the stack frame is marked as free and may be overwritten by any subsequent stack allocation. When s and t go out of scope, Rust calls the String Drop trait method on t, deallocating its character memory in the heap. Drop is not called on s because s no longer owns anything on the heap. Figure 2. String Clone
An alternative exists. Many standard library types, including String, implement the Clone trait, giving them a clone method. A designer can replace an invalidating move with a clone, like this:
let s = String::from("a string"); Creates an s:String control block on the stack pointing to continguous heap memory containing the characters "a string"
let t = s.clone(); Creates a t:String control block on the stack pointing to continguous heap memory containing a copy of the characters "a string"
Now s and t are both valid, each pointing to separate character allocations on the heap with identical contents immediately after cloning. When s and t go out of scope, Rust calls drop() on both because each owns a unique character array allocation. User-defined types can implement clone by deriving the Clone trait with #[derive(Clone)] above the type definition.

2.2  Rust Types and Type Deduction

Rust has a strong type inference engine. You usually don't need to specify types for newly created instances, as long as they are initialized in the definition. The Rust let declarator works much like C++ auto. The code example below illustrates this.
Type Deduction
Fully qualified vs. deduced types
Output
use std::fmt::{Debug};

#[allow(dead_code)]
pub fn run () {

  /*-- fully specified --*/
  let i:i32 = 5;
  let f:f64 = 3.4;
  let a:[f32; 5] = [1.0, 1.5, 2.0, 1.5, 1.0];
  let t:(i32, f64, String) = (1, 2.0, "three".to_string());
  #[derive(Debug)]
  struct S{i:i32, s:&'static str, };
  let s:S = S{i:15, s:"a literal string" };
  #[derive(Debug)]
  enum E {BS(String), MS(String), PhD(String),};
  let e:E = E::MS("Computer Engineering".to_string());

  print!("\n  -- fully specified types --\n");
  print!("\n  i = {:?}", i);
  print!("\n  f = {:?}", f);
  print!("\n  a = {:?}", a);
  print!("\n  t = {:?}", t);
  print!("\n  s = {:?}", s);
  print!("\n  e = {:?}", e);

  /*-- using type deduction --*/
  let i = 5;
  let f = 3.4;
  let a = [1.0, 1.5, 2.0, 1.5, 1.0];
  let t = (1, 2.0, "three".to_string());
  let s = S{i:15, s:"a literal string" };
  let e = E::MS("Computer Engineering".to_string());

  print!("\n\n  -- using type deduction --\n");
  print!("\n  i = {:?}", i);
  print!("\n  f = {:?}", f);
  print!("\n  a = {:?}", a);
  print!("\n  t = {:?}", t);
  print!("\n  s = {:?}", s);
  print!("\n  e = {:?}", e);
}
C:\github\JimFawcett\RustBasicDemos\rust_probes>
cargo -q run

-- fully specified types --

i = 5
f = 3.4
a = [1.0, 1.5, 2.0, 1.5, 1.0]
t = (1, 2.0, "three")
s = S { i: 15, s: "a literal string" }
e = MS("Computer Engineering")

-- using type deduction --

i = 5
f = 3.4
a = [1.0, 1.5, 2.0, 1.5, 1.0]
t = (1, 2.0, "three")
s = S { i: 15, s: "a literal string" }
e = MS("Computer Engineering")

2.2.1  Basic Data Types

Rust basic types are: i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize f32, f64, char, bool, () The last of these, "()" is the unit type. It represents the absence of a value. Examples of all basic types, with code and output from Chap_2_Data/data_types, appear in the details below.
Basic Types The Rust compiler uses type inference, based on literal values and previously identified types in expressions, to infer the type of a newly created variable. Code authors can override inference with explicit type declarations, for clarity or to specify a type with a different size than the inferred default. The example below illustrates this. The basic types are blittable, so they implement the Copy trait.
Basic Types code from main
Output
title("exploring basic types".to_string());
/*
  Rust basic types:
  i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize
  f32, f64, char, bool, ()
*/
let demo :i8 = 3;
putln(&"let demo :i8 = 3;");
log(&demo);

separator();
let demo = 5;
putln(&"let demo = 5;");
log(&demo);

separator();
let demo :usize = 7;
putln(&"let demo :usize = 7;");
log(&demo);

/* Rust floats: f32, f64 */

separator();
let demo = 3.5;
putln(&"let demo = 3.5;");
log(&demo);

separator();
let demo :f32 = -3.5;
putln(&"let demo :f32 = -3.5;");
log(&demo);

/* Rust chars: char */

separator();
let demo = 'a';
putln(&"let demo = 'a';");
log(&demo);

separator();
let demo :char = 'Z';
putln(&"let demo :char = 'Z';");
log(&demo);

/* Rust boolean: bool */

separator();
let demo = true;
putln(&"let demo = true;");
log(&demo);

separator();
let demo :bool = false;
putln(&"let demo :bool = false");
log(&demo);

/* Rust unit type: () */

separator();
let demo = ();
putln(&"let demo = ();");
log(&demo);

separator();
let demo :() = ();
putln(&"let demo :() = ();");
log(&demo);

C:\github\JimFawcett\RustBasicDemos\data_types>
cargo -q run

 exploring basic types
-----------------------
 let demo :i8 = 3;
 TypeId: i8, size: 1
 value:  3
---------------------------------
 let demo = 5;
 TypeId: i32, size: 4
 value:  5
---------------------------------
 let demo :usize = 7;
 TypeId: usize, size: 4
 value:  7
---------------------------------
 let demo = 3.5;
 TypeId: f64, size: 8
 value:  3.5
---------------------------------
 let demo :f32 = -3.5;
 TypeId: f32, size: 4
 value:  -3.5
---------------------------------
 let demo = 'a';
 TypeId: char, size: 4
 value:  'a'
---------------------------------
 let demo :char = 'Z';
 TypeId: char, size: 4
 value:  'Z'
---------------------------------
 let demo = true;
 TypeId: bool, size: 1
 value:  true
---------------------------------
 let demo :bool = false
 TypeId: bool, size: 1
 value:  false
---------------------------------
 let demo = ();
 TypeId: (), size: 0
 value:  ()
---------------------------------
 let demo :() = ();
 TypeId: (), size: 0
 value:  ()
main.rs
#[allow(unused_imports)]
use display::{ putline, title, show_type, log, putlinen };
use std::fmt::{ Debug, Display };

#[allow(dead_code)]
fn put<T: Display>(value: &T) {
  print!("{}", value);
}

fn putln<T: Display>(value: &T) {
  let mut str_temp = String::new();
  str_temp.push_str("\n  ");
  str_temp.push_str(&value.to_string());
  print!("{}", str_temp);
}

fn separator() {
  put(&"\n ---------------------------------");
}

fn main() {
  /* code elided - see panel above */
}
The function main(), in the block to the left, contains all demonstration code shown above. Three functions above main help format output into a readable form.

2.2.2  Aggregate Data Types

Rust aggregate types are: arrays, tuples, strings, references, structs, and enums Examples for all aggregate types, with code and output from RustStory/Chap_2_Data/aggr_probes:
Aggregate Types
Aggregates Demonstration Code
Output
/*-- create and display basic aggregates -*/

fn basic_aggr() {
  show_title("Demonstrate Rust Aggregates");

  /*-- array --*/
  show_label("arrays");
  show_op("let mut arr:[i32; 5] = [1, 2, 3, 4, 5]");
  let mut arr: [i32; 5] = [1, 2, 3, 4, 5];
  show_type(&arr);
  show_value(&arr);
  show_op("arr[1] = -2");
  arr[1] = -2;
  show_value(&arr);
  println!();

  /*-- slice --*/
  show_label("slices");
  show_op("let slc = &mut arr[1..4]");
  let slc = &mut arr[1..4];
  show_type(&slc);
  show_value(&slc);
  show_op("slc[0] = 0");
  slc[0] = 0;
  show_value(&slc);
  show_op("value of array is now:");
  show_value(&arr);
  println!();

  /*-- tuple --*/
  show_label("tuples");
  show_op("let tpl = (42, 'z', \"abc\", 3.14159)");
  #[allow(clippy::approx_constant)]
  let tpl = (42, 'z', "abc", 3.14159);
  show_type(&tpl);
  show_value(&tpl);
  show_op("value of second element is:");
  show_value(&tpl.1);
  println!();

  /*-- string --*/
  show_label("strings");
  show_op("let s = \"a string\".to_string()");
  let mut s = "a string".to_string();
  show_type(&s);
  show_value(&s);
  show_op("s.push_str(\" plus more\")");
  s.push_str(" plus more");
  show_value(&s);
  println!();

  /*-- reference --*/
  show_label("references");
  show_op("let r = &s");
  let r = &s;
  show_type(&r);
  show_value(&r);
  println!();

  /*-- struct --*/
  show_label("structures");
  #[derive(Debug)]
  struct DemStr { i:i32, c:char, d:f64, }
  show_op("let st = DemStr { i:1, c:'a', d:0.333 }");
  let st = DemStr { i:1, c:'a', d:0.333 };
  show_type(&st);
  show_value(&st);
  let second = st.c;
  show_op("let second = st.c");
  show_value(&second);
  println!();

  /*-- enum --*/
  show_label("enumerations");

  #[derive(Debug)]
  enum LangAge { Recent, Ancient }

  #[derive(Debug)]
  enum Langs {
    Rust(LangAge), Fortran(LangAge)
  }

  let a_lang = Langs::Rust(LangAge::Recent);
  show_type(&a_lang);
  show_value(&a_lang);

  let old_lang = Langs::Fortran(LangAge::Ancient);
  show_type(&old_lang);
  show_value(&old_lang);

  /*-- matching requires handling all branches --*/
  match a_lang {
    Langs::Rust(LangAge::Recent) => { println!("  Rust is recent"); }
    Langs::Rust(LangAge::Ancient) => { println!("  Rust is ancient"); }
    Langs::Fortran(LangAge::Recent) => { println!("  Fortran is recent"); }
    Langs::Fortran(LangAge::Ancient) => { println!("  Fortran is ancient"); }
  }
  /*-------------------------------------------------------
    if let can examine one branch and provide
    blanket handling for others
  */
  if let Langs::Rust(LangAge::Recent) = a_lang {
    println!("  Rust was stablized in 2015")
  } else {
    println!("  this language isn't very interesting");
  }
}

fn move_copy() {
  show_title("Demonstrate Copy and Move");

  show_label("copy array of integers");
  show_op("let arri = [ 1, 2, 3, 2, 1]");
  let arri = [ 1, 2, 3, 2, 1];
  show_value(&arri);
  show_op("let carri = arri");
  let carri = arri;
  show_value(&carri);
  // the next statement succeeds because arri was copied
  // println!("{arri:?}");
  println!();

  show_label("copy array of &strs");
  show_op("let arri = [ \"1\", \"2\", \"3\", \"2\", \"1\"]");
  let arri = [ "1", "2", "3", "2", "1"];
  show_value(&arri);
  show_op("let carri = arri");
  let carri = arri;
  show_value(&carri);
  // the next statement succeeds because arri was copied
  // println!("{arri:?}");
  println!();

  show_label("move array of Strings");
  show_op(
    "let arri = [\"1\".to_owned(), \"2\".to_owned(),
    \"3\".to_owned(), \"2\".to_owned(), \"1\".to_owned()])"
  );
  /*------------------------------------------------------
    to_owned() converts copy type &str
    to move type String
  */
  let arri = [
    "1".to_owned(), "2".to_owned(), "3".to_owned(),
    "2".to_owned(), "1".to_owned()
  ];
  show_value(&arri);
  show_op("let carri = arri");
  let carri = arri;
  show_value(&carri);
  // the next statement fails because arri was moved
  // println!("{arri:?}");
  println!("  arri moved so no longer valid\n");
  println!("  an aggregate of all copy types is copy");
  println!("  an aggregate with at least one move type element is move");
}


-----------------------------
 Demonstrate Rust Aggregates
-----------------------------

 arrays
--------
--- let mut arr:[i32; 5] = [1, 2, 3, 4, 5] ---
  TypeId: [i32; 5], size: 20
  value: [1, 2, 3, 4, 5]
--- arr[1] = -2 ---
  value: [1, -2, 3, 4, 5]



  
 slices
--------
--- let slc = &mut arr[1..4] ---
  TypeId: &mut [i32], size: 16
  value: [-2, 3, 4]
--- slc[0] = 0 ---
  value: [0, 3, 4]
--- value of array is now: ---
  value: [1, 0, 3, 4, 5]




 tuples
--------
--- let tpl = (42, 'z', "abc", 3.14159) ---
  TypeId: (i32, char, &str, f64), size: 32
  value: (42, 'z', "abc", 3.14159)
--- value of second element is: ---
  value: 'z'




 strings
---------
--- let s = "a string".to_string() ---
  TypeId: alloc::string::String, size: 24
  value: "a string"
--- s.push_str(" plus more") ---
  value: "a string plus more"




 references
------------
--- let r = &s ---
  TypeId: &alloc::string::String, size: 8
  value: "a string plus more"



 structures
------------
--- let st = DemStr { i:1, c:'a', d:0.333 } ---
  TypeId: aggr_probes::basic_aggr::DemStr, size: 16
  value: DemStr { i: 1, c: 'a', d: 0.333 }
--- let second = st.c ---
  value: 'a'






 enumerations
--------------
  TypeId: aggr_probes::basic_aggr::Langs, size: 2
  value: Rust(Recent)
  TypeId: aggr_probes::basic_aggr::Langs, size: 2
  value: Fortran(Ancient)
  Rust is recent
  Rust was stablized in 2015






















  





---------------------------
 Demonstrate Copy and Move
---------------------------

 copy array of integers
------------------------
--- let arri = [ 1, 2, 3, 2, 1] ---
  value: [1, 2, 3, 2, 1]
--- let carri = arri ---
  value: [1, 2, 3, 2, 1]



 copy array of &strs
---------------------
--- let arri = [ "1", "2", "3", "2", "1"] ---
  value: ["1", "2", "3", "2", "1"]
--- let carri = arri ---
  value: ["1", "2", "3", "2", "1"]






 move array of Strings
-----------------------
--- let arri = ["1".to_owned(), "2".to_owned(),
    "3".to_owned(), "2".to_owned(), "1".to_owned()]) ---
  value: ["1", "2", "3", "2", "1"]
--- let carri = arri ---
  value: ["1", "2", "3", "2", "1"]
  arri moved so no longer valid

  an aggregate of all copy types is copy
  an aggregate with at least one move type element is move

Aggregate types are blittable only when all members are blittable - no Strings, Vecs, etc. In that case they can acquire the Copy trait by declaring derived Copy: #[derive(Debug, Copy, Clone)] struct my_struct { ... } For this declaration, the compiler generates these traits.
  • Debug enables the {:?} format specifier, which uses a standard formatting process for each Rust type.
  • Copy causes the compiler to copy an instance's value by blitting (memcpy) to the new location. The compiler refuses to derive Copy if any member is non-blittable or the type already implements Drop.
  • Clone is not called implicitly. A designer can call clone() explicitly, paying whatever performance penalty the copy requires. Implementing Copy also requires implementing Clone.
Attempting to derive Copy on a non-blittable aggregate type is a compile error. However, you can implement Clone using the clone method on any non-blittable member that has the Clone trait.

2.2.3  Slices of Aggregate Types:

A slice is a non-owning view into an aggregate data structure, covering part or all of it. Consider an array: let arr = [1, 2, 3, 4, 5, 6];
  • let slc1 = &arr[..]; // view the entire array
  • let slc2 = &arr[0..6]; // same as slc1
  • let slc3 = &arr[..3]; // views elements [1, 2, 3]
  • let slc4 = &arr[1..]; // views elements [2, 3, 4, 5, 6]
  • let slc5 = &arr[1..4]; // views elements [2, 3, 4]
Slices apply to array-like types - arrays, vectors, and strings. String slices are only reliable when all characters are ASCII; we discuss this further in the next section.

2.2.4  String Types:

Rust provides two native string types: String and str, and two types for C language bindings: OsString and CString. This section focuses on String and str. The str type is part of core Rust; String is provided by the std library. Both hold sequences of utf-8 characters, which range from 1 to 4 bytes in size. String is implemented using Vec<u8>, so indexing yields a byte - a whole character only for ASCII, and only part of a character for multi-byte encodings. You cannot directly index Strings.

2.2.4.1  String

To retrieve the ith utf-8 char from String s, use: s.chars().nth(i).unwrap(). chars() is an iterator that finds utf-8 character boundaries. nth(i) calls next i times and returns a std::option containing Some(ch) or None. Successful indexing returns Some(ch), which can be used directly. This is an O(N) process because it walks the string to find character boundaries. unwrap() extracts the character directly, panicking on failure. When a panic is not appropriate, use matching to handle the option. Option processing is discussed in the next chapter, Operations. This method works for all utf-8 character sets. For languages that use diacritics, though, diacritics encode as separate chars even though native speakers consider them part of an adjacent character (Hindi, for example). The chars() iterator does not handle that case. Here's a reference: ch08-02-strings in the Rust Book Strings are not blittable. Rebinding a string transfers ownership, and taking a borrow reference suspends the owner's ability to mutate until the borrow ends.

2.2.4.2  str

The str type represents literal strings like "a literal string". Str instances are contiguous blocks of memory, often on the stack, and so are blittable. You almost always encounter them as references, &s. Str instances convert to String in several ways. Two options:
  1. let s = String::from("a literal string");
  2. let s = "a literal string".to_string();
You can create a str by taking a slice of a String or a literal string:
  1. let s1 = "Hello world"; // slice of the whole literal
  2. let s2 = &s[1..3]; // second through 4th bytes of s
Both s1 and s2 have type &str, a reference to a literal string.
Taking a complete slice always works. Partial slices like s2 may not correctly represent characters because Rust chars are utf-8 with sizes ranging from 1 byte (ASCII) to 4 bytes (math symbols and emojis). amos provides an excellent discussion of utf-8 strings.

2.2.5  String Examples:

The String type has methods:
  • let s = String::new();
    Creates new empty String instance
  • let s = String::from("a literal");
    Creates instance from literal
  • let t = s.replace("abc","xyz");
    t is a copy of s with every instance of "abc" replaced with "xyz"
  • s.len();
    returns length of s in bytes, not chars
  • let slice = s.as_str();
    returns slice of entire String s contents
  • s.push('a');
    append char 'a' to end of s.
  • s.push_str("abc");
    appends "abc" to the end of s
  • let st = s.trim();
    returns string with leading and trailing whitespace removed.
  • let iter = s.split_whitespace();
    returns iterator over whitespace separated tokens
  • let iter = s.split('\n');
    returns iterator over lines
  • let iter = s.chars();
    returns an iterator over the utf-8 chars of s
This example, Chap_2_Data/string_probes, demonstrates many of these methods:
String Examples:
String Demonstration Code
Output
fn main() {

  main_title("string_probes");
  putlinen(2);

  /*-- char --*/

  show_op("let v = vec!['R', 'u', 's', 't']");
  let v:Vec<char> = vec!['R', 'u', 's', 't'];
  log(&v);
  log(&'R');
  putlinen(2);

  show_op("let ch = 'a' as u8");
  let ch:u8 = 'a' as u8;
  log(&ch);
  show("char is ", &(ch as char));
  putlinen(2);

  /*-- String --*/

  show_op("let s = String::from(\"Rust\")");
  let s:String = String::from("Rust");
  log(&s);
  let i:usize = 2;
  let ch = at(&s, i);
  print!("\n  in string \"{}\", char at {} is {}", &s, i, ch);
  show("length in bytes of s = {:?}", &s.len());
  putlinen(2);

  show_op("let v = Vec::from(s.clone())");
  let s1 = s.clone();
  let v:Vec<u8> = Vec::from(s1);
  log(&v[0]);
  show("vec from string",&v);
  putlinen(2);

  /*-----------------------------------------------------
    Displaying emoji's to illustrate the potential
    of using utf-8.
  */
  show_op("displaying emoji's");
  let mut s2 = String::new();
  s2.push_str("\u{1F600}");
  s2.push('\u{1F601}');
  s2.push('\u{1F602}');
  s2.push('\u{1F609}');
  print!("\n  {}", s2);
  print!("\n  {}", '\u{1F601}');
  putlinen(2);

  /*-- str --*/

  show_op("let s_slice = &s[..]");
  let s_slice = &s[..];   // slice containing all chars of s
  log(&s_slice);
  show("s_slice = ", &s_slice);
  putlinen(2);

  show_op("let s_slice2 = s.as_str()");
  let s_slice2 = s.as_str();
  log(&s_slice2);
  putlinen(2);

  /*-- create string and mutate --*/

  show_op("let mut s = string::new()");
  let mut s = String::new();
  s.push('a');
  s.push(' ');
  s.push_str("test string");
  log(&s);
  putlinen(2);

  show_op("let t = s.replace(from: \"string\", to: \"Rust String\"");
  let t = s.replace("string","Rust String");
  log(&t);
  putlinen(2);

  show_op("tok in s.split_whitespace()");
  for tok in s.split_whitespace() {
    print!("\n  {}", tok);
  }
  putline();

  /*-----------------------------------------------------
     Another, order n, way to index string:
    - chars returns iterator over utf8 chars in string slice
    - nth(i) calls next on iterator until it gets to i
    - nth(i) returns std::option::Option<char>:
       - that contains Some(ch) or None if operation failed
  */
  show("\n  s = ", &s);
  putline();
  show_op("let result = s.chars().nth(0)");
  putline();
  let result = s.chars().nth(0);
  match result {
    Some(r) => show("  s.chars().nth(0) = ", &r),
    None => print!("\n  couldn't extract char"),
  }
  putline();
  show_op("let result = s.chars().nth(2)");
  putline();
  let result = s.chars().nth(2);
  match result {
    Some(r) => show("  s.chars().nth(2) = ", &r),
    None => print!("\n  couldn't extract char"),
  }
  putlinen(2);

  {
    /*-------------------------------------------------
       Caution here:
       - slice is returning array of bytes, not utf8 chars
       - this works only because we use all ASCII chars
    */
    /*-- slices are non-owning views and are borrows of s --*/
    show_op("let slice_all = &s");
    let slice_all = &s;
    log(&slice_all);
    show("slice_all = ", &slice_all);
    putlinen(2);

    show_op("let third = &s[2..3]");
    let third = &s[2..3];       // string slice with one char
    log(&third);
    show("\n  third = ",&third);
    putlinen(2);

    /*-- this works for utf-8 encoding --*/
    show_op("let ch = third.chars().nth(0)");
    let ch = third.chars().nth(0);  //
    log(&ch);
    match ch {
      Some(x) => { log(&x); show("\n  match ch = ", &x); },
      None => print!("\n can't return ch"),
    }

    ///////////////////////////////////////////////////
    // compile fails
    // - can't modify owner while borrows are active
    //------------------------------------------------
    // s.push('Z');
    // log(&slice_all);

  }   // elem borrow ends here

  s.push('Z');  // ok, borrows no longer active
  putlinen(2);

  /* format_args! macro */

  show_op("let s = std::fmt::format(format_args!(...))");
  let s = std::fmt::format(format_args!("\n  {}, {}, {}", 1, 2, 3.5));
  put_str(&s);
  put(&s);
  putlinen(2);

  show_op("struct S { x:i32, y:f64, s:String, }");
  #[allow(dead_code)]
  #[derive(Debug)]
  struct S {x:i32, y:f64, s:String, }
  let st:S = S { x:3, y:4.2, s:"xyz".to_string() };
  put("\n  ");
  putdb(&st);
  putline();

  sub_title("That's all Folks!");
  putlinen(2);
}


  string_probes
 ===============



--- let v = vec!['R', 'u', 's', 't'] ---
  TypeId: alloc::vec::Vec, size: 24
  value:  ['R', 'u', 's', 't']
  TypeId: char, size: 4
  value:  'R'

--- let ch = 'a' as u8 ---
  TypeId: u8, size: 1
  value:  97char is 'a'





--- let s = String::from("Rust") ---
  TypeId: alloc::string::String, size: 24
  value:  "Rust"
  in string "Rust", char at 2 is slength in bytes of s = {:?}4





--- let v = Vec::from(s.clone()) ---
  TypeId: u8, size: 1
  value:  82vec from string[82, 117, 115, 116]








--- displaying emoji's ---
  😀😁😂😉
  😁









--- let s_slice = &s[..] ---
  TypeId: &str, size: 16
  value:  "Rust"s_slice = "Rust"


--- let s_slice2 = s.as_str() ---
  TypeId: &str, size: 16
  value:  "Rust"





--- let mut s = string::new() ---
  TypeId: alloc::string::String, size: 24
  value:  "a test string"





--- let t = s.replace(from: "string", to: "Rust String" ---
  TypeId: alloc::string::String, size: 24
  value:  "a test Rust String"


--- tok in s.split_whitespace() ---
  a
  test
  string









  s = "a test string"
--- let result = s.chars().nth(0) ---
  s.chars().nth(0) = 'a'
--- let result = s.chars().nth(2) ---
  s.chars().nth(2) = 't'





















--- let slice_all = &s ---
  TypeId: &alloc::string::String, size: 8
  value:  "a test string"slice_all = "a test string"



--- let third = &s[2..3] ---
  TypeId: &str, size: 16
  value:  "t"
  third = "t"



--- let ch = third.chars().nth(0) ---
  TypeId: core::option::Option, size: 4
  value:  Some('t')
  TypeId: char, size: 4
  value:  't'
  match ch = 't'
















--- let s = std::fmt::format(format_args!(...)) ---
  1, 2, 3.5
  1, 2, 3.5



--- struct S { x:i32, y:f64, s:String, } ---
  S { x: 3, y: 4.2, s: "xyz" }


  That's all Folks!
 -------------------

Functions defined above main()
/////////////////////////////////////////////////////////////
// string_probes::main.rs - basic string operations        //
//                                                         //
// Jim Fawcett, https://JimFawcett.github.io, 25 Feb 2020  //
/////////////////////////////////////////////////////////////

#[allow(unused_imports)]
use display::{
  log, slog, show, show_type, show_value,
  putline, putlinen, main_title, sub_title
};
#[allow(unused_imports)]
use std::fmt::{ Debug, Display };

fn show_op(s:&str) {
  let strg = "--- ".to_owned() + s + " ---";
  print!("{}", strg);
}

fn put<T>(t:T) where T:Display {
  print!("{}", t);
}

fn putdb<T>(t:T) where T:Debug {
  print!("{:?}", t);
}

fn put_str(s:&String) {
  print!("{}",s);
}
/*-----------------------------------------------------------
   Note:
   Strings hold utf8 characters, which vary in size, so you
   you can't directly index String instances.
*/
#[allow(dead_code)]
pub fn at(s:&String, i:usize) -> char {
  s.chars().nth(i).unwrap()
}
/*-----------------------------------------------------------
   note:
   - order n, as str chars are utf8, e.g., from 1 to 5 bytes
   - this ugliness is one way to index
   - see below for another, not much better way
*/
#[allow(dead_code)]
pub fn vectorize(s: &str) -> Vec<char> {
  s.chars().collect::<Vec<char>>()
}
/*-- note: order n, from vectorize -- prefer at, above --*/
#[allow(dead_code)]
pub fn get_char(s:&str, i:usize) -> char {
    vectorize(s)[i]
}
/*-- stringize - order n --*/
#[allow(dead_code)]
pub fn stringize(v: &Vec<char>) -> String {
  return v.into_iter().collect()
}
The functions shown above main in demo code help format program output into a readable form.
Rust Strings holding utf-8 characters is both good and bad. On the good side, they represent virtually anything a console can emit - ASCII chars, math symbols, Arabic fonts, European diacritics, and emojis. On the bad side, you can't index a Rust String in constant time, and converting to other data structures can get messy.

2.3  Structs

In Rust, most structs are aggregates of one or more fields where fields may be arbitrary types, named types, or unit type:
  1. StructExprStruct:
    struct Person {
      name:String, occupation:String, age:u32,
    }
    
  2. StructExprTuple:
    struct Person (
      String, String, u32,
    )
    
  3. StructExprUnit:
    struct Person;
                    
Each struct type appears in the top panels - declarations on the left, usage on the right. Output from running the code appears in the bottom right panel.
Define Structs
Use Structs
#[allow(unused_imports)]
use display::{*};
use std::fmt;

/*-- ExprStruct struct --*/
#[derive(Debug)]
struct Person1 {
  name:String, occup:String, id:u32,
}
#[allow(dead_code)]
impl Person1 {
  fn show(&self) {
    print!("\n  Person1: {:?}", &self);
  }
}
/*-- ExprTuple struct --*/
#[derive(Debug)]
struct Person2 (
  String, String, u32
);
#[allow(dead_code)]
impl Person2 {
  fn show(&self) {
    print!("\n  Person2: {:?}", &self);
  }
}
/*-- ExprUnit struct --*/
#[derive(Debug)]
struct Person3;
#[allow(dead_code)]
impl Person3 {
  fn show(&self) {
    print!("\n  Person3");
  }
}
sub_title("Demonstrating Basic Structs");
let p1 = Person1 {
  name:"Jim".to_string(),
  occup:"dev".to_string(),
  id:42
};
p1.show();
let p2 = Person2 {
  0:"Jim".to_string(),
  1:"dev".to_string(),
  2:42
};
p2.show();
let p3 = Person3;
p3.show();
putline();
Output
Demonstrating Basic Structs
-----------------------------
Person1: Person1 { name: "Jim", occup: "dev", id: 42 }
Person2: Person2("Jim", "dev", 42)
Person3

The "Impl" method implementations add methods to a struct that interact with its fields, creating a component type. When defining components, make the struct public, the fields private, and at least some methods public. The next two chapters cover this in detail.

2.4  Enumerations

An enumeration is a type identifier with a set of enumeration item fields, which may be:
  • ItemDiscriminant: a named integral value enum Names { John, Sally = 35, Roger };
  • ItemTuple: a named tuple with items specified by type enum Names { Alok(String, f64), Priya(String, f64), Ram(String, f64) };
  • ItemStruct: a named struct with items specified by name and type enum Names { Jun { occupation: String, age: f64 }, Xing { occupation: String, age: f64 }, Shi { occupation: String, age: f64 }, }
Enumerations can be generic. A common example is Option: enum Option<T>{ Some(T), None } which serves as the return value of functions that may or may not produce a result. Here are some examples:
Enumeration Examples
Enumeration Example Code
Output
// enum_probes::main.rs

use display::{*};
use std::fmt::{Debug};

#[allow(dead_code)]
#[derive(Debug)]
enum Name { John, Jim=42, Jack }

#[allow(dead_code)]
#[derive(Debug)]
enum NameTuple {
    John(String, u32), Jim(String, u32), Jack(String, u32)
}

#[allow(dead_code)]
#[derive(Debug)]
enum NameStruct {
    John { occup:String, id:u32 },
    Jim  { occup:String, id:u32 },
    Jack { occup:String, id:u32 }
}
fn main() {

    main_title("Demonstrating enum_probes");
    print!("\n  - enumerations, match, if let");
    putline();

    /*-- enum discriminant --*/
    sub_title("  -- enum discriminant --  ");
    let test = Name::Jim;
    match test {
        Name::John => {
            let john_discriminant = Name::John as u32;
            print!(
            "\n  I am John. my discriminant is {:?}",
            john_discriminant
        )},
        Name::Jim => {
            let jim_discriminant = Name::Jim as u32;
            print!(
            "\n  I am Jim. my discriminant is {:?}",
            jim_discriminant
        )},
        Name::Jack => {
            let jack_discriminant = Name::Jack as u32;
            print!(
            "\n  I am Jack. my discriminant is {:?}",
            jack_discriminant
        )},
    }
    putline();

    let test1 = Name::John;
    let test2 = Name::Jim;
    let test3 = Name::Jack;

    if let Name::Jack = test1 {
        print!("\n  I am John");
    }
    else {
        print!("\n  I am not John");
    }
    if let Name::Jack = test2 {
        print!("\n  I am Jim");
    }
    else {
        print!("\n  I am not Jim");
    }
    if let Name::Jack = test3 {
        print!("\n  I am Jack");
    }
    else {
        print!("\n  I am not Jack");
    }
    putline();

    /*-- enum tuple --*/
    sub_title("  -- enum tuple --  ");
    let value = NameTuple::John("pilot".to_string(), 52);
    if let NameTuple::John(occup, id) = value {
        print!(
            "
  my name is John
  occupupation is {}
  id is {}", occup, id
        );
    }
    putline();

    /*-- enum struct --*/
    sub_title("  -- enum struct --  ");
    let value = NameStruct::Jack { occup:"plumber".to_string(), id:32 };
    match value {
        NameStruct::Jack {occup, id} => print!("\n Jack - occup: {}, id: {}", occup, id),
        _ => print!("\n  not Jack")
    }
    putline();

    println!("\n\nThat's all Folks!\n");
}
























  Demonstrating enum_probes
 ===========================
  - enumerations, match, if let

    -- enum discriminant --
 -----------------------------
  I am Jim. my discriminant is 42

  I am not John
  I am not Jim
  I am Jack











































  -- enum tuple --
 ----------------------
  my name is John
  occupupation is pilot
  id is 52

  
  
  
  
  
  
  
  -- enum struct --
 -----------------------
 Jack - occup: plumber, id: 32


That's all Folks!

  

2.5  Type Aliases

Type aliases provide an alternate name for an existing type while remaining the same type. Construct an alias like this:
  • type PointF = (f64, f64, f64); // tuple of three doubles
  • type VecPoint = Vec<PointF>
Aliases provide meaningful application domain names for standard types and shortcuts for long type names. Note that Rust uses snake_case for functions and CamelCase for types.

2.6  Std Lib Data Types

The std collections types are: Vec, VecDeque, LinkedList, HashMap, HashSet, BTreeMap, BTreeSet, BinaryHeap The stdlib defines many other types: fs (FileSystem), io, net (TCP, UDP), process, thread, time, and more. This section covers Vec, VecDeque, and HashMap.
StdLib Data Types
stdlib Examples
Output
fn main() {

  show_title("Demonstrate std Library Types");
  use std::collections::{VecDeque, HashMap};

  show_label("std::Vec<T>");

  show_op("let mut vi = vec![1, 2, 3, 2, 1]");
  let mut vi = vec![1, 2, 3, 2, 1];
  show_type(&vi);
  show_value(&vi);
  show_op("vi[1] = -2");
  vi[1] = -2;
  show_value(&vi);
  show_op("vi.push(0)");
  vi.push(0);
  show_value(&vi);
  show_op("vi.insert(1, 42)");
  vi.insert(1, 42);
  show_value(&vi);
  println!();

  show_label("VecDeque<T>");
  show_op("let mut vdeq = VecDeque::<f64>::new()");
  let mut vdeq = VecDeque::<f64>::new();
  show_type(&vdeq);
  show_value(&vdeq);
  show_op("vdeq.push_back(2.5)");
  vdeq.push_back(2.5);
  show_op("vdeq.push_front(1.0)");
  vdeq.push_front(1.0);
  show_value(&vdeq);
  println!();

  show_label("HashMap<K, V>");
  show_op("let mut hm = HashMap::<i32, &str>::new()");
  let mut hm = HashMap::<i32, &str>::new();
  show_type(&hm);
  show_value(&hm);
  show_op("hm.insert(1,\"one\")");
  hm.insert(1,"one");
  show_value(&hm);
  hm.insert(0,"zero");
  show_value(&hm);
  hm.insert(2,"two");
  show_value(&hm);
  hm.insert(-2,"minus two");
  show_value(&hm);
  show_op("hm.remove(&0)");
  hm.remove(&0);
  show_value(&hm);
  /*
    using entry API for HashMap
    - if the key exists then modify the value
      with a closure
  */
  show_op("hm.entry(1).and_modify(|v| *v = \"the number 1\")");
  hm.entry(1).and_modify(|v| *v = "the number 1");
  show_value(&hm);

  println!("\n  That's all Folks!");
}

-------------------------------
 Demonstrate std Library Types
-------------------------------

 std::Vec<T>
-------------
--- let mut vi = vec![1, 2, 3, 2, 1] ---
  TypeId: alloc::vec::Vec, size: 24
  value: [1, 2, 3, 2, 1]
--- vi[1] = -2 ---
  value: [1, -2, 3, 2, 1]
--- vi.push(0) ---
  value: [1, -2, 3, 2, 1, 0]
--- vi.insert(1, 42) ---
  value: [1, 42, -2, 3, 2, 1, 0]





 VecDeque<T>
-------------
--- let mut vdeq = VecDeque::::new() ---
  TypeId: alloc::collections::vec_deque::VecDeque, size: 32   
  value: []
--- vdeq.push_back(2.5) ---
--- vdeq.push_front(1.0) ---
  value: [1.0, 2.5]





 HashMap
---------------
--- let mut hm = HashMap::::new() ---
  TypeId: std::collections::hash::map::HashMap, size: 48
  value: {}
--- hm.insert(1,"one") ---
  value: {1: "one"}
  value: {1: "one", 0: "zero"}
  value: {1: "one", 2: "two", 0: "zero"}
  value: {-2: "minus two", 1: "one", 2: "two", 0: "zero"}
--- hm.remove(&0) ---
  value: {-2: "minus two", 1: "one", 2: "two"}
--- hm.entry(1).and_modify(|v| *v = "the number 1") ---
  value: {-2: "minus two", 1: "the number 1", 2: "two"}

  That's all Folks!
All std collections types are non-blittable - they move, not copy. All implement the Clone trait.

2.7  Epilogue:

This chapter covered storing and presenting data in scalar, aggregate, and structured forms. The next chapter examines ways to operate on this data with functions, operators, and lambdas.

2.7.1  Exercises:

  1. Construct a Vec<i32>, populate it with 5 arbitrary elements, and display the value and address of each element. This reference may help. No unsafe block is required.
  2. Create a std::collections::HashMap and populate it with information about projects you are working on. Use the project name as the key; provide purpose, programming language, and status as associated items. Display the results on the console.
  3. Create an array of Strings with arbitrary values and convert it to a Vec.
  4. Construct a str instance and convert it to a String. Evaluate the addresses of the str, the String, and the first element of the String. Convert the String back to another str. Display everything you built and evaluated.
  5. Declare a struct with fields describing your current employment and display it on the console.
  6. Repeat the last exercise using a tuple. Use type aliases to make the tuple understandable.

2.7.2  References:

Reference Link Description
Character sets The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Escuses!) [author's title] - Joel Spolsky
utf-8 Strings - amos Illustrating how utf-8 strings work with C and with Rust code.
Rust Strings Rust Strings are implemented with Vec<u8> but interpreted as utf-8 chars
regex Crate Rust regex crate provides facilities for parsing, compiling, and executing regular expressions.
Rust Lifetimes Very clear presentation of borrow lifetimes.
Rust Reference: Structs Rust Reference is the official language definition - surprisingly readable.
Rust Containers Container diagrams
rust-lang.org home page Links to download and documentation
Tutorial - tutorialspoint.com Tutorials for most of the Rust parts with code examples.