Rust 基础入门 —— 2.3.所有权和借用
Rust 的最主要光芒: 内存安全 。
实现方式: 所有权系统。
写在前面的序言
因为我们这里实际讲述的内容是关于 内存安全的,所以我们最好先复习一下内存的知识。
然后我们,需要理解的就只有所有权概念,以及为了开发便利,进一步引出的引用借用概念。
永远的基础,内存管理
内存作为存储程序运行时数据的地方,是任何地方都避不开的。除非发展到量子计算,脱离了传统的二进制计算架构。
在这里先说明一下内存讨论的主体颗粒度。我们将着眼于 栈(stack) 和 堆 (heap)。重点明晰的是保存的位置是这两者中的哪一种。
栈
栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,再从顶部拿走。不能从中间也不能从底部增加或拿走盘子!
增加数据叫做进栈,移出数据则叫做出栈。
因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。
堆
与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。
接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。
性能区别
写入方面:入栈比在堆上分配内存要快,因为入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备。
读取方面:得益于 CPU 高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在 10 倍以上!栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中。访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。
因此,处理器处理分配在栈上数据会比在堆上的数据更加高效。
所有权和堆栈
所谓的所有权,他存在的意义就是通过某种逻辑实现对堆上数据的管理。
接下来,让我们详细的介绍所有权对内存管理的具体逻辑。
所有权原则
让我们首先明确规则,再去详细了解内涵
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
那么什么是 变量的作用域呢?
变量的作用域
同 c++ 类似,作用域是变量合法有效的范围,
从变量的创建开始有效,止于它离开作用域为止。代码说明如下:
{ // s 在这里无效,它尚未声明let s = "zry"; // s 在这里无效,它尚未声明// code ....
} // s 在这里无效,它尚未声明
堆上数据和 栈上数据
在这里我们会通过 代码来介绍在 rust 中 什么数据会在堆上,什么数据会在栈上。
我们使用 String
类型进行介绍。
先继续使用上面的代码 let s = "zry"
这里的s
是被编译器 硬编码进程序里的字符串值,类型是 &str
。但是它有一些缺陷。如下:
- 字符串字面值是不可变的,因为被硬编码到程序代码中。
- 并非所有字符串的值都能在编写代码时得知。
- 例如:获取用户输入的数据时,编译器肯定是不能在编译时预先知道这部分内容应当怎么写的。
所以,Rust 提供了 动态字符串类型:String
。这个类型被分配到堆上,当出现需要再扩充空间或者缩小时,都可以很方便实现。
具体的创建方式如下:
let s = String::from("zry");
// `::` 是一种调用操作符,这里表示调用 `String` 中的 `from` 方法,因为 `String` 存储在堆上是动态的,你可以这样修改它:s.push_str("say hello, world!"); // push_str() 在字符串后追加字面值 println!("{}", s); // 将打印 `zry say hello, world!`
到此,我们知道怎样在堆上创建数据(String)和栈上创建数据 (&str)
接下来,我们将根据这两点 展开讲述 所有权的交互。
所有权的交互
所有权的交互分为三种:
- 转移所有权,A的给了B,B能用,A不能用
- 克隆所有权(深拷贝):B抄了A的,多了一份数据,A和B各自使用各自的。
- 拷贝所有权(浅拷贝):B知道A有,共用一份数据,A和B使用同一份内容。
所有权是对堆上数据的管理权限。先看栈上数据的代码:
let x = 5;
let y = x;
代码背后的逻辑很简单, 将 5
绑定到变量 x
;接着拷贝 x
的值赋给 y
,最终 x
和 y
都等于 5
,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存
我们在前面就讨论过,拷贝是一个应当重视的行为,那么:这里使用拷贝有什么问题吗?
实际上,我们重视拷贝的原因是因为拷贝需要增加空间,保护数据,消耗时间。在规模变得足够大的时候,就会造成性能浪费。
而这种栈上操作的数据足够的简单,拷贝这个行为也只是需要复制一个整数大小(i32
,4个字节)的内存即可,因此在这种情况下,拷贝的速度远快于堆上创建内存,而内容也足够小,不会造成性能浪费。
实际上 Rust 的基本类型都是通过自动拷贝的方式来赋值的
接下来详细介绍:
转移所有权
从基础类型赋值之后,我们使用String
类型完成这一节的演示。
首先代码如下:
let s1 = String::from("zry");
let s2 = s1;
这里 String
并不是基础类型,rust只对基础类型进行自动拷贝
String
类型数据是存放在堆上的,它本身是一个复合类型,你可以把它简单抽象是一个结构体来方便理解。它提供了三部分:存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中的堆指针是根本,字符串长度、字符串容量是为了性能考量的辅助优化。
- 堆指针指向了真实存储字符串内容的堆内存,
- 字符串容量是堆内存分配空间的大小
- 字符串长度是目前已经使用的大小。
回到代码中来,继续讨论 let s2 = s1;
的这一步:
分成两种情况讨论:
-
拷贝
String
和存储在堆上的字节数组 如果该语句是拷贝所有数据(深拷贝),那么无论是String
本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响 -
只拷贝
String
本身 这样的拷贝非常快,因为在 64 位机器上就拷贝了8字节的指针
、8字节的长度
、8字节的容量
,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是:一个值只允许有一个所有者,而现在这个值(堆上的真实字符串数据)有了两个所有者:s1
和s2
。
好吧,就假定一个值可以拥有两个所有者,会发生什么呢?
当变量离开作用域后,Rust 会自动调用 drop
函数并清理变量的堆内存。不过由于两个 String
变量指向了同一位置。这就有了一个问题:当 s1
和 s2
离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
因此,Rust 这样解决问题:当 s1
赋予 s2
后,Rust 认为 s1
不再有效,因此也无需在 s1
离开作用域后 drop
任何东西,这就是把所有权从 s1
转移给了 s2
,s1
在被赋予 s2
后就马上失效了。
再来看看,在所有权转移后再来使用旧的所有者,会发生什么:
let s1 = String::from("zry");
let s2 = s1;println!("{} sqy hello world!", s1);
由于 Rust 禁止你使用无效的引用,你会看到以下的错误:
error[E0382]: use of moved value: `s1`--> src/main.rs:5:28|
3 | let s2 = s1;| -- value moved here
4 |
5 | println!("{}, world!", s1);| ^^ value used here after move|= note: move occurs because `s1` has type `std::string::String`, which doesnot implement the `Copy` trait
现在再回头看看之前的规则,相信大家已经有了更深刻的理解:
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
如果你在其他语言中听说过术语 浅拷贝(shallow copy) 和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 s1
无效了,因此这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1
被移动到了 s2
中。那么具体发生了什么,用一张图简单说明:
![[Pasted image 20230815214121.png]]
这样就解决了我们之前的问题,s1
不再指向任何数据,只有 s2
是有效的,当 s2
离开作用域,它就会释放内存。 到此为止,我们也清楚了为什么 Rust 称呼 let a = b
为变量绑定。
绑定的是变量中记录的指针和数据实际地址这两者。
扩展:引用的所有权
再来看一段代码:
fn main() {let x: &str = "hello, world";let y = x;println!("{},{}",x,y);
}
这段代码,大家觉得会否报错?如果参考之前的 String
所有权转移的例子,那这段代码也应该报错才是,但是实际上呢?
这段代码和之前的 String
有一个本质上的区别:在 String
的例子中 s1
持有了通过String::from("hello")
创建的值的所有权,而这个例子中,x
只是引用了存储在二进制中的字符串 "hello, world"
,并没有持有所有权。
因此 let y = x
中,仅仅是对该引用进行了拷贝,此时 y
和 x
都引用了同一个字符串。如果还不理解也没关系,当学习了下一章节 “引用与借用” 后,大家自然而言就会理解。
克隆所有权
Rust 中是支持深拷贝的,被称之为 克隆所有权。
- 但是要注意:Rust 永远不会自动创建数据的深拷贝。
- 所以,如果你发现rust 在进行自动的复制,那么其是浅拷贝(栈上数据的赋值
let x =1; let i=x;
),或者是转移所有权(let s::String = “String”; let y = s;)。
克隆所有权的具体方式是使用 clone()
这个关键字。示例如下:
let s1 = String::from("zry");
let s2 = s1.clone();println!("{} > {} ",s1,s2);
当让,我们可以丰富扩展一下,rust 有一个叫做 Ok
ERR
的东西,可以这样方便测试:
// 定义一个函数 test_string,返回一个 Result 类型,
// 其中 Ok 部分包含一个字符串,Err 部分包含一个 &'static str 类型的错误信息
fn test_string() -> Result<String, &'static str> { let s = String::from("hello world"); //创建一个包含 "hello world" 的 String 类型变量 slet y = s.clone(); //深拷贝 s 的字符串堆。println!("{} > {}", s,y);Ok(y) //将变量s y 作为 Ok 部分的返回值
}fn main() { //主函数println!("Hello, world!"); //打印 "Hello, world!"match test_string() { //使用 match 匹配 test_string 函数的返回值Ok(z) => println!("{}", z), //如果返回的是 Ok,则将字符串 z 打印出来Err(e) => eprintln!("Error: {}", e), //如果返回的是 Err,则将错误信息 e 打印出来}
}
执行结果:
PS ...\ZryCode\CODE\Rust\file23_08_21> cargo run Compiling file23_08_21 v0.1.0 (...\ZryCode\CODE\Rust\file23_08_21)Finished dev [unoptimized + debuginfo] target(s) in 0.60sRunning `target\debug\file23_08_21.exe`
Hello, world!
hello world > hello world
hello world
- 注意事项:深拷贝性能消耗是要大于其他方式的。因此,对于热点路径(执行比较频繁的代码),使用
clone
会极大的降低程序性能。
拷贝所有权
在上面我们说到了拷贝所有权,这里详细介绍。
- 拷贝所有权,是指浅拷贝。
- 浅拷贝只发生在栈上。因此性能很高
通过代码介绍:
let x = 5;
let y = x;
println!("x = {} ,y = {}",x,y);
上面的代码中,我们并没有进行 clone
,如果按照之前的说法:所有权改变了以后,原有变量不再使用,这里x
,应当不能使用了,但是实际运行之后,依旧打印出了x
的内容。
这里引出 rust 中的 一个特征:Copy
整型这样的基本类型是在编译时已知大小的,会被存储在栈上。Rust 通过copy
这样的特征来保证拥有这样特征的类型,可以实现:一个旧的变量在被赋值给其他变量后任然可以使用。
哪些类型可 Copy
?
- 任何基本类型的组合都可以
Copy
, - 不需要分配内存或某种形式资源的类型是可以
Copy
的。
如下是一些 Copy
的类型:
- 所有整数类型,比如
u32
- 布尔类型,
bool
,它的值是true
和false
- 所有浮点数类型,比如
f64
- 字符类型,
char
- 元组,当且仅当其包含的类型也都是
Copy
的时候。比如,(i32, i32)
是Copy
的,但(i32, String)
就不是 - 不可变引用
&T
,例如 扩展:引用的所有权 中的例子,但是注意: 可变引用&mut T
是不可以 Copy的
函数传值与返回
既然我们前面说到了:赋值是会改变所有权的。那么自然会聊到一个点:函数传值。
在将值传递给函数时,一样会发生 移动 或者 复制 ,就像赋值一样。具体可以看下面的代码:
// 定义一个函数 test_string,返回一个 Result 类型,
// 其中 Ok 部分包含一个字符串,Err 部分包含一个 &'static str 类型的错误信息
fn test_string() -> Result<String, &'static str> {let s = String::from("hello world"); //创建一个包含 "hello world" 的 String 类型变量 slet y = s.clone(); //深拷贝 s 的字符串堆。println!("{} > {}", s, y);Ok(y) //将变量s y 作为 Ok 部分的返回值
}fn test_string_2() {let x = 5;let y = x;println!("x = {} ,y = {}", x, y);
}fn takes_ownership(some_string: String) {// some_string 进入作用域println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放fn makes_copy(some_integer: i32) {// some_integer 进入作用域println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作fn main() {//主函数println!("Hello, world!"); //打印 "Hello, world!"match test_string() {//使用 match 匹配 test_string 函数的返回值Ok(z) => println!("{}", z), //如果返回的是 Ok,则将字符串 z 打印出来Err(e) => eprintln!("Error: {}", e), //如果返回的是 Err,则将错误信息 e 打印出来}test_string_2();let s = String::from("hello"); // s 进入作用域//-------------------------------------takes_ownership(s); // s 的值移动到函数里 ...// ... 所以到这里不再有效let x = 5; // x 进入作用域makes_copy(x); // x 应该移动函数里,// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,// 所以不会有特殊操作
运行后如下结果:
PS ...\ZryCode\CODE\Rust\file23_08_21> cargo runCompiling file23_08_21 v0.1.0 (...\ZryCode\CODE\Rust\file23_08_21)Finished dev [unoptimized + debuginfo] target(s) in 1.41sRunning `target\debug\file23_08_21.exe`
Hello, world!
hello world > hello world
hello world
x = 5 ,y = 5
hello
5
同样的,函数返回值也有所有权,例如:
// 定义一个函数 test_string,返回一个 Result 类型,
// 其中 Ok 部分包含一个字符串,Err 部分包含一个 &'static str 类型的错误信息
fn test_string() -> Result<String, &'static str> {let s = String::from("hello world"); //创建一个包含 "hello world" 的 String 类型变量 slet y = s.clone(); //深拷贝 s 的字符串堆。println!("{} > {}", s, y);Ok(y) //将变量s y 作为 Ok 部分的返回值
}fn test_string_2() {let x = 5;let y = x;println!("x = {} ,y = {}", x, y);
}fn takes_ownership(some_string: String) {// some_string 进入作用域println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放fn makes_copy(some_integer: i32) {// some_integer 进入作用域println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作fn main() {//主函数println!("Hello, world!"); //打印 "Hello, world!"match test_string() {//使用 match 匹配 test_string 函数的返回值Ok(z) => println!("{}", z), //如果返回的是 Ok,则将字符串 z 打印出来Err(e) => eprintln!("Error: {}", e), //如果返回的是 Err,则将错误信息 e 打印出来}test_string_2();let s = String::from("hello"); // s 进入作用域//-------------------------------------takes_ownership(s); // s 的值移动到函数里 ...// ... 所以到这里不再有效println!("S == {}",s);let x = 5; // x 进入作用域makes_copy(x); // x 应该移动函数里,// 但 i32 是 Copy 的,所以在后面可继续使用 xlet s1 = gives_ownership_2(); // gives_ownership_2 将返回值// 移给 s1let s2 = String::from("hello"); // s2 进入作用域let s3 = takes_and_gives_back_2(s2); // s2 被移动到// takes_and_gives_back_2 中,// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,// 所以什么也不会发生。s1 移出作用域并被丢弃fn gives_ownership_2() -> String {// gives_ownership_2 将返回值移动给// 调用它的函数let some_string = String::from("hello"); // some_string 进入作用域.some_string // 返回 some_string 并移出给调用的函数
}// takes_and_gives_back_2 将传入字符串并返回该值
fn takes_and_gives_back_2(a_string: String) -> String {// a_string 进入作用域a_string // 返回 a_string 并移出给调用的函数
}
其中如果在变量传入函数后,再次使用变量,就会报错如下:
error[E0382]: borrow of moved value: `s`--> src\main.rs:51:24|
46 | let s = String::from("hello"); // s 进入作用域| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
...
49 | takes_ownership(s); // s 的值移动到函数里 ...| - value moved here
50 | // ... 所以到这里不再有效
51 | println!("S == {}",s);| ^ value borrowed here after move|
很明显,这样的方式不利于我们开发时使用。
因此 ,我们继续介绍 引用和借用。 所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。
相关文章:
Rust 基础入门 —— 2.3.所有权和借用
Rust 的最主要光芒: 内存安全 。 实现方式: 所有权系统。 写在前面的序言 因为我们这里实际讲述的内容是关于 内存安全的,所以我们最好先复习一下内存的知识。 然后我们,需要理解的就只有所有权概念,以及为了开发便…...
Node.js-Express框架基本使用
Express介绍 Express是基于 node.js 的web应用开发框架,是一个封装好的工具包,便于开发web应用(HTTP服务) Express基本使用 // 1.安装 npm i express // 2.导入 express 模块 const express require("express"); // 3…...

