Rust语法:所有权引用生命周期

文章目录

    • 所有权
      • 垃圾回收管理内存
      • 手动管理内存
      • Rust的所有权
      • 所有权转移
      • 函数所有权传递
    • 引用与借用
      • 可变与不可变引用
    • 生命周期
      • 悬垂引用
      • 函数生命周期声明
      • 结构体的生命周期声明
      • Rust生命周期的自行推断
      • 生命周期约束
      • 静态生命周期

所有权

垃圾回收管理内存

Python,Java这类语言在管理内存时引用了一种叫做垃圾回收的技术,这种技术会为每个变量设置一个引用计数器(reference counter),来统计每个对象的引用次数。

一旦某个对象的引用数为0,垃圾回收器就会择取一个时机将其所占用的空间回收。
以Python为例子

x = [1, 2] # 列表对象[1, 2]被x引用,引用计数为1
y = x # 列表对象[1, 2]被y引用,引用计数为2
del x # 列表对象[1, 2]的引用x被删除,此时并不删除对象,引用计数为1
del y # 列表对象[1, 2]的引用y被删除,此时并不立即删除对象,引用计数为0
# 但此对象将会在下一次垃圾回收时被清理

这种垃圾回收的方式很好的避免了开发者去管理内存,但是也造成了一些列问题。也就是:

  1. 这种垃圾回收不会立刻进行,而是会选取某个时机进行,这无疑在这段时间内这段内存会被一直占用。
  2. 垃圾回收对性能的消耗比较大,因为编译器/解释器需要不停的追踪每个对象的引用计数来决定某个变量是否会被回收。

手动管理内存

C/C++就采取了手动管理内存的方式,也就是自己申请内存,自己需要去释放,如果不释放就可能导致内存占用的堆积,这种方式的好处在于可控性和高性能,坏处在于很容易出现各种错误。例如二次释放。

void test(int arr[]){free(arr); //此时该数组已经被释放
}int main(){int *arr = (int *)malloc(sizeof(int) * 10);test(arr);free(arr); //这里又进行了第二次释放return 0;
}

上述代码可能你觉着不会写的这么蠢,但是如果当代码逻辑复杂起来,你很难保证你不会犯这样的错误。
二次释放的危害很大,因为当一块内存被释放时可能会被新的对象再次占用,而地二次释放则会破坏新的对象的存储从而造成一系列严重的错误。

Rust的所有权

Rust采取了一种非传统的做法,即采用了一种叫做所有权的机制,其规则如下:

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

来看下面的例子


fn main() {let x = 4; //x的作用域开始位置{//y的作用域开始位置let y = 10;}//y的作用域结束位置,作用域结束,y被丢弃
}//x的作用域结束位置,作用域结束,x被丢弃

此时就需要提一下堆和栈这两种内存了,其主要的区别如下:

栈是一种后进先出的内存,其内部高度组织化,并且栈内存的数据需要实现知道其大小,堆栈内存的存取速度很快

与栈内存不同,堆可以存储结果(指不能在编译时就确定大小)未知大小的数据,在存取时速度比较慢,OS会寻找一块可以存的下的内存然后分配给对应的进程。对于这块堆内存来说,会有一个指针指向这块,而这个指针是要存在栈中的。

存取速度
分配内存大小可不固定必须固定

在Rust中,有以下类型都是存储在栈内存上的,这些类型都实现了一个叫做copy的trait
在这里插入图片描述
(图片来自于b站杨旭)
由于这些类型都是存在栈上的,而栈的存储速度远高于堆,所以对于Rust来说,上述的数据在赋值传递时往往会直接进行拷贝,而不需要引用之类的技术。

fn main(){let x = 10;let y = x; //此时y有拷贝了一份10
}

所有权转移

由于我们知道上述的类型都是存在栈的,所以体现不出所有权,我们使用另一种存在堆上的类型String来演示所有权。

fn main(){let s = String::from("hello"); //从字符串字面值得到一个String类型
}

这里需要区分一下String和字符串字面值的区别,这里的String是一个存在堆内存的变量,其大小是可以扩充变化的。而字符串字面值(如let x = “hello”;)是固定的,在编译时就已经确定的了。

