Create an array of Strings on the heap, where the size of the array is defined at
run-time. Display the strings, then swap the first and last strings and display the
results. Did you have to copy any characters to swap the two strings?
This is a loaded question: arrays are static, with memory layout defined at compile time.
If we broaden our definition of array to "indexable collection of items residing in a
contiguous memory block" then we can indeed define the size at run-time using a
Vec<T>.
Now, we can create an array, of size determined at run-time, by creating a full slice of
the Vec<T> So we didn't need the broader definition, but did have to take
a circuitous path to provide an answer to the question, as asked.
If you try to directly swap elements you will run afoul of Rust's ownership rules - you can't
have more than one mutable reference. Instead, look at the Vec::swap function. If you look at its
source code - it's simple (and available - just google for rust vec swap and press the
[src] button). The same is true for swap for slices. That will allow you to fully answer the question.
Solution for Problem #2
Solution #2
/////////////////////////////////////////////////////////////
// This is an exploration of creating and managing arrays.
// Almost always you should prefer to use std::Vec.
fn exercise_3() {
print!("\n -- solution #2 --");
/*-- create static array in stack and swap --*/
let s1 = "one".to_string();
let s2 = "two".to_string();
let s3 = "three".to_string();
let s4 = "four".to_string();
let s5 = "five".to_string();
let mut stack_array =
[
s1.clone(),
s2.clone(),
s3.clone(),
s4.clone(),
s5.clone()
];
print!("\n stack_array = {:?}", stack_array);
stack_array.swap(0,4);
print!("\n stack_array = {:?}", stack_array);
/*-- create static array in heap and swap --*/
let mut heap_array = Box::new([s1, s2, s3, s4, s5]);
print!("\n heap_array = {:?}", heap_array);
heap_array.swap(0,4);
print!("\n heap_array = {:?}", heap_array);
/*-- create dynamic vector in heap and swap --*/
let n = 5;
let mut v = Vec::<String>::new();
for i in 0..n {
v.push((i + 1).to_string());
}
let mut heap_vec = Box::new(v);
print!("\n heap_vec = {:?}", heap_vec);
heap_vec.swap(0,4);
print!("\n heap_vec = {:?}\n", heap_vec);
/*-- create array with run-time size and store in heap --*/
let n = 7; // run-time size, could have been passed in by user
let mut vn = Vec::<String>::new();
for i in 0..n {
vn.push((2*i).to_string());
}
/*-- here's array, created as slice of vec --*/
let arr_n = &mut vn[..];
show_type_value("arr_n", &arr_n);
let heap_arr_n = Box::new(arr_n);
show_type_value("heap_arr_n", &heap_arr_n);
heap_arr_n.swap(0,6);
show_type_value("heap_arr_n", &heap_arr_n);
/*-- bring array back to stack, consuming src --*/
let arr_back = *heap_arr_n;
show_type_value("arr_back", &arr_back);
/*-- curious things you can do with arrays --*/
/////////////////////////////////////////////
// Can copy string into array of Strings
arr_back[2] = "third element".to_string();
/////////////////////////////////////////////
// This sequence of statements copies chars
// to effect swap. Uses clone because
// Strings can't be moved out of array.
let first: String = arr_back[0].clone();
let last: String = arr_back[6].clone();
arr_back[0] = last;
arr_back[6] = first;
/////////////////////////////////////////////
// This statment just swaps pointers
//-------------------------------------------
// arr_back.swap(0,6);
show_type_value("swapped arr_back", &arr_back);
println!();
}
Output:
-- solution #2 --
stack_array = ["one", "two", "three", "four", "five"]
stack_array = ["five", "two", "three", "four", "one"]
heap_array = ["one", "two", "three", "four", "five"]
heap_array = ["five", "two", "three", "four", "one"]
heap_vec = ["1", "2", "3", "4", "5"]
heap_vec = ["5", "2", "3", "4", "1"]
arr_n = ["0", "2", "4", "6", "8", "10", "12"]
&&mut [alloc::string::String]
heap_arr_n = ["0", "2", "4", "6", "8", "10", "12"]
&alloc::boxed::Box<&mut [alloc::string::String]>
heap_arr_n = ["12", "2", "4", "6", "8", "10", "0"]
&alloc::boxed::Box<&mut [alloc::string::String]>
arr_back = ["12", "2", "4", "6", "8", "10", "0"]
&&mut [alloc::string::String]
swapped arr_back = ["0", "2", "third element", "6", "8", "10", "12"]
&&mut [alloc::string::String]
The solution, above, provides a lot of details about using heap memory,
working with vectors and with arrays. You will see handled all of the issues cited
in the Exercise 3 statement with comments to help illustrate
what the code is trying to do.
Create a string that is simultaneously shared by two different identifiers. What do you have
to do to modify the string?
Shared mutation is not allowed in safe Rust as that can lead to undefined behavior
through unsafe memory access.
But, suppose that the shared mutations are staggered in
time so that they don't intereact? Rust's borrow checker can often analyze
this case and allow it. But sometimes the checker is just not strong enough to
detect this structure and a build fails for safe code.
This is exactly the primary use case for RefCell. It defers some borrow checking
to run-time. Then if the accesses are all truely staggered, the processing succeeds.
The answer I want to show you uses RefCell to defer borrow checking to run-time.
Solution for Problem #3
run_time_checking:main.rs
/////////////////////////////////////////////////////////////
// run_time_checking::main.rs - demo RefCell //
// //
// Jim Fawcett, https://JimFawcett.github.io, 08 Jun 2020 //
/////////////////////////////////////////////////////////////
/*
Demonstrate deferring ownership rule checking
to run-time by using RefCell.
*/
#![allow(dead_code)]
use std::cell::RefCell;
use std::io::*;
fn putline() { println!(); }
/*-----------------------------------------------
This first function compiles.
Borrow checker flow analysis accepts it.
It has either immutable or mutable borrow,
but not both. Borrow checker analyzed this!
*/
fn test_checker1(p: bool) {
let mut s = String::from("this is a test");
let mut rs: &String = &"rs".to_string();
let mut mrs: &mut String = &mut "mrs".to_string();
if p {
rs = &s;
print!("\n rs = {:?}", rs);
} // immutable borrow goes out of scope
else {
mrs = &mut s;
print!("\n mrs = {:?}", mrs);
} // mutable borrow goes out of scope
print!("\n mrs = {:?}", mrs);
print!("\n rs = {:?}", rs);
putline();
}
/*-----------------------------------------------
Borrow checker flow analysis fails here even
if called with p1 != p2. Borrow checker
can't tell without analyzing caller -
too complicated for compiler analysis.
Note: this function is same as test_checker1
except now has two predicates.
*/
// fn test_checker2(p1: bool, p2: bool) {
// let mut s = String::from("this is a test");
// let mut rs: &String = &"rs".to_string();
// let mut mrs: &mut String = &mut "mrs".to_string();
// if p1 {
// rs = &s;
// }
// if p2 {
// mrs = &mut s;
// }
// print!("\n mrs = {:?}", mrs);
// print!("\n rs = {:?}", rs);
// }
/*-----------------------------------------------
This function replicates functionality of
test_checker2. It uses RefCell to defer
checking to run-time (hiding mutability),
So borrow checker accepts it.
I've added a new case using predicate p3
that doesn't satisfy ownership rules even
at run-time, and so panics.
*/
fn test_checker3(p1: bool, p2: bool, p3:bool) {
print!(
"\n -- test_checcker3({}, {}, {}) --",
p1, p2, p3
);
let s = String::from("this is a test"); // replacement
let sp1 = RefCell::new(s);
print!("\n &sp1 = {:?}", &sp1.borrow());
let sp2 = RefCell::new("rsp2".to_string()); // initial val
let mut rsp2 = &sp2;
let sp3 = RefCell::new("rsp3".to_string()); // initial val
let mut rsp3: &RefCell<String> = &sp3;
if p1 {
print!("\n -- immutable borrow --");
rsp2 = &sp1;
print!("\n &rsp2.borrow() = {:?}", &rsp2.borrow());
} // borrow goes out of scope
if p2 {
print!("\n -- mutable borrow --");
let mut x = sp1.borrow_mut();
x.push_str(" and more");
rsp3 = &sp1;
print!("\n &rsp3 = {:?}", &rsp2);
} // borrow goes out of scope
if p3 { // panic if p3 true
print!("\n -- immutable and mutable borrow --");
let _ = std::io::stdout().flush();
rsp2 = &sp1; // immutable borrow
let mut x = sp1.borrow_mut(); // mutable borrow
x.push_str(" and more"); // mutates sp1 inner
/*-- looks like immutable borrow so compiles --*/
rsp3 = &sp1;
print!("\n &rsp2.borrow() = {:?}", &rsp2.borrow());
print!("\n &rsp3.borrow() = {:?}", &rsp3.borrow());
} // never get here - ownership rules violated
print!("\n -- final results --");
print!("\n &rsp2.borrow() = {:?}", &rsp2.borrow());
print!("\n &rsp3.borrow() = {:?}", &rsp3.borrow());
putline();
}
fn main() {
// test_checker1(true); // succeeds
// test_checker1(false); // succeeds
test_checker3(true, true, false); // succeeds
test_checker3(true, true, true); // panics
println!("\n\n That's all Folks!\n\n");
}
Output:
-- test_checcker3(true, true, false) --
&sp1 = "this is a test"
-- immutable borrow --
&rsp2.borrow() = "this is a test"
-- mutable borrow --
&rsp3 = RefCell { value: }
-- final results --
&rsp2.borrow() = "this is a test and more"
&rsp3.borrow() = "this is a test and more"
-- test_checcker3(true, true, true) --
&sp1 = "this is a test"
-- immutable borrow --
&rsp2.borrow() = "this is a test"
-- mutable borrow --
&rsp3 = RefCell { value: }
-- immutable and mutable borrow --
thread 'main' panicked at 'already mutably borrowed:
BorrowError', ...
Deferring checking to run-time doesn't let you violate the ownership rules.
You use this technique when the compiler's static analysis can't tell if the
rules will be broken. In this case, unless you do something like this, the build will
fail.
Using RefCell hides the mutability of a borrow, which
allows the code to compile, and then run-time processing will succeed or panic depending
on whether ownership rules are actually satisfied or not.
This turns out to be a very important technique for sharing between threads, as we will
see in the Threads Bite.