阿里云通用算力型u1云服务器CPU性能详细说明
阿里云服务器u1是通用算力型云服务器,CPU采用2.5 GHz主频的Intel(R) Xeon(R) Platinum处理器,通用算力型u1云服务器不适用于游戏和高频交易等需要极致性能的应用场景及对业务性能一致性有强诉求的应用场景(比如业务HA场景主备机需要性能一致)ÿ…...

设计模式之创建者模式
文章目录 一、介绍二、应用三、案例1. 麦当劳11随心配2. 代码演示3. 演示结果 四、优缺点五、送给读者 一、介绍 建造者模式(Builder Pattern)属于创建型设计模式,很多博客文章的对它的作用解释为用于将复杂对象的创建过程与其细节表示分离。但对于初学者来说&…...

Java之包,权限修饰符,final关键字详解
包 2.1 包 包在操作系统中其实就是一个文件夹。包是用来分门别类的管理技术,不同的技术类放在不同的包下,方便管理和维护。 在IDEA项目中,建包的操作如下: 包名的命名规范: 路径名.路径名.xxx.xxx // 例如ÿ…...
“深入解析JVM:Java虚拟机内部原理揭秘“
标题:深入解析JVM:Java虚拟机内部原理揭秘 摘要:本文将深入探讨Java虚拟机(JVM)的内部原理,包括JVM的架构、运行时数据区域、垃圾回收机制以及即时编译器等重要组成部分。通过对JVM内部原理的解析…...