来看下面的代码:

fn main(){let s1 = String::from("hello"); //从字符串字面值得到一个String类型let s2 = s1;println!("{}", s); //会报错
}

上述代码报错了,我们来看报错信息:

error[E0382]: borrow of moved value: `s1`--> src\main.rs:4:17|
2 |     let s1 = String::from("hello"); //从字符串字面值得到一个String类型|         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;|              - value moved here
4 |     println!("{}", s1); //会报错|                    ^ value borrowed here after move|= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

报错的内容说,我们借用了一个已经移动了的值s。
还记着开头所说的所有权规则么:

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

String::from(“hello”);这个值最开始被s所拥有,但是当让s2 = s1时,这个值就不在归s所有了,而是发生了move(移动),所有权交给了s2。

这是因为有第二条: 一个值只能同时被一个变量所拥有的缘故,所以s1不再拥有String::from(“hello”);,自然也就无法再被使用了。

我们从内存上来看是这样的(图片来自于b站杨旭),刚开始s1是一个指针指向了hello这块堆内存
在这里插入图片描述
后来由于s2=s1,所以指针得到了复制,得到了如下的场景
在这里插入图片描述
但由于一个值只能有一个所有者,所以先前的s1指针将被视为无效,于是得到了如下的场景。
在这里插入图片描述
所以最终看起来就好像s1的所有权移动到了s2一样,发生了所有权(对hello这个值的拥有)的移动。

这么做有一个很大的好处,那就是由于一个值在同一时刻只有一个所有者,那么在离开作用域时,对应的值就不会被反复释放

fn main(){let s1 = String::from("hello"); //s1作用域开始let s2 = s1;//s2作用域开始,s1失去所有权
}//s1,s2作用域结束,由于s1没有所有权,所以只有s2会释放这块堆内存。

这样就有效的避免了二次释放的的问题,但是同时又给开发者引入了很多新的难题。

函数所有权传递

但一个变量被当做参数传递给函数时,其所有权将会被移交到函数内部的形式参数上。看如下例子:

fn get_len(s: String) -> usize{ //此时,这里的形式参数s得到了外部s1的所有权return s.len(); //返回s的长度
} //s的作用域结束,而s又具有hello的所有权,所以hello会被释放fn main(){let s1 = String::from("hello"); //从字符串字面值得到一个String类型let len = get_len(s1); //把s1交给函数get_lenprintln!("{}", s1); //由于此时所有权已经交给了函数内部的形式参数s,所以s1没有所有权,导致报错
}

上面的注释已经说名了具体状况,其发生错误的原因就在于传递参数时会发生所有权的转移,而转移后有没有发生归还,而导致使用了没有所有权的变量。

解决方法1:
使用shadowing这个特性,将使用权再返回回去。

fn get_len(s: String) -> (usize, String){ //返回长度和字符串return (s.len(), s);
}fn main(){let s = String::from("hello"); //从字符串字面值得到一个String类型let (s, len) = get_len(s); //用Tuple拆包和shadowing的特性归还使用权println!("{}'s len is {}", s, len);
}

解决方法2:
参看下面的引用部分

如果你不想这么麻烦,你想直接复制一份hello传过去,而不是吧使用权交过去,你可以使用clone方法,这个方法会把堆上的数据克隆一份,然后给传给函数。

fn get_len(s: String) -> usize{ //返回长度和字符串return s.len();
}fn main(){let s = String::from("hello"); //从字符串字面值得到一个String类型let len = get_len(s.clone()); //把s克隆一份,此时新克隆的部分的使用权将交给形式参数sprintln!("{}'s len is {}", s, len);
}

注意,克隆意味着你要在堆上复制一份数据,这是相当耗时的。但好处在于,你可以不用为所有权而烦恼。

引用与借用

每次想要传一个值都会进行所有权转移显然十分的头疼,于是就有了借用,借用实际上就是一种引用,但是它的特殊之处在于他不会拿到所有权。 借用使用&操作符来表示。例子如下:

