Rust零碎总结

1.Rust里没有null的概念,但是实际上有很多地方是需要null的概念的,这个时候就可以用Option来代替,它是泛型T的一个包装类,就是C#里的int?或Java里的Optional;

【但反序列化貌似是可以没有null概念,没有这个属性用默认值就好了,Java的Json反序列化貌似本身就是这样做的】

2.rust里没有分号结尾的代码叫表达式expression,如a+b,它能够被自动"return",有分号的叫语句statement;

3.Rust里void类型是叫单元类型,一般用()表示,也可以忽略;

4.Rust里有引用和解引用的概念,引用变量销毁时由于它没有指向对象的所有权,所以不会销毁对象(即不会delete),而解引用是获得了所有权所以在其声明周期结束后会主动销毁指向的对象;引用用&,解引用用*;

5.Rust对于基础类型是复制语义,而对于复合类型是移动语义;

6.移动后的变量它的所有权将发生转移,如let a = b,此时之后b销毁了不会释放其之前指向的地址空间;

7.所有的变量都有隐式的生命周期,生命周期一过,那些拥有所有权的变量将会释放它们指向的地址(编译器帮我们做,不需要像C++一样手动delete)

8.rust里默认的变量(绑定一个值)是不可变的(所以它的借用或叫引用也必须是不可变的,当变量是不可变变量时,它可以在一个作用域里拥有多个不可变借用;

而如果变量是可变的(加了mut),那么在一个作用域里只能有一个可变借用;(有点像读写锁)

9.变量默认不可变,而不是不能移动所有权;

10.虽然非mut的变量,不同获取其mut的引用,但是可以将非mut变量移动所有权给mut变量,比如let a = S{};let mut b = a;

11.Rust里mod.rs和lib.rs都是很特殊的文件,最好就不要定义某个mod文件是mod.rs【对比student.rs】,mod.rs是用来描述一个模块的,比如model模块,可以创建一个model目录,然后里面添加一个mod.rs来描述model目录以及声明model里哪些子模块或者model模块里的哪些方法或struct等可以导出到外部使用;

而lib.rs则是创建lib项目时会在src根目录下,它用来描述src根目录下哪些rs文件可以被导出【其实可以理解为src是一个mod目录,这个时候lib.rs就类似mod.rs了,不过一般不在lib.rs里定义类型或函数】

12.Rust用cargo build来生成库文件或可执行文件,默认是debug的,可以通过cargo build --release来生成release文件;【cargo build会自动去下载依赖包及更新Cargo.lock文件】

13.Rust可以通过include_str!(...)宏在编译期间从配置文件里获取数据赋值给常量;

14.Rust的Mutex如果要在多线程里实现同步区域的锁(本质上是锁对象/变量),必须是let mutex_guard = mutex.lock().unwrap();,注意这个mutex_guard必须留着哪怕不用,如果用了let _ = mutex.lock().unwrap();那么将会导致mutex_guard立刻释放从而获得锁后又立刻释放锁,因而加锁失败;锁的释放就是通过每次mutex_guard的销毁实现的;

15.Rust里类型是包含三部分的,第一部分是类型基础名(想不到更好的描述词。。后面解释是啥意思),第二部分是泛型名(或叫泛型类型),第三部分是生命周期标志;

比如struct Kk<'a T> {..},这里基础类型名是Kk,泛型是T,声明周期标志是'a;这里需要注意的是泛型类型名不是必须的,只有泛型类型才会有;而基础类型名和声明周期标志是一定必须的,但是声明周期标志大多数情况可以省略系统自动帮我们搞定

对于HashMap或者LinkedList之类的key或value的类型它们必须是确定的唯一类型,std::any::Any不能作为key或value类型(Any其实和泛型差不多,但是又有很大的区别,Any确实作为函数参数的时候可以动态确定类型,但是它不具备反射功能,因此

要动态确定的类型必须先在代码里写明可能是哪些类型然后来转换为这些类型,这些类型可以没有任何相关性【比如两种entity,any是A则返回A的a属性,any是B则返回B的b属性,这点是泛型做不到的;但是any是需要在代码里写好是可能为A或B类型】)

因此LinkedList之类的元素的类型是唯一确定的,自然就包括了类型的生命周期标志(不写也是存在被编译器推断的标志),所以不能往List或Map等添加两种不同声明周期标志的类型元素,哪怕元素的基础类型名和泛型都一样但是声明周期标志不一样也不行;

举个栗子:

let mut hash_vec: HashMap<u32, &str> = HashMap::new(); 
    let str1 = "ssf".to_string();
    hash_vec.insert(3, str1.as_str());
    let str2: String = "ccc".to_string();
    hash_vec.insert(4, str2.as_str());
    println!("{:?}", hash_vec);
let mut list: Vec<&str> = Vec::new(); 
    let str1 = "ssf".to_string();
    list.push(str1.as_str());
    let str2: String = "ccc".to_string();
    list.push(str2.as_str());
    println!("{:?}", list);

上面的代码不会报错,因为我们添加的两个pair的key的生命周期都是推断为'static而value推断为假设叫'a(名字不重要重要的是两个元素的value生命周期一致)所以编译通过;

而如果是这样的代码:

let mut hash_vec: HashMap<u32, &str> = HashMap::new(); 
    hash_vec.insert(3, "aaa");
    {
        let str2: String = "ccc".to_string();
        hash_vec.insert(4, str2.as_str());
    }
    println!("{:?}", hash_vec);
let mut list: Vec<&str> = Vec::new(); 
    list.push("aaa");
    {
        let str2: String = "ccc".to_string();
        list.push(str2.as_str());
    }
    println!("{:?}", list);

则会报错,因为第二个元素的value生命周期和第一个元素的不一致,也就是说尽管第二个元素类型也是&str,但是其实不是一个类型(当然报错提示是第二个元素value活的不够长)【看后面的解释,这个结论其实有问题。。】

【好吧,上面的生命周期标志的理解可能不太符合实际测试情况(留着作参考价值)。。。,继续测试如下】

let mut hash_vec: HashMap<u32, &str> = HashMap::new(); 
    hash_vec.insert(3, "aaa");
    let str2: String = "ccc".to_string();
    hash_vec.insert(4, str2.as_str());
    println!("{:?}", hash_vec);
let mut list: Vec<&str> = Vec::new(); 
    list.push("aaa");
    let str2: String = "ccc".to_string();
    list.push(str2.as_str());
    println!("{:?}", list);

这段代码不会报错,但是显然是不符合我之前的结论的,第一个元素的value生命周期明显是static,第二个则不是,但是也没有报错,所以从这里来看对于map或list并不是说元素类型必须完全一致,而是说每个元素的生命周期要大于等于map或list,否则我们在后续调用map.get(1)如果正好就是生命周期不够的那个元素,比如上上个示例的4, str2.as_str(),那么就会出现内存安全问题(引用了销毁的元素)

继续看代码:

#[derive(Debug)]

struct Kkk<'a, 'b> {

pro1: &'a String,

pro2: &'b i32,

}

---------------------------------
{ let sse = 3; let mut k = Kkk {pro1: &"sss".to_string(), pro2: &sse}; println!("{:?}", k); { let ssk = 4; k.pro2 = &ssk; println!("{:?}", k); } //println!("{:?}", k); }

这段代码不会报错,因为后续没有再用到k了,所以尽管ssk的生命周期比k要小,但是也不会报错(这个是rust编译器优化的地方,估计以前也是报错的),而把注释的代码反注释就会报错,提示ssk存活的不够长;注意这里哪怕是输出k.pro1没有用到pro2也报错,这个是rust编译器还没那么智能(以后检测粒度更小后可能不会报错)

16.对于这样的代码:

let kkk = "sss";

这里"sss"返回的是一个胖指针,即地址和长度,地址就是指向"sss"这个&str的str对象的起始地址和字节长度(所以str对象是一块连续的内存);但是这个str对象是只存了sss三个字符的;

17.rust里不允许直接使用不确定类型,比如str,因此*"sss"会报错是因为它产生了一个str类型而不是因为不能对指向static对象的借用进行解引用;

18.自动解引用可以通过实现Deref来做到,比如我们一个指针它指向Foo类型对象(包括智能指针和裸指针)有实现test方法,如果我们为Foo实现了Deref,那么我们调用Foo指针的test方法时,由于编译器判断这个指针没有test方法,而它指向的Foo对象实现了test方法,就会自动帮我们隐式的加上解引用操作符;编译器的逻辑是:发现我们代码里的表达式里指针使用不正确,比如类型不一致,没有想关的属性,没有相关的方法,就会尝试为这些不正确的指针进行自动解引用(假设实现了,还有个DerefMut);但是有一些情况编译器不会帮我们自动解引用,比如指针(智能指针)和其指向的类型都有某个方法时,然后这里调用的这个方法我们需要的是指向对象的,但是编译器不知道因此不会自动解引用。

19.解引用后不是说原来的对象的所有权就被转移了(如果是实现了Copy则是复制),比如我只是解引用后调用对象的方法(且该方法里是&self而非self),或者是println!("{}", *foo)这样的宏调用解引用是不会发生所有权转移的【因为这个宏其实也是调用foo的Display trait的实现方法,是&self】,但是假设Foo没有实现Copy trait,然后调用了一个参数类型是Foo的函数就会发生所有权转移,如果是直接调用是可以的,比如test(foo);【前提是上面没有其他地方有获取foo的借用】,后面的代码里就不允许用foo了,但是如果是let k = &foo;test(*k);则不允许move,因为借用没有权利移交所有权,这样调用则可以test(foo),只要后面不再用foo和k即可;

【经过测试,函数里无法对借用参数解引用后然后移交所有权,因为借用没有权利移交所有权【但是如果参数是Box智能指针则可以在函数内let k: Foo = *box来移交所有权,因为box拥有对Foo对象的所有权】(会移交所有权给另一个函数的对象最好创建在堆里能减少内存占用,除非编译器能进行很细节的智能优化【因为移交所有权给参数没有优化的化是发生了数据拷贝的(不过通过Copy trait)】)】

【经过测试,无法对static对象(没有实现Copy)移交所有权,因为static对象是不能被RAII的,而能移交所有权的对象本身是可以RAII的,所以let k = *"ss"错误其实有两方面,一个是产生了str(当前版本不允许这种unsized类型),一方面就是试图移交static对象所有权,注意这个unsized不是说str没有具体大小,而是指在编译期间编译器无法确定str类型的大小(&str不是只能字面量得到),它不像i32就占4个字节,因此称之为unsized的,一个trait参数也是,由于很多类型都是可以实现该参数trait,因此该参数也是unsized】

20.基础类型是实现了Copy trait的,所以不能用来做一些解引用测试;

21.对借用的解引用是不能可能发生所有权转移的,因为借用没有这个权利,但是如果这个指针是Box(借用也是一种指针)则可以通过*box来移交所有权,box通过let k: Foo = *box;移交所有权后不能再使用box(Box的移交所有权和普通变量不一样,普通变量是let m = a,那么a就不能再使用了);

22.借用可以这么理解,借用每次用的时候(每个用到这个借用变量的地方)都类似我们现实中的别人跑过来看一下源数据是什么样然后用这个数据,(比如数据是一块黑板的内容我是owner可以随时修改,借用[别人]每次用的时候[即调用借用的地方,如println!("{}", borRef)到了这个点借用方就跑我这来看下黑板上的最新数据然后记忆后用它而不是把黑板拿走),因此我是黑板的主人,所以可以随时把这个黑板转给别人让别人成为新主人,如果我转让后,那么借用方就不能再跑我这来记忆黑板数据了(即再调用借用变量);

我就是那个引用变量名,黑板就是这个变量指向的栈空间,而别人则是借用变量名,对借用变量名的调用则是别人跑我这来看黑板上的数据并记忆到它要用的地方的过程(调用方法则可以理解为在我这申请调用方法后记忆相关数据到要用的地方)

至于智能指针则多了一个藏黑板的地图;移交黑板则是通过藏黑板地图来实现移交,即let kk = *box;移交黑板后藏黑板地图也就没用了,因此无法继续使用藏黑板地图,即box不能再用(不要杠);

而别人借用的过程对于Box有两种方式,第一种是let bor = &*box;即我直接告诉它黑板在哪,bor每个用到的地方都会跑到藏黑板的地方临时看一遍黑板上的数据或临时使用一下黑板得到他要的数据记忆后用在它被调用的地方;

第二种是let bor = &box;即它每次用(每个被调用bor的地方)都跑我这来看一下藏黑板地图然后由地图找到黑板再临时使用下黑板得到他需要的数据他记忆后用在被调用处(这里不要用人的思维说为什么每次都要看地图,第一次看完没记住吗?你就理解为这种bor记忆力很不好必须每次看一遍地图。。);同样的,我也是可以随时转出黑板的(此时地图也将没用了因此别也不能来看地图了,别杠),比如println!("{:?}", bor);,这个代码就相当于别人还要跑我这来看地图或者看我的黑板,在我转出黑板后是不允许的。

23.解引用不是只能解&这种普通的借用(所以还是就只叫借用比较好,否则人家一听解引用好像是专门解&一样,但是实际上还能解Box这种智能指针),借用和智能指针都是引用的一种

24.如果要以“字面量”方式生产HashMap可以这么写:

  let x: HashMap<_, _> = vec![("aa", 11), ("bb", 22)].into_iter().collect();
    let x1: HashMap<_, _> = [("a", 1), ("b", 2)].to_vec().into_iter().collect();
    let x2 = vec![("aa", 11), ("bb", 22)].into_iter().collect::<HashMap<_, _>>();
    let x3 = [("a", 1), ("b", 2)].to_vec().into_iter().collect::<HashMap<_, _>>();

25.Cargo.toml里的version如果不加符号,比如version="0.1.0"那么它等价于version="^0.1.0",即大于等于0.1.0版本小于0.2.0版本,如果非要完全等于0.1.0版本可以version="=0.1.0"

^1.2.3 := >=1.2.3 <2.0.0
^0.2.3 := >=0.2.3 <0.3.0
^0.0.3 := >=0.0.3 <0.0.4
^0.0   := >=0.0.0 <0.1.0
^0     := >=0.0.0 <1.0.0

26.实现了Deref和Drop(DerefMut)的就算智能指针,可以用*解引用(书上是这么说的,暂且这么记)【经过测试好像Drop都未必需要手动实现,能Deref就能用*了】

27.let mut k = 8;let m = &mut k;然后允许let u = *m,(正常情况是不允许的)因为k实现了Copy,所以这里没有移交所有权而是Copy了数据返回;而*m += 1;则是对k的值进行了修改,这个语句没有发生所有权转移,因为没有声明一个变量来将*m移动给它(假设m指向的对象没有实现Copy)

28.

let mutex = Mutex::new(0i32);
    let mutex1 = Mutex::new(0i64);
    let mutex2 = Mutex::new(0i64);
    println!("{:?}, {:?}, {:?}", mutex.type_id(), mutex1.type_id(), mutex2.type_id());

由上面得出mutex和mutex1不是同一个类型,而mutex1和mutex2是同一个类型,尽管它们都是由Mutex包裹起来的(不过看Mutex的类型声明就很明了,Mutex是一个泛型类型)。

29.Fn(), FnOnce有个很大的区别是FnOnce是只能执行一次,因此对于如下代码是正确的:

#[derive(Debug, Clone)]
struct Foo {
    pro1: i32
}

fn main() {
    let mm = Foo{pro1: 9};
    test1(move || {  // move是因为test1里F是static的缘故
        println!("{},$$$", mm.pro1);
        test2(mm);
    });
}

fn test1<F>(f: F) where F: FnOnce() + 'static {
    f();
}

fn test2(fo: Foo) {
    println!("$$%%%{}", fo.pro1);
}

但是将test1的FnOnce()换成Fn()则报错,因为f这个closure是可能执行多次的(Fn()),因此在调用test2时会将所有权move给test2的fo,如果执行多次则会多次move显然是不正确的;这里可以在调用test2处改成mm.clone()或者实现Foo的Copy;

30.static的closure也不是必须显示声明move,比如这种写法是常用的(而且是推荐的)

let mms = Foo{pro1: 10};
    std::thread::spawn(move || {
        println!("{:?}", mms);
        //let su = mms;  // flag1
    });

但是如果我们去掉move,则mms其实是外部mms的借用,因此不符合spawn里f是static的约束;

但是如果我们去掉move,然后取消flag1的注释则代码又运行成功,因为这里我们隐式的告诉了编译器这里需要move 外部mms变量,su的类型就是Foo而非&Foo;不过虽然有这个功能,最好还是显示的写出move(即上面的代码不去掉move且取消flag1不会报错或warning)

31.Rust的访问权限和Java等不一样,它的struct字段没有所谓私有字段的说法,如果不加pub则字段是mod内可访问(而Java里存在私有字段和default访问权限字段【即包】),而如果用了serde_derive的话不加pub也可以被serde访问的原因是这个宏生成了类似Java getter setter的方法;rust里pub对于struct或方法或字段是一样的,都是针对mod,super,crate,other crate这几种访问权限。

32.如果是Option可以用if let Some(ss) = option,如果是Result可以用match来分别针对Some和Err来拆箱。

33.&user.name注意这里不存在移动所有权的说法,user.name确实是获取的name的字段,但是它还没有复制给一个变量,因此没有移动所有权;而let kk = &user.name;在赋值之前是先&获取的是user.name的借用,因此这里没有发生所有权转移。

34.rust的xx.crate文件其实是xx.tar.gz文件,可以通过https://crates.io/api/v1/crates/name/version/download来下载(需要修改name和version)

35.在cargo项目里面使用cargo tree可以展示这个项目所有的依赖(如果有xx.crate文件,可以将其解压后cd到里面执行cargo tree来查看总共需要哪些依赖【记得把xx.crate这个也算上】)

36.xx.crate文件里有完整的代码(除了.git提交记录之类的,因此完全可以当成源码使用)

37.rust目前没有稳定的abi,因此无法将rust项目编译成rust特有的动态链接库【但是其实如果编译器一样的话是可以的,只是这样适用范围就太狭窄了,官方没有做】,目前只能编译成C语言格式的abi;

38.rust的自引用可以是struct Book {aa: Foo, aa_ref: &Foo},其中aa_ref的值会是aa的引用,这种就是一种自引用;而struct Book {prop: &Book},prop是其所属Book对象的引用,则此Book对象是循环递归对象,当然,它也是自引用的一种;

39.lazy_static是神器,不知道它原理是什么,用标准库似乎没法实现它的功能?凡是要在局部域需要创建static生命周期的局部对象,都需要用到这个【特别是你的初始化对象或函数无法是const的时候】

40.cargo vendor命令的应用场景,比如写的rust程序要跑流水线,而流水线的rust构建引擎所在的主机里安装了rust,但是却无法访问外网,所以自己的rust程序只用了标准库是可以跑通流水线的,但是用了第三方库就不行了(注意自己本地是可以访问cargo的外部仓库地址,所以本地跑有第三方库的程序是OK的);那这里就有一个问题了,依赖了第三方的程序本地可以跑但是流水线跑不了,怎么让两者都OK呢?

cargo vendor的作用就来了,它就是将依赖的第三方库导入到自己的程序里作为自己“这个程序里写的代码”,由于第三方库最终也是依赖的标准库,所以自然整体只需要标准库就能跑通了,从而解决了构建引擎只能跑标准库的问题【当然这种方式有个问题就是必须将cargo vendor之后下载到自己项目里的第三方库代码一起提交到码云里】【注意直接在项目下用cargo vendor --respect-source-config),好了后可以看到项目根目录下有个vendor目录,里面有下载到项目里的第三方库,然后在项目根目录下建立.cargo目录,里面再创建config文件,config文件里配置如下:

[source.crates-io]
replace-with = "vendored-sources"

[source.vendored-sources]
directory = "./vendor"

这个时候再cargo build就不会去依赖本地仓库和网络了【但是注意,在cargo vendor --respect-source-config好之前,需要先将项目里的.cargo/config的replace-with先注释掉用#【这样cargo就会用全局的replace-with】,等vendor好后再取消注释【而cargo vendor --respect-source-config之前又要先执行cargo update --offline】,否则就形成了蛋生鸡鸡生蛋的死锁,因为此时vendor目录里没有依赖,所以如果以./vendor为“仓库目录”就会找不到,就无法补充到vendor目录】,而是直接从项目根目录的vendor目录里获取依赖包,其他的cargo tree之类的命令也一样【所以本地执行命令第一步是取消replace-with = "vendored-sources",然后cargo update --offline,cargo vendor --respect-source-config【这个命令还可以用来完全的下载所有依赖的离线包到本地仓库,cargo tree cargo build等命令都没法做到】,还原replace-with = "vendored-sources",cargo build】;

41.注意rust的cargo build --release不是生产的意思,release是一种程序优化级别【当然小应用改完立刻可以发的那种也可以把--release当成发生产的标志,可以通过cfg!(debug_assertions|test|..)来区分】

但是现在的程序基本上都是要跑流水线,即LOCAL,DEV,ST,UAT,PRE,PROD,所以针对每一种环境都是需要一种配置的,而且这里每种环境的编译都是--release才对【本地调试代码时才普通的cargo build】所以这里最好的方式是在Cargo.toml里添加运行环境的features【针对可执行程序】,如env_dev,env_st,env_uat,env_pre,env_prod,可以默认运行环境features为dev;【其实cargo提供一个类似features的叫environment就好了】然后不同的流水线环境就可以用不同的编译,如cargo build --release --features env_st【然后if cfg!(env_st) && cfg!(release)】

想了想还是动态判断控制台参数好了,判断cargo run environment=dev判断是否存在environment,如果不存在且是release编译的则报错,如果是debug编译的没有environment的话就默认是dev环境,没必要非得cfg!(..)编译时获取参数,这根本加不了什么速,只能是启动速度确实快多少毫秒而已,运行速度一点没加;然后所有的配置文件如log4rs的,dotenv的都是可以自己判断是什么环境来读取什么文件如log4rs.dev.yaml,log4rs.st.yaml,程序里判断environment是st则读取log4rs.st.yaml,而不应该寄希望于log4rs能自动识别,否则每个crate都搞一套自己的识别environment的标准,那么命令行参数等岂不是要各种配置搞死。。

42.rust编译出来的如demo.exe可以./demo.exe --environment=st来运行,可以获取到命令行参数--environment=st,但是如果直接cargo run --environment=st会报错,因为cargo run里不接受--environment,要用这种方式cargo run -- --environment=st来实现,中间的--不会传给demo.exe