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.
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):
-
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
-
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
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.
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.
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 types vs. deduced types
//mod types;
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);
}
Output:
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.
Basic Types
Basic Types code from main:
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);
main.rs - defines put, putln, and separator
#[allow(unused_imports)]
use display::{putline, title, show_type, log, putlinen};
use std::fmt::{Debug, Display};
use std::any::Any;
#[allow(dead_code)]
fn put<T: Any + Debug + Display>(value: &T) {
print!("{}", value);
}
fn putln<T: Any + Debug + 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
}
Output for Basic Types:
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: ()
The basic types are blittable, and so implement the Copy trait.
2.2.2 Aggregate Data Types
Rust aggregate types are:
arrays, tuples, strings, references, structs, and enums
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);
Examples for all of the aggregate types, showing code and output from
RustBasicDemos: data_types:
Aggregate Types
Aggregate Types code from main
// code elided
/* Rust arrays: [1,2,3] */
separator();
let demo = [1, 2, 3];
putln(&"let demo = [1, 2, 3];");
log(&demo);
separator();
let demo = [(1.5, 2.3), (3.1, 3.9)];
putln(&"let demo = [(1.5, 2.3), (3.1, 3.9)];");
log(&demo);
/* Rust tuples: (1,'z',3.5) */
separator();
let demo = (1, 2.5, (1,'a'), [1,2,3]);
putln(&"let demo = (1, 2.5, (1,'a'), [1,2,3]);");
log(&demo);
/* Rust Strings: String::from("a string") */
separator();
let demo = String::from("a demo String");
putln(
&"let demo = String::from(\"a demo String\");"
);
log(&demo);
/* Rust references: */
separator();
let mut demo = String::from("a demo String");
putln(
&"let demo = String::from(\"a demo String\");"
);
log(&demo);
let rdemo = &mut demo;
putln(&"let rdemo = &mut demo;");
putln(&"redemo.push_str(\" more stuff\")");
rdemo.push_str(" more stuff");
// compile fails - this is attemp to make second
// mutable borrow because push_str takes &self
// demo.push_str(" still more stuff");
log(rdemo);
putln(&"demo.push_str(\" still more stuff\"");
// this call succeeds only because rdemo
// is not used below call
demo.push_str(" still more stuff");
log(&demo);
// log(rdemo); // if this call is uncommented
// both calls above fail
putline();
/* 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);
putline();
/*-------------------------------------------
Rust structs:
struct Point { x:f64, y:f64, z:f64, t:}
*/
separator();
#[derive(Debug)]
struct Point {
x:f64, y:f64, z:f64, name:String,
}
let demo = Point {
x:1.5, y:2.5, z:3.7,
name:String::from("Peter")
};
putln(
&"struct Point {
x:f64, y:f64, z:f64, name:String,
}"
);
putln(
&"let demo = Point {
x:1.5, y:2.5, z:3.7,
name:String::from(\"Peter\") };"
);
log(&demo);
/* Rust enums: enum POS { bs, ms, phd } */
separator();
#[derive(Debug)]
#[allow(dead_code)]
enum POS {
BS(String), MS(String), PhD(String),
};
let demo = POS::MS(
String::from("Computer Engineering")
);
putln(
&"enum POS {
BS(String), MS(String), PhD(String),
};"
);
putln(
&"let demo =
POS::MS(
String::from(\"Computer Engineering\")
);"
);
log(&demo);
// code elided
Output for Aggegate Types
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: ()
---------------------------------
let demo = [1, 2, 3];
TypeId: [i32; 3], size: 12
value: [1, 2, 3]
---------------------------------
let demo = [(1.5, 2.3), (3.1, 3.9)];
TypeId: [(f64, f64); 2], size: 32
value: [(1.5, 2.3), (3.1, 3.9)]
---------------------------------
let demo = (1, 2.5, (1,'a'), [1,2,3]);
TypeId:
(i32, f64, (i32, char), [i32; 3]),
size: 32
value: (1, 2.5, (1, 'a'), [1, 2, 3])
---------------------------------
let demo = String::from("a demo String");
TypeId: alloc::string::String, size: 12
value: String (13): a demo String
---------------------------------
let demo = String::from("a demo String");
TypeId: alloc::string::String, size: 12
value: String (13): a demo String
let rdemo = &mut demo;
redemo.push_str(" more stuff")
TypeId: alloc::string::String, size: 12
value: String (24): a demo String more stuff
demo.push_str(" still more stuff"
TypeId: alloc::string::String, size: 12
value: String (41):
a demo String more stuff still more stuff
TypeId: alloc::string::String, size: 12
value: "s is owner"
TypeId: &alloc::string::String, size: 4
value: "s is owner"
TypeId: alloc::string::String, size: 12
value: "s is owner with stuff"
---------------------------------
struct Point {
x:f64, y:f64, z:f64, name:String,
}
let demo = Point {
x:1.5, y:2.5, z:3.7,
name:String::from("Peter")
};
TypeId: data_types::main::Point, size: 40
value: Point {
x: 1.5, y: 2.5, z: 3.7, name: "Peter"
}
---------------------------------
enum POS {
BS(String), MS(String), PhD(String),
};
let demo =
POS::MS(String::from(
"Computer Engineering")
);
TypeId: data_types::main::POS, size: 16
value: MS("Computer Engineering")
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:
-
let s = String::from("a literal string");
-
let s = "a literal string".to_string();
And we can create an str by taking a slice of a String or a literal string:
-
let s1 = "Hello world"; // slice of the whole literal
-
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 showing many of these methods in action.
String Examples:
string-probes::main.rs
#[allow(unused_imports)]
use display::{
log, slog, show, show_type, show_value,
putline, putlinen, main_title, sub_title
};
/*-----------------------------------------------------------
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
-----------------------------------------------------------*/
fn main() {
main_title("string_probes");
putline();
/*-- char --*/
let v:Vec<char> = vec!['R', 'u', 's', 't'];
log(&v);
log(&'R');
putline();
let ch:u8 = 'a' as u8;
log(&ch);
show("char is ", &(ch as char));
putline();
/*-- String --*/
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());
putline();
let s1 = s.clone();
let v:Vec<u8> = Vec::from(s1);
slog(&v[0]);
show("vec from string",&v);
putline();
/*-- str --*/
let s_slice = &s[..]; // slice containing all chars of s
slog(&s_slice);
show("s_slice = ", &s_slice);
putline();
let s_slice2 = s.as_str();
slog(&s_slice2);
putline();
/*-- create string and mutate --*/
let mut s = String::new();
s.push('a');
s.push(' ');
s.push_str("test string");
log(&s);
putline();
let t = s.replace("string","Rust String");
slog(&t);
putline();
for tok in s.split_whitespace() {
print!("\n {}", tok);
}
putline();
//-----------------------
// this works too
// let iter = s.split_whitespace();
// for tok in iter {
// 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
*/
let result = s.chars().nth(0);
match result {
Some(r) => show("s.chars().nth(0) = ", &r),
None => print!("\n couldn't extract char"),
}
show("s = ", &s);
let result = s.chars().nth(2);
match result {
Some(r) => show("s.chars().nth(2) = ", &r),
None => print!("\n couldn't extract char"),
}
show("s = ", &s);
putline();
{
/*-------------------------------------------------
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 --*/
let slice_all = &s;
slog(&slice_all);
show("slice_all = ", &slice_all);
putline();
let third = &s[2..3]; // string slice with one char
slog(&third);
show("third = ",&third);
putline();
/*-- this works for utf-8 encoding --*/
let ch = third.chars().nth(0); //
log(&ch);
match ch {
Some(x) => { log(&x); show("match ch = ", &x); },
None => print!("\n can't return ch"),
}
///////////////////////////////////////////////////
// compile fails
// - can't modify owner while borrows are active
//------------------------------------------------
// s.push('Z');
// slog(&slice_all);
} // borrow ends here
s.push('Z'); // ok, borrow no longer active
putline();
sub_title("That's all Folks!");
putlinen(2);
}
Output
C:\github\JimFawcett\RustBasicDemos\string_probes>
cargo -q run
string_probes
===============
TypeId: alloc::vec::Vec
, size: 12
value: ['R', 'u', 's', 't']
TypeId: char, size: 4
value: 'R'
TypeId: u8, size: 1
value: 97
char is 'a'
TypeId: alloc::string::String, size: 12
value: String (4): Rust
in string "Rust", char at 2 is s
length in bytes of s = {:?}4
TypeId: u8, size: 1
value: 82
vec from string[82, 117, 115, 116]
TypeId: &str, size: 8
value: "Rust"
s_slice = "Rust"
TypeId: &str, size: 8
value: "Rust"
TypeId: alloc::string::String, size: 12
value: String (13): a test string
TypeId: alloc::string::String, size: 12
value: "a test Rust String"
a
test
string
s.chars().nth(0) = 'a'
s = "a test string"
s.chars().nth(2) = 't'
s = "a test string"
TypeId: &alloc::string::String, size: 4
value: "a test string"
slice_all = "a test string"
TypeId: &str, size: 8
value: "t"
third = "t"
TypeId: core::option::Option
, size: 4
value: Some('t')
TypeId: char, size: 4
value: 't'
match ch = 't'
That's all Folks!
-------------------
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:
-
StructExprStruct:
struct Person {
name:String, occupation:String, age:u32,
}
-
StructExprTuple:
struct Person {
String, String, u32,
}
-
StructExprUnit:
struct Person;
Here are some examples:
Struct Code:
#[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");
}
}
Using Code:
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:
Three types of enumerations
/*-- discriminant enum --*/
enum Name { John, Jim=42, Jack }
/*-- tuple enum --*/
enum NameTuple {
John(String, u32), Jim(String, u32), Jack(String, u32)
}
/*-- struct enum --*/
enum NameStruct {
John { occup:String, id:u32 },
Jim { occup:String, id:u32 },
Jack { occup:String, id:u32 }
}
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 and HashMap.
StdLib Data Types
Stdlib Types code from main
/* Rust Vectors: Vec<(i32, f64, char)> - vector of tuples */
separator();
let mut demo :Vec<(i32, f64, char)> = Vec::new();
demo.push((1, 2.5, 'z'));
demo.push((2, 3.5, 'A'));
putln(&"let mut demo :Vec<(i32, f64, char)> = Vec::new();");
log(&demo);
/* Rust HashMap: HashMap<String,i32> */
separator();
use std::collections::HashMap;
let mut demo :HashMap<String, i32> = HashMap::new();
demo.insert("one".to_string(), 1);
demo.insert("two".to_string(), 2);
putln(&"let mut demo :HashMap<String, i32> = HashMap::new();");
log(&demo);
Output for Stdlib Types
---------------------------------
let mut demo :Vec<(i32, f64, char)> = Vec::new();
TypeId: alloc::vec::Vec<(i32, f64, char)>, size: 12
value: [(1, 2.5, 'z'), (2, 3.5, 'A')]
---------------------------------
let mut demo :HashMap = HashMap::new();
TypeId: std::collections::hash::map::HashMap, size: 40
value: {"one": 1, "two": 2}
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:
-
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.
-
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.
-
Create an array of Strings, populating with arbitrary values. Convert the array to a Vec.
-
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.
-
Declare a struct that has fields to describe your current employment. Display that on the console.
-
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.
|