fn get_len(s: &String) -> usize{ //s是一个借用,而不是转交所有权return s.len();
}fn main(){let s1 = String::from("hello"); //从字符串字面值得到一个String类型let len = get_len(&s1); //传入s1的引用println!("{}'s len is {}", s1, len);
}

借用在内存上看是这个样子的:
在这里插入图片描述
可以看到引用s只是指向了s1,并不是指向了堆内存所在的位置。所以借用可以看做是变量的引用。因为有这个特性,所以借用并没有拿到了s1的所有权,只是暂时的借了过来。

所以在get_len函数结束之后,由于s只是借用所以无权释放hello。所有权还在s1手中。

可变与不可变引用

上述的借用很好的解决了传参的问题,但是还没完。有时我们希望修改一下对应的值,如果你直接修改就会发现报错这是因为你没加mut关键字。

fn get_len(s: &mut String) -> usize{ //可变字符串引用return s.len();
}fn main(){let mut s = String::from("hello"); //从字符串字面值得到一个String类型let len = get_len(&mut s);//传入可变字符串引用println!("{}'s len is {}", s, len);
}

问题似乎解决了,似乎皆大欢喜,但是,更大的问题随之出现了。我们来看下面的例子

fn main(){let mut s: String = String::from("hello");let mut re1: &String = &mut s;let mut re2: &String = &mut s;//此处报错println!("{} {}", re1, re2);
}

报错的理由很简单,同一作用域内不允许出现两个同一变量的可变引用。Rust这么做是为了防止数据竞争的发生。数据竞争会由一下行为引发:

1. 两个或更多的指针同时访问同一数据
2. 至少有一个指针被用来写入数据
3. 没有同步数据访问的机制
(参考来源: https://course.rs/)

同时为了保证不可变引用不会因为可变引用的修改而发生异常,于是规定,在同一作用域内不可变引用和可变引用不能同时出现。

fn main(){let mut s: String = String::from("hello");let mut re1: &String = &s;let mut re2: &String = &mut s; //不能同时出现println!("{} {}", re1, re2);
}

但是在同一作用域内,可以同时出现多个不可变引用。

如果实在需要用到多个可变引用,则可以通过大括号来创建新的作用域。

fn main(){let mut s: String = String::from("hello");{let mut re1: &String = &s;}let mut re2: &String = &mut s;println!("{}", re2); //此时已经无法调用re1,因为它的作用域在大括号内,在此处已经失效
}

生命周期

对于Rust来说每个变量都有一个自己的生命周期,也就是说,每个变量有一个有效地范围。

如果一个变量已经失效,而仍然使用他的引用,就会造成悬垂引用。而Rust的生命周期就是为了避免悬垂引用这个错误而设计的。

悬垂引用

来看下面的代码

fn main() {let result;{let tmp = String::from("abc");result = &tmp;}println!("{}", result);
}

上述代码会报错,这是因为result的生命周期在整个main内,但是tmp只是内部的一个局部变量,在print时,tmp已经失效可以认为是空的。

但是此时result仍然持有并想使用tmp的引用,因此引发了报错。来看一下报错内容:

error[E0597]: `tmp` does not live long enough--> src\main.rs:6:18|
5 |         let tmp = String::from("abc");|             --- binding `tmp` declared here
6 |         result = &tmp;|                  ^^^^ borrowed value does not live long enough
7 |     }|     - `tmp` dropped here while still borrowed
8 |     println!("{}", result);|                    ------ borrow later used hereFor more information about this error, try `rustc --explain E0597`.

内容说的很直白,说的是result借用了一个获得还没自己长的变量。

我们来看看两个变量的生命周期(就是存活时间)

fn main() {let result;-------------------------------+{                                         |<-result的生命周期let tmp = String::from("abc");-----+  |result = &tmp;      tmp的生命周期->  |  |}--------------------------------------+  |println!("{}", result);                   |
}---------------------------------------------+

从上图可以直观的看到,result的存活范围(生命周期)大于tmp的生命周期。一个大的生命周期的变量借用了一个小的,所以才会导致了错误的发生。

我们只需要修改上述代码就可以使其正确