Mac下Jmeter安装及基本使用
本篇文章只是简单的介绍下Jmeter的下载安装和最基本使用 1、初识Jmeter 前一段时间客户端app自测的过程中,有偶现请求某个接口返回数据为空的问题,领导让我循环100次请求这个接口,看看有没有结果为空的问题。听同事说有Jmeter的专业测试工具…...

云计算与边缘计算:加速数字化转型的关键驱动力
云计算和边缘计算技术正以惊人的速度改变着企业的业务和基础架构。这些先进的技术为企业带来了灵活性、可扩展性和成本效益的优势,重新定义了业务运作的方式。 云计算是通过互联网将计算资源提供给用户的一种服务模式。通过云计算,企业可以将应用程序、…...

TheGem主题 - 创意多用途和高性能WooCommerce WordPress主题/网站
TheGem主题概述 – 适合所有人的TheGem 作为设计元素、样式和功能的终极 Web 构建工具箱而设计和开发,TheGem主题将帮助您在几分钟内构建一个令人印象深刻的高性能网站,而无需触及一行代码。不要在编码上浪费时间,探索你的创造力!…...
Pytorch-day10-模型部署推理-checkpoint
模型部署&推理 模型部署模型推理 我们会将PyTorch训练好的模型转换为ONNX 格式,然后使用ONNX Runtime运行它进行推理 1、ONNX ONNX( Open Neural Network Exchange) 是 Facebook (现Meta) 和微软在2017年共同发布的,用于标准描述计算图的一种格式…...
vue使用websocket
建立websocket.js // 信息提示 import { Message } from element-ui // 引入用户id import { getTenantId, getAccessToken } from /utils/auth// websocket地址 var url ws://192.168.2.20:48081/websocket/message // websocket实例 var ws // 重连定时器实例 var tt // w…...

