又到了每日一题时刻: 每日一题: 819. 最常见的单词
//给定一个段落 (paragraph) 和一个禁用单词列表 (banned)。返回出现次数最多,同时不在禁用列表中的单词。
//题目保证至少有一个词不在禁用列表中,而且答案唯一。
//禁用列表中的单词用小写字母表示,不含标点符号。段落中的单词不区分大小写。答案都是小写字母。
// 又是复杂的Rust与简单的Python
use std::collections::HashMap;
impl Solution {
pub fn most_common_word(paragraph: String, banned: Vec<String>) -> String {
let mut ban_hash:HashMap<String,i32> = HashMap::new();
let mut cal_hash:HashMap<String,i32> = HashMap::new();
for word in banned.iter(){
ban_hash.insert(word.to_string(),1);
}
let mut c = String::from("");
let mut max_ = 0;
let mut max_word = String::from("");
for letter in paragraph.chars(){
if (letter == ' '||letter =='!'||letter == '?' ||letter == '\''||letter == ','||letter == '.'||letter == ';' ){
let b:i32 = match cal_hash.get(&c){
Some(num) =>{*num},
_ => {0},
};
if b == 0{
match ban_hash.get(&c){
Some(_) => {
c = String::from("");
},
_ => {},
}
}
if c != String::from(""){
// 这里和两行之后加克隆的原理很简单,因为所有权.当时写到这的时候改成引用就要把Hash表以及一堆相关的类型给变掉,是在改不动了
cal_hash.insert(c.clone(),b + 1);
if b + 1 > max_{
max_word = c.clone() ;
max_ = b + 1;
}
c = String::from("");
}
}else{
let le = letter.to_ascii_lowercase();
c = c + &(le.to_string());
}
}
// 最后有可能没有分割号,要把最后的情况考虑进来
if c != String::from(""){
let b:i32 = match cal_hash.get(&c){
Some(num) =>{*num},
_ => {0},
};
if b == 0{
match ban_hash.get(&c){
Some(_) => {
// println!("{:?},{},{}",cal_hash,max_,max_word);
return max_word},
_ => {},
}
}
// 这里之所以不需要像上面一样判断是因为如果在ban的Hash表中就直接返回了,走不到这一步的
cal_hash.insert(c.clone(),b + 1);
if b + 1 > max_{
max_word = c.clone() ;
max_ = b + 1;
}
}
// println!("{:?},{},{}",cal_hash,max_,max_word);
max_word
}
}
//执行用时:0 ms, 在所有 Rust 提交中击败了100.00% 的用户
//内存消耗:2.1 MB, 在所有 Rust 提交中击败了33.33% 的用户
# 我还是写的稍微有点复杂,但是思路及其清晰, 和上面相比比较好看
class Solution:
def mostCommonWord(self, paragraph: str, banned: List[str]) -> str:
par2 = ""
biaodian = "!?',;."
for char_ in paragraph:
if char_ not in biaodian:
par2 += char_
else:
par2 += ' '
par_split = par2.split(" ")
ban_dict = {}
dict_par = {}
max_len = 0
max_word = ''
for word in banned:
ban_dict[word] = 1
for word in par_split:
if word == '':
continue
word = word.lower()
if dict_par.get(word):
dict_par[word] += 1
if dict_par[word]> max_len:
max_word = word
max_len = dict_par[word]
else:
if ban_dict.get(word):
continue
else:
dict_par[word] = 1
if dict_par[word]> max_len:
max_word = word
max_len = dict_par[word]
return max_word
# 执行用时:36 ms, 在所有 Python3 提交中击败了82.16% 的用户
# 内存消耗:15.1 MB, 在所有 Python3 提交中击败了16.29% 的用户
Rust错误处理
我们在之前就已经说过Rust具有极强的安全性,其中一部分原因就是因为在大部分时候在编译就会提示错误,并处理。
我们可以将可能发生的错误分为两类: 可恢复的错误与不可恢复的错误。很多其他语言直接用Exception这种方法把两种错误柔道一起,但是Rust本身没有异常的概念,所以Rust的错误处理势必要知道错误类型:
-
可恢复的错误: 例如文件未找到,可以再次尝试查找文件。Rust利用Result<T,E> 这样的泛型
-
不可恢复的错误: Bug, 例如访问的索引超出范围。会引发panic! 宏终止执行程序:
-
程序打印一个错误信息
-
展开(unwind) , 清理调用栈 (Stack)。清理调用栈有两种方式: 1.(默认)Rust沿着调用栈往回走清理所有数据 2.Rust直接退出程序,交给OS清理数据(这种方法生成的二进制文件会更小)
// 我们在Cargp.toml的profile.release 中将panic设置为abort [package] name = "Panic_" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.release] panic = "abort" [dependencies]
-
退出程序(Quit)
-
既然panic是一个宏,那我们就可以使用
fn main() {
println!("Hello, world!");
panic!("Something Wrong");
}
// 什么panic, 在那个文职全给我们打印出来了
Hello, world!
thread 'main' panicked at 'Something Wrong', src\main.rs:3:5
Result 枚举
更多时候我们遇到错误是Result这种我们想并且应该又能里避免的错误,这个时候我们就可以使用Result来做错误的修补。
// 标准库中的定义, 我们无需写
enum Result<T,E>{
Ok(T), // 操作成功返回Ok数据的类型。
Err(E), // 失败返回错误的类型
}
//我们依然可以使用match 进行处理
// 一个打开文件的例子。
fn main() {
println!("Hello, world!");
let p = File::open("hello.txt");
match p{
Ok(_) => {println!("读取成功");},
Err(e) =>{
println!("读取失败,具体错误如下");
println!("{}",e);
// 使用println!("{:?}",e); 获取更详细的错误信息。
},
}
}
Hello, world!
读取失败,具体错误如下
系统找不到指定的文件。 (os error 2)
当然,频繁的match会为我们写代码造成极大的干扰,看我今天的每日一题就知道有多狗了 在后面我们可以使用必报的方法简化match. 其实这种match我们在第一个例子里学到过呀,就是Random游戏中读取字符串的时候就是。也就是说我们还可以用except进行错误的抛出。
传播错误
有时候我们是在函数中可能发生错误,但是我们想要用主函数或者调用函数的代码去进行错误的分类判断是否进行下一个步骤。这样我们就要传播错误
// read_file.rs
pub mod file_control{
use std::error::Error;
use std::fs::File;
use std::io;
pub fn readfile(path:String) -> Result<File, io::Error> {
return File::open(path);
}
pub fn createfile(name:String) -> Result<File, io::Error> {
return File::create(name);
}
}
// main.rs
use std::fs::File;
mod read_file;
use read_file::file_control as file_ctrl;
fn main() {
let find_file = file_ctrl::readfile("Good_Morning.txt".to_string());
if find_file.is_err(){
let make_file = file_ctrl::createfile("Good_Morning.txt".to_string());
if make_file.is_err(){
panic!("Something wrong with writing");
}else{
println!("File writing is ok {:?}",make_file);
}
let find_file = file_ctrl::readfile("Good_Morning.txt".to_string());
if find_file.is_err() {
panic!("Something wrong with reading");
}else{
println!("File reading is ok {:?}",find_file);
}
}
}
File writing is ok Ok(File { handle: 0xcc, path: "\\\\?\\G:\\Rust\\Rust_Project\\Panic_\\Good_Morning.txt" })
File reading is ok Ok(File { handle: 0xd0, path: "\\\\?\\G:\\Rust\\Rust_Project\\Panic_\\Good_Morning.txt" })
Rust 可以用? 来代指不定枚举类型,一旦发生Err直接返回Err.?的强大之处在于它会隐式的调用from 函数,from函数可以将我们err一定程度的进行类型转换,这样方便我们定义好io::error类型后不用修改,? 会自动隐式转换成我们需要的类型。但是? 运算符只能在返回类型为Result的函数中使用。
Panic使用时机。
那既然有了Result, 是不是就不需要Panic了呢?答案是否定的。panic的使用时机与规则如下:
- 定义一个可能失败的函数时,优先考虑 Result
- 编写示例, 原形代码,测试可以使用panic (unwrap, except)
- 当代码最终可能处于损坏状态的时候(某些假设,保证,约定或不可变性被打破),最好使用Panic eg. 传入无意义的参数值, 调用外部不可控代码。
讲道理这些我都还接触的比较少,毕竟没有真正投入生产环境中,这些我也就是听个乐
泛型,Trait, 生命周期
从这一节开始,才是真正的接触到Rust的核心基础,前面相当于数据结构打底。
泛型,Trait, 生命周期; 测试,断言,单元,集成测试; 重构; 闭包,迭代器;发布配置;智能指针, Box, 多线程, 高级函数与宏等等。我只能说,剩下的越学越变态。兄弟们加油~~(吐血)~~
泛型
发逆行是一种数据类型,是具体类型或去他属性的抽象代替, 用来处理重复代码问题。简单来说,泛型像相当于我们构建模板,并用占位符占位为我们追钟使用提供便利。
eg. 一个函数 fn largest(list:&[T]) -> T{...}
我们使用T作为泛型的类型。在Rust中我们可以在不同作用于使用泛型:
-
函数中定义泛型:
// 用T代替类型指定泛型 pub(crate) fn iters_<T> (iterat__:Vec<T>){ for p in iterat__{ println!("{}",p); } } fn main() { iters_(vec![2,3,4]); iters_(vec!["a","b","c"]); } // 当然这样会报错 // 这里直接告诉你怎么限制了 error[E0277]: `T` doesn't implement `std::fmt::Display` --> src\main.rs:3:23 | 3 | println!("{}",p); | ^ `T` cannot be formatted with the default formatter // 看了错误也很好理解,这个T没有Display的trait, 那我们把他限制为有Display的trait即可,编译器直接给我们答案了 // 用: 的方式限制即可 pub(crate) fn iters_<T: std::fmt::Display> (iterat__:Vec<T>){ for p in iterat__{ print!("{} ",p); } println!(); } fn main() { iters_(vec![2,3,4]); iters_(vec!["a","b","c"]); } 2 3 4 a b c
在约束泛型的时候可以使用+ 进行多个trait的约束
-
在结构体中声明泛型
#[derive(Debug)] // 因为这里给Sturct写了 derive(Debug), 其内所有变量参数由于Struct保持一致,就无需限定类型 struct test_score<T>{ subject:String, // 可能是整数,可能是小数 score: T, } fn main(){ let Python_score = test_score{ subject: "Python".to_string(), score: 59, }; let Rust_score = test_score{ subject: "Rust".to_string(), score: 59.9, }; println!("Python score is {:?}",Python_score); println!("Rust_score is {:?}", Rust_score); } Python score is test_score { subject: "Python", score: 59 } Rust_score is test_score { subject: "Rust", score: 59.9 } // 泛型并不是只能对一个类型进行繁华,而是可泛化多种类型。 // struct test_score<T,U>{ // subject:U, // score: T, //}
我们通常不会设置太多的泛型参数,不然不确定的类型过多读代码相当麻烦,代码也要重组为更小的单元
-
Enum 使枚举的变体持有泛型数据类型,具体和Struct几乎一样也没啥好说的
-
方法定义中叶可以使用泛型(枚举或结构体的方法)这个也很好理解,函数都可以使用泛型,方法肯定也可以嘛
impl<T: std::fmt::Display> test_score<T> { fn get_score_(&self) -> &T{ // 这里必须返回self的引用类型,否则self丧失所有权,编译的时候也会报错 return &self.score; } } fn(){ let a = *(Rust_score.get_score_()); let b = *(Python_score.get_score_()); println!("My Python score is {}, And my rust score is {}",b,a); } My Python score is 59, And my rust score is 59.9
-
泛型代码和使用具体类型代码的**运行速度是一样的,因为Rust会在编译的时候进行单态化,话句话是编译的时候就推断出来类型了。**结果就是编译时间编时间边长,但是运行不受影响。
Trait
Trait是Rust不同类型之间交互的基础,可以告诉Rust编译器某种类型具有哪些可以和其他类型共享的功能。,是抽象的定义共享行为。 Trait还包括一Trait bound(约束):泛型类型参数指定为实现了特性行为的类型。比较抽象,有点像Interface但是又有所区别。
-
定义一个trait :把方法签名放在一起,来定义实现某种目的所必须的一种行为:
- trait使用关键字trait定义,并且之定义方法的签名而不定义实现。
- trait可以有多个方法,每个方法的签名占一行,以;结尾。
- 实现该trait的类型必须提供具体的方法实现。
// 定义一个Trait, 里面有两个一个是翻译成中文,一个翻译成英语的方法签名 pub trait Translation{ fn translate_to_chinese(&self) -> String; fn translate_to_english(&self) -> String; }
我们既然写了trait, 就要实现。语法是impl trait名 for 类型名, 支持重写。
enum Message{ English(String), Chinese(String), } impl Translation for Message{ fn translate_to_chinese(&self) -> String { match self{ Message::Chinese(sentence) =>{return sentence.to_string();}, Message::English(sentence) => {return "我转换成了中文".to_string();}, } } fn translate_to_english(&self) -> String { match self { Message::Chinese(sentence) => { return "I'm a translated sentence".to_string(); }, Message::English(sentence) => { return sentence.to_string(); }, } } } // 我们随便写一个泛形调用, 加上我们Translation的约束 // 约束支持用where子句的写法: fn yanshu<T>(val: &T) -> String where T:Translation + std::fmt::Display,{ return "这是一个演示".to_string(); } fn std_out_english<T:Translation> (val:&T){ println!("{}",(*val).translate_to_english()); } fn main(){ let sentences = Message::Chinese("我是中文啊".to_string()); std_out_english(&sentences); // 在传入一个没有定义约束的Trait肯定报错的例子 std_out_english(&"Hello".to_string()); // 报错是 the trait `Translation` is not implemented for `String` } I'm a translated sentence
-
我们可以在某个类型上实现某个trait的前提是这个类型
或
这个trait是在本地crate定义的.意思就是说我们可以给我们上面的String类型加上我们定义的trait, 但是我们无法为Vec这种系统定义好的类型加上Display这种系统定义好的trait.也就是不让我们改源码级别的东西 -
主要是确保你和其他人不能破坏彼此间的代码,同时让Rust编译器知道自己到底应该编译使用哪个trait,放置同名混乱。
// 我们为上面的枚举实现Display,就是可以打印 // 首先我们要知道Display是咋实现的,我们直接跳源码mod.rs // 源码真的超级友好,还给了个示例 pub trait Display { /// Formats the value using the given formatter. /// /// # Examples /// /// ``` /// use std::fmt; /// /// struct Position { /// longitude: f32, /// latitude: f32, /// } /// /// impl fmt::Display for Position { /// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { /// write!(f, "({}, {})", self.longitude, self.latitude) /// } /// } /// /// assert_eq!("(1.987, 2.983)", /// format!("{}", Position { longitude: 1.987, latitude: 2.983, })); /// ``` #[stable(feature = "rust1", since = "1.0.0")] fn fmt(&self, f: &mut Formatter<'_>) -> Result; } // 也就是说,我们使用write宏会返回一个fmt::result类型,这样就足够了
所以说看源码很重要。我们跟着源码写一个例子即可
use std::fmt; use std::fmt::{Display, Formatter}; enum Message{ English(String), Chinese(String), } impl fmt::Display for Message { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { // 这里输出的格式纯粹是闲的蛋疼 Message::English(sentence) => { write!(f, "[Enum:Message\n [type: English\n [sentence:{}]\n ]\n]",sentence)}, Message::Chinese(sentence)=> { write!(f, "[Enum:Message\n [type: Chinese\n [sentence:{}]\n ]\n]",sentence)}, } } } fn main(){ let sentences = Message::Chinese("一句中文".to_string()); println!("{}",sentences) } [Enum:Message [type: Chinese [sentence:一句中文] ] ]
生命周期
生命周期就连这个老师都分了四节来讲,看来是很蓝的。 在Rust中我们常常为了让某些变量不是去所有权使用引用传递,生命周期与所有权息息相关,指引用保持有效的作用域,可见生命周期的重要性。在大多数情况下,生命周期是隐式的,可被推断的。但是当引用的生命周期可能以不同的方式相关联的时候我们就必须手动的标注生命周期。我们还是直接看例子吧
fn main(){
let a;
{
let b = 10;
a = &b;
}
println!("a:{}",a);
}
我们来看看上面这一段代码,乍一看是好像没啥问题的,但是连编译都没通过, 这究竟是编译器的扭曲还是Rust的沦丧。啊,看着错误我们就能恍然大悟。因为:我们把b的引用赋给了a.但是b本身在调用println!(a)的时候已经被释放了,a就算引用,人家已经不在内存中了你找不到,所以提示活的不够久。
5 | a = &b;
| ^^ borrowed value does not live long enough
生命周期存在的target就是避免垂悬引用(dangling reference), y额就是上面的情况。 在编译的时候Rust要求任何一个变量的生命周期要大于等于其引用的生命周期。对于上面的例子,我们直接更改b的作用域,让b的作用域包含a即可。
但是对于我们没法修改作用于的情况,比如函数,这个时候我们就需要显示定义生命周期了
fn main(){
let a = "123";
let b = try_(a,&123);
}
fn try_(a:&str,b:&i32) -> &str{
a
}
// 会发生如下的报错。 我再刚开始函数那里用的切片引用函数出现过这种情况,当时啥也不会直接哭死。
5 | fn try_(a:&str,b:&i32) -> &str{
| ---- ---- ^ expected named lifetime parameter
help: consider introducing a named lifetime parameter
|
5 | fn try_<'a>(a:&'a str,b:&'a i32) -> &'a str{
| ++++ ++ ++ ++
为什么会产生这种情况? 因为我们在函数try_中传递了两个引用,返回了一个引用,编译器无法判断返回的引用使用谁的生命周期。在Rust中不管引用的类型是什么,只要传参出现超过一个引用且最后返回一个引用类型,就会发生生命周期的混乱。这个时候,我们必须让返回的引用类型与我们传入的引用类型生命周期关联,这样,Rust知道改返回值有什么样的生命周期。
生命周期的关联类似于泛形,<‘关联参数名>的方式进行关联:
fn main(){
let a = "123";
let b = try_(a,&123);
}
// 这里声明了一个生命周期a 后面引用中含a的说明生命周期显示指出不小于a,也就是整个函数
fn try_<'a>(a:&'a str,b:&i32) -> &'a str{
a
}
这样的的生命周期标注 :
- 不会影响引用传参的生命周期长度。我们上述的例子并没有改变传参和返回参数的生命周期,但是却规定了所有带标注的引用生命周期不会小于这个函数的作用域。换句话说我们编译时不会把返回值看作仅函数是作用域的引用。一般来说 Rust会将’a的标注的引用的生命周期显示的指定为被’a标注的最短但是大于函数的生命周期
- 当制定了泛形生命周期参数,函数可以接受任何带有生命周期的引用。
ps. 关于 &str 很有趣的一件事, 我们直接定义一个let a = “123”, 这个&str类型的a变量有点东西,什么东西呢,它直接放到binary下面,其生命周期是全局静态, 不会出现所有权失效,不会出现垂悬引用,所以蒸的是有点东西。其他的&i32啥的都没这特权—(不行就是不行).
当然,函数之后,我们就要考虑Struct于Enum了。。讲真这三个哥们以后每讲一个和新特性都有他们那真的,有点绷不住
-
Struct 引用类型的生命周期:
Struct必须给自身所有引用类型属性添加生命周期标注,生命周期会与结构体实例相同
struct Hello<'a>{ Some_:&'a str, } fn main(){ let mut c:&String; { let hello = Hello{ Some_:"I'm fine", }; c = &(hello.Some_.to_string()); println!("{}",hello.Some_); } // 垂悬引用,这里不直接使用&str也是上面ps的原因 // println!("{}",c); }
生命周期出现在函数或方法的参数: 输入生命周期。同理,出现在return 的叫输出生命周期
生命周期省略:如果编译器可以推断出生命周期就可以进行生命周期的省略。
- 如果每个引用类型参数都有自己的生命周期
- 如果只有一个恩铭周期参数,该生命周期被付给所有输出生命周期
- 如果输入存在&self, self生命周期传递给所有输出生命周期参数。