fn main() {let result;let tmp = String::from("abc");result = &tmp;println!("{}", result);
}

此时在调用print时,tmp的生命周期还没有结束,但是此时你也能发现result的生命周期其实还是大于tmp的,也就意味着

大周期借用小周期不一定会出错。但是在很多情况下,Rust还是认为这种情况存在风险,会在编译阶段就拒绝我们。

但是小周期借用大周期确实一定不会出错的(因为此时小周期会率先失效,而不会借用到一个失效的大周期对象)

函数生命周期声明

先来看一个函数,有一个需求,要求返回两个字符串中最长的那一个的借用。此时你会想这么写代码

fn get_greater(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y}
}

此时你觉着则个代码写得没问题,但是你却发现编译器报错了。你会发现错误如下:

error[E0106]: missing lifetime specifier--> src\main.rs:9:37|
9 | fn get_greater(x: &str, y: &str) -> &str {|                   ----     ----     ^ expected named lifetime parameter|= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

什么意思呢,也就是说Rust期望你手动添加一个生命周期的声明。

在Rust眼里,这个代码会返回一个字符串的引用&str。而这个引用可能来自x或者y,或者是其他的地方。

这取决于你如何写函数内部的实现,此时的Rust编译器犯迷糊了因为他不知道&str的生命周期大概是多长。
是和x一样长?还是和y一样长?还是什么?如果是和x一样长,且x的生命周期大于y的,那么返回一个y的引用就可能会出现悬空指针。
看下面例子:


fn main() {let result;let s1 = String::from("abc"); //s1生命周期大{let s2 = String::from("abcd");//s2生命周期小result = get_greater(s1.as_str(), s2.as_str()); //把小的引用赋给了大的}println!("{}", result); //此时s2已经销毁,result指针悬空
}fn get_greater(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y //此时返回了小的那一个}
}

上面的代码就产生了这种不安全的行为,归其原因在于我们可能会返回一个小的生命周期的借用给一个大的生命周期的变量。

而此时由于Rust并不清楚你的函数干了什么操作,所以推断不出返回值的借用的生命周期到底是大的还是小的。

所以此时Rust要求你对传入的参数的引用的生命周期加以限定,以保证返回值的生命周期是可以被Rust编译器推断的(没错,生命周期标注就是为了告诉Rust编译器你的返回值的生命周期是多大,从而让Rust能够检查出潜在的悬空指针错误)

生命周期标注符号使用’作为开头,例如

'a
'b
'abc

他么需要放在函数名后的尖括号里,表明这是一个生命周期标注变量

fn get_greater<'a>(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y }
}

然后你就需要为后续的引用标注上生命周期。其写法是在&后面加上生命周期标注变量再加类型

fn get_greater<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y }
}

我们来看上面的代码,定义了一个生命周期标注变量’a, 其中x生命周期的大小是’a, y也是。返回值也是。也就是说x,y,返回值有同样的生命周期。

你可能会疑惑这是啥意思,x和y显然可能不同。此时你可以认为‘a的大小就是x和y中最小的那一个(显然让一个小的强行拥有大的是错误的)。

所以上述代码的标注在告诉编译器两件事:

  1. 对函数内部,返回的引用必须要和x和y的生命周期相同,否则函数内部的实现有问题。
  2. 对于函数外部,返回的值是x和y生命周期中最小的那一个,所以在外部进行检查时,如果将这个小的赋给了大的,那么编译器可以直接预判报错。

看完上述的两点,你会发现加了生命周期并不改变程序的一分一毫,只是给编译器指明了一条检查你错误的道路。

我们再来看下面代码加深对这句话的理解:

fn main() {let result;let s1 = String::from("abc");{let s2 = String::from("abcd");result = get_greater(s1.as_str(), s2.as_str());}println!("{}", result);
}fn get_greater<'a>(x: &'a str, y: &str) -> &'a str {if x.len() > y.len() {x} else {y //此处会报错}
}

上述代码只给x和返回值加上了生命周期的限制,也就是说返回值和x有一样的生命周期。
但是此时Rust编译器检查你的代码,发现了你逻辑上的漏洞,因为你返回了y,而y的生命周期并不是’a。所以此时在y处报错


