语法
入门中文教程链接:
https://kaisery.github.io/trpl-zh-cn/ch02-00-guessing-game-tutorial.html
字符串的字节与字符
字节是u8,一个字节;字符是unicode编码,占4个字节。
字符串的字节序列用as_bytes()返回,字符序列用chars()返回。
字符串的len()返回的是字节数,而非字符数。字符数用s.chars().count()来获得。
字符串拼接
用+可以,但推荐使用format!宏。例如:
let s = format!("position:{} not recognized", pos)
常量
常量在编译时被求值,它们的值会在使用的地方被内联。
常量初始化时不能调用非const方法,否则报错:
calls in constants are limited to constant functions, tuple structs and tuple variants
原因也是因为const是编译期求值,当然只能调用编译期的const函数了。
const函数
与C++的const方法含义完全不同,C++里const函数的意思是,不修改其成员变量。
但rust里const方法指的是编译时执行的函数 ,所以里面是不能有for循环的。
全局静态变量
当我们用非const函数初始化全局static变量时,也会报与const类似的错误:
calls in statics are limited to constant functions, tuple structs and tuple variants
可见,static与const一样,也是一个编译期的概念,区别仅在于前者会在编译期分配存储空间,而后者不会。
但这种行为就跟C++或java里的全局静态变量区别很大了,后者实际是在运行期初始化的(确切的说,是在程序启动时)。因为实际使用中,很多全局static变量的初始化难以做到编译期就确定、也很难不依赖非const函数。
所以,rust里的全局static变量根本就不是我们想要的。那么,有没有等价替代物呢?
有,用lazy_static!宏:
lazy_static! {
static ref RESERVED: Mutex<HashMap<String, TokenKind>> = Mutex::new(HashMap::from([
("and".to_string(), TokenKind::AND),
("or".to_string(), TokenKind::OR),
("div".to_string(), TokenKind::DIV),
...
]));
}
加锁是防止多线程并发访问出错。我们要这样访问:
RESERVED.lock().unwrap()
.get(m.to_lowercase().as_str())
.copied()
.unwrap_or(TokenKind::ID)
数组与Vec
跟C++一样,数组长度编译期指定,所以必须要指定常量长度。
Vec类似于C++的vector,可以自动扩展。而且有vec!宏,写起来跟数组很类似。
hashmap
字面量初始化
hashmap的字面量初始化,使用HashMap::from方法,入参是个数组:
let reserved: HashMap<&str, TokenKind> = HashMap::from([
("and", TokenKind::AND),
("or", TokenKind::OR),
("div", TokenKind::DIV),
...
]);
不用一个个insert的调用。
带默认值的键查询
get with default的写法:
reserved.get(m.to_lowercase().as_str()).copied().unwrap_or(TokenKind::ID)
get返回的是一个Option<&V>,所以,需要用copied转出Option<V>,最后再用unwrap_or获取键不在时的默认值。
正则表达式
find和capture区别
像python里,提供了几种能力:match从头开始匹配,search从任意位置开始匹配,两者都只匹配一次;findall、finditer则找到所有匹配。且,python并不区分是普通模式还是分组捕获模式(capture),后者仅仅是返回的MatchObject对象支持group(1)、group(2)…方法来获取捕获的分组值。
rust里则无此种能力,find等价于python的search,从任意位置开始匹配,且只匹配一次;find_iter找到所有匹配。但find和find_iter都只对应于普通模式,要做分组捕获,得使用captures方法。
样例如下:
#[test]
fn test_reg_find() {
let reg = Regex::new("create\\s+table\\s+(\\w+)");
let mo = reg.unwrap().find_at("create table t_a", 0).unwrap();
assert_eq!(mo.as_str(), "create table t_a");
}
fn test_reg_capture() {
let reg = Regex::new("create\\s+table\\s+(\\w+)");
let co = reg.unwrap().captures_at("create table t_a", 0).unwrap();
assert_eq!(co.get(0).unwrap().as_str(), "create table t_a");
assert_eq!(co.get(1).unwrap().as_str(), "t_a");
assert_eq!(co.len(), 2);
}
find与find_at
我一开始使用的时候,对find_at的理解错误了,我以为find_at(str, n)等价于find(str[n…]),但实际不是的,看看下面例子:
let s = " # ";
let pat = Regex::new("^#").unwrap();
assert_eq!(pat.find(&s[2..]).unwrap().as_str(), "#");
assert_eq!(pat.find_at(s, 2), None);
find_at正如它注释里讲的,它会考虑正则表达式的上下文,这里的上下文就是^,它表示要从整个字符串s的最开始处匹配,即使我们指定了偏移量=2,事实上,它是无效的。
所以,我个人对于提供find_at的动机不太理解,作为API来讲,感觉很容易被误解。
null
rust没有空指针的概念,只能用Option的枚举None。
判断一个对象是否None有两种写法:==或is_none
#[test]
fn test_none() {
let tk: Option<TokenKind> = None;
assert!(tk == None);
assert!(tk.is_none());
}
异常
rust没有异常,只有panic。像unwrap方法一般都会触发panic,panic会导致程序coredump,所以,除了在UT里,一般不建议使用unwrap,而要用match或if let的替代写法,当然,后者写起来比unwrap繁琐多了。
赋值语句
rust里的对象,用引用传递(rust术语叫borrow)没问题,用值传递会导致所有权转移,本质上就是C++里的auto_ptr。所以,看似普通的赋值语句也会像auto_ptr那样带来销毁式拷贝(destruction copy)的效果:
#[test]
fn test_assign() {
let r1 = Rectangle {
width: 1,
height: 2
};
// value moved here
let r2 = r1;
// value borrowed here after move
println!("{}", r1.width);
println!("{}", r2.width);
}
对于enum类型,也是这个结论,因为enum本质上也是一个对象。
但对于基本类型(如数字)及字符串字面量(&str)类型,赋值语句并不触发所有权转移,是没有问题的。比如下面代码可以正常执行:
#[test]
fn test_assign() {
let i = 1;
let j = i;
println!("{}", i);
println!("{}", j);
// 这里一旦写成 let s = "abc".to_String();就会编译报错了
let s = "abc";
let s1 = s;
println!("{}", s);
println!("{}", s1);
}
Option::unwrap
注意Option::unwrap的定义:
pub const fn unwrap(self) -> T {
match self {
Some(val) => val,
None => unwrap_failed(),
}
}
传入的是值self,而不是通常的引用&self。所以,unwarp调用完之后,原本的Option对象就消失了,不能再被访问了。亦即unwrap是一次性的调用方法。
闭包与函数指针
闭包就是匿名函数,可以捕获上下文。类似于其它语言的lambda。它有三个trait(Fn
、FnMut
和 FnOnce
)。
函数指针(fn,注意f小写)实现了所有三个闭包 trait(Fn
、FnMut
和 FnOnce
),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。
亦即,函数指针就是闭包。
但,闭包不一定是函数指针。闭包只有在不引用upvalue的时候,才可以被自动转型为函数指针,否则会报错:
closures can only be coerced to `fn` types if they do not capture any variables
注意
:fn是类型,Fn是trait,fn可以用作泛型参数,而Fn则必须加上dyn关键字才可用作泛型参数
闭包作为Vec元素
由前可知,Fn是trait,所以Vec类型参数里不能直接写Fn,但可以写fn。此时只要传入的闭包元素不引用upvalue,编译器会自动将fn强转Fn的。但若引用了upvalue,指定Vec类型参数为Fn,像这样:
let v: Vec<dyn Fn(&str) -> usize> = vec![|s| s.len(), |s| s.len()+x];
就会报错:
the size for values of type `dyn for<'a> Fn(&'a str) -> usize` cannot be known at compilation time
此时只能使用Box来解决trait object作为Vec类型参数无法计算size的问题,这么写即可:
let v: Vec<Box<dyn Fn(&str) -> usize> > = vec![Box::new(|s| s.len()), Box::new(|s| s.len()+x)];
for item in v {
println!("{}", item("abc"));
}
这里特别说明一点:声明时必须明确指定Box<dyn Fn(&str) -> usize>类型作为Vec元素,否则依赖rust的自动类型推断,又会报错:
let v: Vec<_> = vec![Box::new(|s:&str| s.len()), Box::new(|s:&str| s.len()+x)];
这里未强制指定Vec元素类型,rust会报错:
expected closure, found a different closure
因为rust认为
|s:&str| s.len()
|s:&str| s.len()+x)
是两种类型的闭包,尽管两者的入参和返回值是一样的。
或者这样写也是可以的,就是难看点:
let v: Vec<_> = vec![Box::new(|s:&str| s.len()) as Box<dyn Fn(&str) -> usize>, Box::new(|s:&str| s.len()+x)];
这里把第一个闭包强转为Box<dyn Fn(&str) -> usize>,给rust类型推断一个指引,让它知道Vec的元素到底是啥,后面的闭包就不用再强转了。
struct构造
struct的构造器写起来有点麻烦,正常是要带字段名。不过,如果入参同字段名相同,也是可以省略掉字段名的。
我们一般会封装静态构造方法来简化struct的构造。不过rust不支持函数重载,所以不同参数个数的静态构造器得起不同的名字,这点比较麻烦(或者用builder模式?)。