jmeter入门:接口压力测试全解析
一.对接口压力测试 1.配置 1.添加线程组(参数上文有解释 这里不介绍) 2.添加取样器 不用解释一看就知道填什么。。。 3.添加头信息(否则请求头对不上) 也不用解释。。。 4.配置监听器 可以尝试使用这几个监听器。 2.聚合结果…...

go、java、.net、C#、nodejs、vue、react、python程序问题进群咨询
1、面试辅导 2、程序辅导 3、一对一腾讯会议辅导 3、业务逻辑辅导 4、各种bug帮你解决。 5、培训小白 6、顺利拿到offer...
树莓派4B最新系统Bullseye 64 bit使用xrdp远程桌面黑屏卡顿问题
1、树莓派换源 打开源文件 sudo nano /etc/apt/sources.list注释原来的,更换为清华源 deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib no…...
EasyExcel入门介绍及工具类,网络下载excel
前言:在这里分享自己第一次使用EasyExcel并且编写工具类,且在接口中支持excel文件下载的一系列流程,包含所有前后端(JSJAVA)完整代码,可以根据自己需要自行提取,仅供参考。 一.引入EasyExcel依赖…...

【HarmonyOS北向开发】-04 ArkTS开发语言-ArkTS基础知识
飞书原文档:Docs...