fn main() {let result;let s1 = String::from("abc");{let s2 = String::from("abcd");result = get_greater(s1.as_str(), s2.as_str()); //在此处报错}println!("{}", result);
}fn get_greater<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}

此时我们对函数加以修正,Rust编译器会发现,此时函数内部返回值都有‘a的生命周期。所以函数内部的逻辑是正确的。

那么此时Rust编译器就会开始对外对调用者进行检查了,由于函数指明了x和y有相同的生命周期。

但是其中一个生命周期过小(s2的过小),导致’a就是过小的这个生命周期('a就是s2)的生命周期。此时编译器检查了一遍后就发现(编译器只看函数名就知道返回值的生命周期了,不需要知道函数内部的实现细节,这就是为啥要标注生命周期,就是给编译器看的),返回值的生命周期小于它要赋给的变量的生命周期。

也就是开头的大周期变量借用小周期变量。所以此时果断报错。

综上可以看出一件事,生命周期的标注就是为了让Rust能够在编译时,对函数内检查你的逻辑上的错误,对外检查调用时是否会发生错误。所以这个标注只起到检查作用(伺候好编译器老爷

结构体的生命周期声明

在结构体和枚举中也有可能出现引用,此时我们就需要给每个引用标注一下生命周期了。
如下:

struct Test<'a>{name: &'a String;
}

此时这个标注的含义就是,name这个生命周期至少要比结构体活得还要长。我们可以举一个反例,如下:

struct Test<'a>{name: &'a String
}fn main() {let test;{let name = String::from("abc");test = Test{name: &name //此处会报错,说name生命周期太短};}println!("{}",test.name);
}

此时Rust检查后发现了,name还没test活得长,所以果断报错。
主义在实现时实使用impl实现的时候也需要标注上生命周期,因为这个相当于是结构体名的一部分。

impl<'a> Test<'a>{fn print_hello(&self) -> (){}
}

Rust生命周期的自行推断

在某些情况下Rust可以自己推断出返回值的生命周期,主要按照如下的规则:
在这里插入图片描述
我们来看例子1:

fn test(s: &String) -> &String{}

上面的代码就没有报错,这是因为Rust已经推断出了返回值的生命周期。

  1. 根据第一条规则,每个引用类型的参数都有自己的生命周期,于是s有一个生命周期
  2. 根据第二条规则,只有一个输入,于是这个输入的生命周期将会给于返回值

于是Rust推断出了返回值引用的生命周期,这是因为返回值的生命周期只可能来自于输入,因为函数内部的创建的对象,在返回引用时会造成悬垂引用(因为函数一结束,被引用的对象就失效了)。

再来看另一种情况

impl<'a> Test<'a>{fn print_hello(&self, word: &String) -> &String{word}
}

上述情况也不会报错,理由如下:

  1. 根据第一条规则,每个引用类型的参数都有自己的生命周期,于是self和word有自己的生命周期
  2. 根据第三条原则,self的生命周期会被赋给word,和返回值,此时所有的参数的生命周期都已经推断出。

生命周期约束

对于生命周期标注来说,我们可以标注任意多的类别例如下面的这个式子:

fn test<'a, 'b>(s1: &'a String, s2: &'b String) -> &String{s1
}

当然,上述的代码是错误的,因为编译器无法推断出返回值的生命周期。

我们稍作修改后就得到了这个:

fn test<'a, 'b>(s1: &'a String, s2: &'b String) -> &'a String{s1
}

此时我们就得到了这个式子,就不报错了,如果我们突发奇想换一个式子呢?我们返回s2会如何

fn test<'a, 'b>(s1: &'a String, s2: &'b String) -> &'a String{s2
}

此时编译器又报错了,因为编译器不知道s1和s2到底啥关系,返回的应该是’a的周期,但是实际返回的是’b的周期。

此时我们可以通过对生命周期的关系增加约束来达到解决问题的目的:

fn test<'a, 'b>(s1: &'a String, s2: &'b String) -> &'a Stringwhere 'b: 'a //'b生命周期大于'a
{s2
}

