Rust学习笔记
Rust编程语言入门教程课程笔记
Lecture 13: Functional Language Features Iterators and Closures
rust
use std::thread;
use std::time::Duration;
//Fn Traits : Fn, FnMut, FnOnce
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation: T,
value: Option<u32>,
}
impl<T> Cacher<T>
where T:
Fn(u32) -> u32,
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
}
}
}
}
#[derive(PartialEq, Debug)]
pub struct Shoe {
size: u32,
style: String,
}
pub fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter()
.filter(|s| s.size == shoe_size)
.collect()
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(
simulated_user_specified_value,
simulated_random_number
);
let x = 4;
let equal_to_x = |z| z == x; //closure can use variable from the scope in which they're defined
// fn equal_to_x(z: i32) -> bool { z == x } //function can't use variable from the scope in which they're defined
let y = 4;
assert!(equal_to_x(y));
//capturing the environment with closures
// 1. FnOnce: consumes the variables it captures from its enclosing scope, known as the closure's environment.
// 2. FnMut: can change the environment because it mutably borrows values.
// 3. Fn: borrows values from the environment immutably.
// Fn > FnMut > FnOnce
// move the ownership of the environment variables into the closure
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x; //x is moved into the closure and so the closure owns x
// println!("can't use x here: {:?}", x); //error: value borrowed here after move
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
//iterators
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); //v1_iter is immutable
for val in v1_iter {
println!("Got: {}", val);
}
//iterator trait
// pub trait Iterator {
// type Item;
// fn next(&mut self) -> Option<Self::Item>;
// }
}
fn _simulated_expensive_calculation(intensity: u32) -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
intensity
}
//compare function and closure
// 1. function: fn add_one_v1(x: u32) -> u32 { x + 1 }
// 2. closure: let add_one_v2 = |x: u32| -> u32 { x + 1 };
// 3. closure: let add_one_v3 = |x| { x + 1 };
// 4. closure: let add_one_v4 = |x| x + 1;
// The compiler will infer only one concrete type for each parameter and for the return value
// of closures, because the compiler will analyze the body of the closure to determine the types
// of each parameter and the return type. If there are multiple ways to infer the types, the
// compiler will error. This code will not compile because the compiler can't determine the
// types of the closure's parameters or the return type of the closure:
// let example_closure = |x| x;
// let s = example_closure(String::from("hello"));
// let n = example_closure(5);
fn generate_workout(intensity: u32, random_num: u32) {
// let expensive_closure = |num: u32| -> u32{ //the compiler can infer the type of the parameter and the return type
// println!("calculating slowly...");
// thread::sleep(Duration::from_secs(2));
// num
// };
let mut expensive_closure = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
// let expensive_result = simulated_expensive_calculation(intensity);
if intensity < 25 {
// println!(
// "Today, do {} pushups!",
// simulated_expensive_calculation(intensity)
// );
// println!(
// "Next, do {} situps!",
// simulated_expensive_calculation(intensity)
// );
//optimization 1
// println!(
// "Today, do {} pushups!",
// expensive_result
// );
// println!(
// "Next, do {} situps!",
// expensive_result
// );
println!(
"Today, do {} pushups!",
expensive_closure.value(intensity)
);
println!(
"Next, do {} situps!",
expensive_closure.value(intensity)
);
} else {
if random_num == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
// println!(
// "Today, run for {} minutes!",
// simulated_expensive_calculation(intensity)
// );
// println!(
// "Today, run for {} minutes!",
// expensive_result
// );
println!(
"Today, run for {} minutes!",
expensive_closure.value(intensity)
);
}
}
}
//custom iterator
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32; //associated type
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6{
Some(self.count)
} else {
None
}
}
}
//Zero Cost Abstractions: the abstraction doesn't incur any additional runtime overhead
//because the compiler generates code as if we had written the lower-level or more repetitive code by hand.
#[cfg(test)]
mod tests {
#[test]
fn call_with_different_values(){
let mut c = super::Cacher::new(|a| a);
let v1 = c.value(1);
assert_eq!(v1, 1);
let v2 = c.value(2);
assert_ne!(v2, 2);
}
#[test]
fn iterator_demonstration(){
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter(); //v1_iter is mutable
assert_eq!(v1_iter.next(), Some(&1)); //next is a consuming adaptor because it takes ownership of the iterator
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None); //next is a consuming adaptor because it takes ownership of the iterator
}
#[test]
fn iterator_sum(){
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum(); //sum is a consuming adaptor because it takes ownership of the iterator
assert_eq!(total, 6);
}
#[test]
fn iterator_map(){
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); //map is a consuming adaptor because it takes ownership of the iterator
assert_eq!(v2, vec![2, 3, 4]);
}
#[test]
fn filter_by_size(){
let shoes = vec![
super::Shoe { size: 10, style: String::from("sneaker") },
super::Shoe { size: 13, style: String::from("sandal") },
super::Shoe { size: 10, style: String::from("boot") },
];
let in_my_size = super::shoes_in_my_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
super::Shoe { size: 10, style: String::from("sneaker") },
super::Shoe { size: 10, style: String::from("boot") },
]
);
}
#[test]
fn calling_next_directly(){
let mut counter = super::Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(counter.next(), Some(5));
assert_eq!(counter.next(), None); //next is a consuming adaptor because it takes ownership of the iterator
}
#[test]
fn using_other_iterator_trait_methods(){
let sum: u32 = super::Counter::new()
.zip(super::Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum(); //sum is a consuming adaptor because it takes ownership of the iterator
assert_eq!(18, sum);
}
}
Improving Our I/O Project: mini_grep
rust
//grep: globally search a regular expression and print
use std::env;//command line arguments
use std::process;//exit
use minigrep::Config;//Config struct
use minigrep::run;//run function
//Separation of Concerns for Binary Projects
//Splitting code into a main.rs and a lib.rs is a good default choice when starting a binary project.
//1. Split your program into a main.rs and a lib.rs and move your program's logic to lib.rs.
//2. As long as your command line parsing logic is small, it can remain in main.rs.
//3. When the command line parsing logic starts getting complicated, extract it from main.rs and move it to lib.rs.
fn main() {
// let args: Vec<String> = env::args().collect();//collect command line arguments
let args = env::args();//iterator over the command line arguments
// println!("{:?}", args);//print command line arguments //[./target/debug/minigrep, xxxx, yyyy]
// let query = &args[1];//query string
// let filename = &args[2];//filename
//let (query, filename) = parse_config(&args[1..]);//parse command line arguments
// let config = parse_config(&args);//parse command line arguments
// let config = Config::new(&args);//parse command line arguments
let config = Config::build(args).unwrap_or_else(|err| {
// println!("Problem parsing arguments: {}", err);
eprintln!("Problem parsing arguments: {}", err);//error handling: print to stderr
process::exit(1);
});//parse command line arguments
// println!("Searching for {}", query);
// println!("In file {}", filename);
// let contents = fs::read_to_string(config.filename)
// .expect("Something went wrong reading the file");//read file
// println!("With text:\n{}", contents);
if let Err(e) = run(config){
// println!("Application error: {}", e);
eprintln!("Application error: {}", e);//error handling: print to stderr
process::exit(1);
}
}
rust
use std::fs;//file system
use std::error::Error;//error handling
use std::env;//environment variables
// fn parse_config(args: &[String]) -> (&str, &str) {
// let query = &args[1];//query string
// let filename = &args[2];//filename
// (query, filename)
// }
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
//.expect("Something went wrong reading the file");//read file
//println!("With text:\n{}", contents);
let results = if config.case_sensitive {//if case sensitive
search(&config.query, &contents)//search case sensitive
} else {
search_case_insensitive(&config.query, &contents)//search case insensitive
};
// for line in search(&config.query, &contents) {//iterate over each line
// println!("{}", line);//print line
// }
for line in results {//iterate over each line
println!("{}", line);//print line
}
Ok(())
}
pub struct Config {
query: String,
filename: String,
case_sensitive: bool,
}
// fn parse_config(args: &[String]) -> Config {
// let query = args[1].clone();//query string
// let filename = args[2].clone();//filename
// Config { query, filename }
// }
impl Config {
// fn new(args: &[String]) -> Config {
// if args.len() < 3 {
// panic!("not enough arguments");
// }
// let query = args[1].clone();//query string
// let filename = args[2].clone();//filename
// Config { query, filename }
// }
//pub fn build(args: &[String]) -> Result<Config, &'static str> {
pub fn build(mut args: std::env::Args) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
// let query = args[1].clone();
// let filename = args[2].clone();
args.next();//skip program name
let query = match args.next() {//query string
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let filename = match args.next() {//filename
Some(arg) => arg,
None => return Err("Didn't get a file name"),
};
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();//case sensitive
Ok(Config {
query,
filename,
case_sensitive
})
}
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {//<'a> lifetime annotation
// let mut results = Vec::new();//mutable vector
// for line in contents.lines() {//iterate over each line
// if line.contains(query) {//if line contains query
// results.push(line);//add line to results
// }
// }
// results//return results
contents.lines()//iterate over each line
.filter(|line| line.contains(query))//if line contains query
.collect()//collect into vector
}
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {//<'a> lifetime annotation
//let mut results = Vec::new();//mutable vector
let query = query.to_lowercase();//convert query to lowercase
// for line in contents.lines() {//iterate over each line
// if line.to_lowercase().contains(&query) {//if line contains query
// results.push(line);//add line to results
// }
// }
// results//return results
contents.lines()//iterate over each line
.filter(|line| line.to_lowercase().contains(&query))//if line contains query
.collect()//collect into vector
}
//TDD: Test-Driven Development
//Writing a Failing Test and Seeing It Pass
//1. Write a test that fails and run it to make sure it fails for the reason you expect.
//2. Write or modify just enough code to make the new test pass.
//3. Refactor the code you just added or changed and make sure the tests continue to pass.
//4. Repeat from step 1!
#[cfg(test)]
mod tests {
use super::*;//import outer scope
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents)
);
}
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents)
);
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}