【Alibaba中间件技术系列】「RocketMQ技术专题」小白专区之领略一下RocketMQ基础之最!
应一些小伙伴们的私信,希望可以介绍一下RocketMQ的基础,那么我们现在就从0开始,进入RocketMQ的基础学习及概念介绍,为学习和使用RocketMQ打好基础! RocketMQ是一款快速地、可靠地、分布式、容易使用的消息中间件&#…...
营销活动:提升小程序的用户活跃度的关键
在现今竞争激烈的商业环境中,小程序已成为企业私域营销的重要工具之一。然而,拥有一个小程序并不足以保证用户的活跃度。营销活动作为推动用户参与的有效方式,对于提升小程序的用户活跃度起着至关重要的作用。本文将深入探讨营销活动在提升小…...
Neo4j之CALL基础
CALL 语句用于调用 Neo4j 数据库中预定义的函数、过程或者自定义的函数。它是用来执行一些特定操作或计算的重要工具。以下是一些常用的 CALL 语句示例和解释: 调用内置函数: CALL db.labels()这个示例中,调用了内置函数 db.labels() 来获取…...

【TypeScript】元组
元组(Tuple)是 TypeScript 中的一种特殊数据类型,它允许你定义一个固定数量和类型的元素组合。元组可以包含不同类型的数据,每个数据的类型在元组中都是固定的。以下是 TypeScript 中元组的基本用法和特点: // 声明一…...

【入坑系列】TiDB 强制索引在不同库下不生效问题
文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比
在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...

基于Java+MySQL实现(GUI)客户管理系统
客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息,对客户进行统一管理,可以把所有客户信息录入系统,进行维护和统计功能。可通过文件的方式保存相关录入数据,对…...

【Linux】Linux 系统默认的目录及作用说明
博主介绍:✌全网粉丝23W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...

【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...

李沐--动手学深度学习--GRU
1.GRU从零开始实现 #9.1.2GRU从零开始实现 import torch from torch import nn from d2l import torch as d2l#首先读取 8.5节中使用的时间机器数据集 batch_size,num_steps 32,35 train_iter,vocab d2l.load_data_time_machine(batch_size,num_steps) #初始化模型参数 def …...