about
RustStory Data
6/8/2022
Rust Story Code

Chapter 2. - Rust Data

Types, type deduction, ownership

2.0  Prologue

Compiler enforced memory safety is one of the primary features of Rust. That is implemented with a strict ownership policy, ensuring that aliasing and mutation do not occur concurrently 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);
By value, we mean the state of an identifier's bound instance. The value of a String, for example, is the collection of all its characters. The issue for memory safety is that changes to the String's state may change its memory allocation if the original allocation did not have enough capacity for the change. Thus all aliases of the identifier responsible for the change will hold invalid references. When new names bind to an existing value, the value will, for blittable types, be copied, and will be moved for non-blittable types. Note that a move is a tranfer of ownership, so the original owner may 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 terminates, the owner's ability to mutate is restored.
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);
Each data may have any number of immutable references, often referred to as shared references. However, only one mutable reference can be taken to data that has no other references. That's referred to as a unique reference. References are often referred to as borrows, as the reference borrows the ability to view or mutate the referend. When an immutable reference is taken on an instance of some type 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. All of these rules are enforced by the Rust compiler. 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, that is, the thread of execution leaves the scope in which x and y are defined, nothing happens other than the stack frame is marked as free and may be over-written at any time due to another stack allocation. When s and t go out of scope, the string Drop trait method is called on t, deallocating its character memory in the heap. The Drop trait method is not called on s since it no longer owns anything on the heap.
Figure 2. String Clone
There is an alternative to that scenario. Many of the stdlibrary types, including String, implement the Clone trait, giving them a clone method. So, a designer can replace a move that invalidates its source with the construction of 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, both s and t are valid, each pointing to their own character allocations on the heap, which, immediately following the clone operation, have the same characters. When s and t go out of scope, the String Drop trait method drop() is called on both s and t because they each own unique character array allocations. User defined types can also implement the clone method by deriving the Clone trait, e.g., #[derive(Clone)] just above the type definition.

2.2  Rust Types and Type Deduction

Rust has a strong type inference engine so you usually don't need to qualify newly created instances with their types as long as they are initialized in the definition. The Rust let declarator works much like the C++ auto declarator. This is illustrated in the code example, below.
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 the basic types including code and output in Chap_2_Data/data_types are shown in the details below.
Basic Types Rust compiler uses type inference, based on literal and previously identified type values in expressions to infer the type of a newly created variable. Code authors can override that with explicit type declarations, either for clarity or to establish a type that has a different size than the expected inferred type. That is illustrated in the example, below. Note that the basic types are blittable, and so 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 in the left block above.
Code above main consists of three functions that help format output into a relatively readable form.

2.2.2  Aggregate Data Types

Rust aggregate types are: arrays, tuples, strings, references, structs, and enums Examples for all of the aggregate types, showing 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 if and only if they have all blittable members, e.g., no Strings, Vecs, ... In that case they can acquire the Copy trait, simply by qualifying them as implementing derived Copy: #[derive(Debug, Copy, Clone)] struct my_struct { ... } For this declaration the compiler generates these traits.
  • Debug allows you to use {:?} in a format which uses a standard formatting process for each of the Rust types.
  • Copy causes the compiler to copy an instance's value by blitting (memcpy) to the new location. The compiler will refuse to derive Copy if any member is non-blittable or the type already implements the Drop trait.
  • Clone is not called implicitly, but a designer can write code that calls clone() and then pays whatever performance penalty accrues for making the copy. If you implement Copy you are also required to implement Clone.
If an aggregate type is non-blittable, then attempting to derive the Copy trait is a compile error. However, you can implement Clone using the clone method on any non-blittable member if the member has the Clone trait.

2.2.3  Slices of Aggregate Types:

A slice is a non-owning view into an aggregate data structure that may or may not be viewing the complete data structure. 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]
It only makes sense to take slices of array-like things, e.g., arrays, vectors, and strings. Also, string slices only make sense if all the string characters are ASCII. We will discuss this further in the next section.

2.2.4  String Types:

Rust provides two native string types: String and str, and two types intended for use with C language bindings: OsString and CString. We will focus here on String and str. The str type is part of the core Rust language and String is provided in the std library. Both contain sequences of utf-8 characters. The size of utf-8 characters ranges from 1 to 4 bytes. But String is implemented using Vec<u8>, so indexing into the Vec only yields a byte which will be a whole character if ASCII, but only part of a character otherwise. That means that you can't index Strings.

2.2.4.1  String

You retrieve the ith utf-8 char from a String, s, using: s.chars().nth(i).unwrap(). chars() is a String iterator that knows how to find utf-8 character boundaries. nth(i) calls next on the iterator i times. That returns a std::option that contains either Some(ch) or None. If the indexing succeeded that returns Some(ch) and we can use ch directly. Note that this is an order N process because we walk down the string looking for character boundaries. Using unwrap() attempts to use the character directly. If indexing failed that would result in a panic. In cases where a panic is not appropriate (flight navigation system for the Boeing 797) we use matching to react to the option. We will discuss option processing in the next chapter, Operations. The method described above works for all utf-8 character sets. However, for languages that use diacritics, the diacritics get encoded in a separate char even though a speaker of the language would say that those are part of an adjacent character in that language (Hindi for example). the chars() iterator is not smart enough to handle that situation. Here's a reference: ch08-02-strings in the Rust Book Note that Strings are not blittable, so rebinding a string to a new name 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 litteral strings like "a literal string". These are implemented with contiguous blocks of memory, often on the stack, and so are blittable. You almost always encounter literal strings as references, &s. strs can be converted to String instances in several ways. Here's two:
  1. let s = String::from("a literal string");
  2. let s = "a literal string".to_string();
And we can create an 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, but, since Rust chars are utf-8 with sizes that range from 1 byte (ASCII characters) to 4 bytes for math symbols and emojis, a partial slice like s2 may not have a correct representation of the second through 4th characters of s. An excellent discussion of utf-8 strings is provided by amos.

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
Here's an example, Chap_2_Data/string_probes showing many of these methods in action:
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 in the block to the right appear above main in demo code. They are used for display to help make program output readable.
The fact that Rust Strings hold utf-8 characters is good news and bad news. The good news is they can represent virtually anything a console can emit, e.g., ASCII chars, math symbols, arabic fonts, european diacritics, and emojis. The bad news is that 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 the fields may be arbitray 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 of these struct types is declared and used in the left and right top panels. Output from running the using code is shown 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

You may be puzzled by the "Impl" method implementations. They add methods to a struct that interact with the struct's fields. That creates a component type. When defining components we usually make the struct public, its fields private, and at least some of its methods public. We will discuss all this in the next two chapters.

2.4  Enumerations

An enumeration is a type identifier with a set of enumeration item fields. Fields 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 in Rust code is the Option: enum Option<T>{ Some(T), None } which can be used as the return value of a function that may or may not generate 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, but is in fact the same type. You construct an alias like this:
  • type PointF = (f64, f64, f64); // tuple of three doubles
  • type VecPoint = Vec<PointF>
Aliases help us provide meaningful application domain names for standard types and shortcuts for long type names. Note that the Rust naming convention 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 There are many other types defined in the stdlib for fs (FileSystem), io, net (TCP, UDP types), process, thread, time, ... In this section we will only look briefly at 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!
The std collections types are all non-blittable and are moved not copied. All implement the Clone trait.

2.7  Epilogue:

This chapter has been all about storing and presenting data in scalar, aggregate, and structured forms. In the next chapter we will be looking at ways to operate on this data with functions, operators, and lambdas.

2.7.1  Exercises:

  1. Construct a vec of i32 elements, populate it with 5 arbitrary elements, then display the value and address of each element. This reference may help. Note that you don't need an unsafe block for this exercise.
  2. Create an instance of std::collections::HashMap. Populate it with information about projects on which you are working. Use project name as the key, and provide a small collection of items about the project, i.e., purpose, programming language, and status. Display the results on the console.
  3. Create an array of Strings, populating with arbitrary values. Convert the array to a Vec.
  4. Construct a str instance and convert it to a String. Evaluate the address of the str, the String, and the first element of the String. Now, convert the String back to another str. Display everything you have built and evaluated.
  5. Declare a struct that has fields to describe your current employment. Display that on the console.
  6. Repeat the last exercise, but use 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.
  Next Prev Pages Sections About Keys