此时又不报错了,因为此时我们返回的是’b周期,而他比’a是要更大的。所以这个返回不会造成悬垂引用(因为返回的比’a要大,'a会造成的悬垂引用,'b不一定会造成

静态生命周期

对于一些变量它的生命周期可能是整个程序的运行期间,于是可以使用’static这个特殊的标注来声明一个整个程序期间的生命周期。

fn test(s: &'static str){}

其中,常见的字符串字面值&str采用的就是’static类型的生命周期

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/45270.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Cesium加载ArcGIS Server4490且orgin -400 400的切片服务

Cesium在使用加载Cesium.ArcGisMapServerImageryProvider加载切片服务时&#xff0c;默认只支持wgs84的4326坐标系&#xff0c;不支持CGCS2000的4490坐标系。 如果是ArcGIS发布的4490坐标系的切片服务&#xff0c;如果原点在orgin X: -180.0Y: 90.0的情况下&#xff0c;我们可…

【最新可用】VMware中ubuntu与主机window之间使用共享文件夹传输大文件

一、VMware设置共享文件夹 &#xff08;1&#xff09;虚拟机关机情况下&#xff0c;创建一个共享文件夹 &#xff08;2&#xff09;ubuntu中挂载共享文件夹 1、如果之前已经挂载 hgfs&#xff0c;先取消挂载 sudo umount /mnt/hgfs2、重新使用以下命令挂载 sudo /usr/bin/vmh…

CompletableFuture总结和实践

CompletableFuture被设计在Java中进行异步编程。异步编程意味着在主线程之外创建一个独立的线程&#xff0c;与主线程分隔开&#xff0c;并在上面运行一个非阻塞的任务&#xff0c;然后通知主线程进展&#xff0c;成功或者失败。 一、概述 1.CompletableFuture和Future的区别&…

Cesium之水流模型

关于Primitive。 Primitive和Entity&#xff0c;一般翻译成图元和实体&#xff0c;图元更接近底层&#xff0c;实体是封装后的高级对象&#xff0c;使用更加简便。一般来说&#xff0c;Primitive的使用相对繁琐&#xff0c;相比Entity需要使用者自己初始化更多对象&#xff0c…

day9 STM32 I2C总线通信

I2C总线简介 I2C总线介绍 I2C&#xff08;Inter-Integrated Circuit&#xff09;总线&#xff08;也称IIC或I2C&#xff09;是由PHILIPS公司开发的两线式串行总线&#xff0c;用于连接微控制器及其外围设备&#xff0c;是微电子通信控制领域广泛采用的一种总线标准。 它是同步通…

【Android】相对布局(RelativeLayout)最全解析

【Android】相对布局&#xff08;RelativeLayout&#xff09;最全解析 一、相对布局&#xff08;RelativeLayout&#xff09;概述二、根据父容器定位三、根据兄弟控件定位 一、相对布局&#xff08;RelativeLayout&#xff09;概述 相对布局&#xff08;RelativeLayout&#x…

【仿写框架之仿写Tomact】四、封装HttpRequest对象(属性映射http请求报文)、HttpResponse对象(属性映射http响应报文)

文章目录 1、创建HttpRequest对象2、创建HttpResponse对象 1、创建HttpRequest对象 HttpRequest对象中的属性与HTTP协议中的内容对应&#xff0c;用于后序servlet从request中获取请求中的参数。 参照http请求报文&#xff1a; import java.io.BufferedReader; import java…

(详解踩坑)GIT版本回滚git stash、git reset、git reset --hard、git revert

目录 背景 一、&#xff08;git log、git reflog&#xff09;查看git提交日志及命令历史 1.1 git log&#xff08;提交日志&#xff09; 1.2 git reflog&#xff08;命令历史&#xff09; 二、git reset&#xff08;回退到指定的版本&#xff0c;并且保留更改&#xff09; …

Vue-9.集成(.editorconfig、.eslintrc.js、.prettierrc)

介绍 同时使用 .editorconfig、.prettierrc 和 .eslintrc.js 是很常见的做法&#xff0c;因为它们可以在不同层面上帮助确保代码的格式一致性和质量。这种组合可以在开发过程中提供全面的代码维护和质量保证。然而&#xff0c;这也可能增加一些复杂性&#xff0c;需要谨慎配置…

Redis消息传递:发布订阅模式详解

目录 1.Redis发布订阅简介 2.发布/订阅使用 2.1 基于频道(Channel)的发布/订阅 2.2 基于模式(pattern)的发布/订阅 3.深入理解Redis的订阅发布机制 3.1 基于频道(Channel)的发布/订阅如何实现的&#xff1f; 3.2 基于模式(Pattern)的发布/订阅如何实现的&#xff1f; 3.3 Sp…

【深入探究人工智能】:常见机器学习算法总结

文章目录 1、前言1.1 机器学习算法的两步骤1.2 机器学习算法分类 2、逻辑回归算法2.1 逻辑函数2.2 逻辑回归可以用于多类分类2.3 逻辑回归中的系数 3、线性回归算法3.1 线性回归的假设3.2 确定线性回归模型的拟合优度3.3线性回归中的异常值处理 4、支持向量机&#xff08;SVM&a…

React+Typescript 状态管理

好 本文 我们来说说状态管理 也就是我们的 state 我们直接顺便写一个组件 参考代码如下 import * as React from "react";interface IProps {title: string,age: number }interface IState {count:number }export default class hello extends React.Component<I…

python 自动化学习(四) pyppeteer 浏览器操作自动化

背景 之前我在工作中涉及到了很多地方都是重复性的页面点点点工作&#xff0c;又因为安全保密原则不开放接口和数据库&#xff0c;只有一个页面来提供点击进行操作&#xff0c;就想着用前面学的自动化来实现&#xff0c;但发现前面学的模拟操作对浏览器来说并没有那么友好&…

安装pyrender和OSMesa

1&#xff09;安装 pyrender Pyrender是一个基于OpenGL的库&#xff0c;可以加载和渲染三维网格、点云、相机等对象3。 pip install pyrender 2&#xff09;理解PyOpenGL和OSMesa的关系是: PyOpenGL是Python的OpenGL绑定库&#xff08;接口壳子&#xff09;,它提供了在Python中…

代码部署到服务器

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…

uniapp 企业微信侧边栏开发网页授权 注入企业权限 注入应用权限 获取userid(2)

1、网页授权&#xff0c;获取code 代码&#xff1a; oauthUrl() {const that thisuni.removeStorageSync(code)let REDIRECT_URI encodeURIComponent(window.location.href)let CORPID webConfig.appIdlet url https://open.weixin.qq.com/connect/oauth2/authorize?appi…

Flink-----Yarn应用模式作业提交流程

Yarn应用模式作业提交流程 在Yarn当中又分为Session&#xff0c;PerJob&#xff0c;Application&#xff0c;建议和推荐使用独立集群的&#xff0c;其中就包含PerJob 和Application&#xff0c;但是1.17版本的Flink已将PerJob标记为过时&#xff0c;并且Application可以解决Pe…

AI绘画之三_StableDiffusion_界面操作

1 介绍 首先&#xff0c;介绍界面中的重要元素&#xff0c;如图所示&#xff1a; 基础模型&#xff1a;基础模型是最重要的设置项文生图&#xff1a;选项卡列出了各大功能&#xff0c;文生图指通过文字生成图片图生图&#xff1a;图生图指通过图片和文字生成图片修复照片&am…

sql类型-用户定义表类型

一、创建用户定义表类型String_Table_Type CREATE TYPE String_Table_Type AS TABLE ( Id nvarchar(200) NOT NULL ) GO DECLARE test String_Table_Type INSERT INTO test VALUES(a),(b),(c) SELECT * FROM test 二、SqlSugar中使用

【IMX6ULL驱动开发学习】06.DHT11温湿度传感器驱动程序编写与测试

一、DHT11简介 DHT11是一款可测量温度和湿度的传感器。比如市面上一些空气加湿器&#xff0c;会测量空气中湿度&#xff0c;再根据测量结果决定是否继续加湿。 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器&#xff0c;具有超小体积、极低功耗的特点…