当前位置: 首页 > news >正文

008、所有权

        所有权可以说是Rust中最为独特的一个功能了。正是所有权概念和相关工具的引入,Rust才能够在没有垃圾回收机制的前提下保障内存安全。

        因此,正确地了解所有权概念及其在Rust中的实现方式,对于所有Rust开发者来讲都是十分重要的。在本文中,我们会详细地讨论所有权及其相关功能:借用、切片,以及Rust在内存中布局数据的方式。

        先预览下本文要讲的主要内容:

1. 栈与堆

        由于所有权的某些内容会涉及栈与堆,所以让我们先来简单地了解一下它们。

        栈和堆都是代码在运行时可以使用的内存空间,不过它们通常以不同的结构组织而成。栈会以我们放入值时的顺序来存储它们,并以相反的顺序将值取出。

        这也就是所谓的 “后进先出” 策略。你可以把栈上的操作想象成堆放盘子:当你需要放置盘子时,你只能将它们放置在栈的顶部,而当你需要取出盘子时,你也只能从顶部取出。

        换句话说,你没有办法从中间或底部插入或移除盘子。用术语来讲,添加数据这一操作被称作入栈,移除数据则被称作出栈

        所有存储在栈中的数据都必须拥有一个已知且固定的大小。对于那些在编译期无法确定大小的数据,你就只能将它们存储在堆中。

        堆空间的管理是较为松散的:当你希望将数据放入堆中时,你就可以请求特定大小的空间。操作系统会根据你的请求在堆中找到一块足够大的可用空间,将它标记为已使用,并把指向这片空间地址的指针返回给我们。

        这一过程就是所谓的堆分配,它也常常被简称为分配。将值压入栈中不叫分配。由于指针的大小是固定的且可以在编译期确定,所以可以将指针存储在栈中。当你想要访问指针所指向的具体数据时,可以通过指针指向的地址来访问。

        你可以把这个过程想象为到餐厅聚餐。当你到达餐厅表明自己需要的座位数后,服务员会找到一张足够大的空桌子,并将你们领过去入座。

        即便这时有小伙伴来迟了,他们也可以通过询问你们就座的位置来找到你们。向栈上推入数据要比在堆上进行分配更有效率一些,因为操作系统省去了搜索新数据存储位置的工作;这个位置永远处于栈的顶端。

        除此之外,操作系统在堆上分配空间时还必须首先找到足够放下对应数据的空间,并进行某些记录工作来协调随后进行的其余分配操作。

        由于多了指针跳转的环节,所以访问堆上的数据要慢于访问栈上的数据。一般来说,现代处理器在进行计算的过程中,由于缓存的缘故,指令在内存中跳转的次数越多,性能就越差。

        继续使用上面的餐厅来作类比。假设现在同时有许多桌的顾客正在等待服务员的处理。那么最高效的处理方式自然是报完一张桌子所有的订单后再接着服务下一张桌子的顾客。

        而一旦服务员每次在单个桌子前只处理单个订单,那么他就不得不浪费较多的时间往返于不同的桌子之间。

        出于同样的原因,处理器在操作排布紧密的数据(比如在栈上)时要比操作排布稀疏的数据(比如在堆上)有效率得多。另外,分配命令本身也可能消耗不少时间周期。

        许多系统编程语言都需要你记录代码中分配的堆空间,最小化堆上的冗余数据,并及时清理堆上的无用数据以避免耗尽空间。

        而所有权概念则解决了这些问题。一旦你熟练地掌握了所有权及其相关工具,就可以将这些问题交给Rust处理,减轻用于思考栈和堆的心智负担。不过,知晓如何使用和管理堆内存可以帮助我们理解所有权存在的意义及其背后的工作原理。

2. 所有权规则

        现在,让我们来具体看一看所有权规则。你最好先将这些规则记下来,我们会在随后的内容中通过示例来解释它们:

💫 Rust中,每一个值都有一个对应的变量作为它的所有者

💫 在同一时间内,值有且仅有一个所有者

💫 当所有者离开自己的作用域时,它持有的值就会被释放掉。 

3. 变量作用域 

         作为所有权的第一个示例,我们先来了解一下变量的作用域。简单来讲,作用域是一个对象在程序中有效的范围。假设有这样一个变量:

let s = "hello";

        这里的变量 s 指向了一个字符串字面量,它的值被硬编码到了当前的程序中。变量从声明的位置开始直到当前作用域结束都是有效的。下面示例中的注释对变量的有效范围给出了具体的说明: 

{                               // 由于变量 s 还未被声明,所以它在这里是不可用的  let s = "hello";            // 从这里开始,变量 s 变成可用// 执行与 s 相关的操作
}                               // 作用域到这里结束,变量 s 再次不可用

        这里有两个重点:

💫 s 在进入作用域后变得有效。

💫 它会保持自己的有效性直到自己离开作用域为止。

        到目前为止,Rust语言中变量的有效性与作用域之间的关系跟其他编程语言中的类似。现在,让我们继续在作用域的基础上学习 String 类型。 

4. String类型 

        为了演示所有权的相关规则,我们需要一个特别的数据类型,它要比之前文章的“数据类型”中涉及的类型都更加复杂。

        之前接触的那些类型会将数据存储在栈上,并在离开自己的作用域时将数据弹出栈空间。我们需要一个存储在堆上的数据类型来研究Rust是如何自动回收这些数据的。

        我们将以 String 类型为例,并将注意力集中到 String 类型与所有权概念相关的部分。这些部分同样适用于标准库中提供的或你自己创建的其他复杂数据类型。

        我们会在后面文章中更加深入地讲解 String 类型。你已经在上面的示例中接触过字符串字面量了,它们是那些被硬编码进程序的字符串值。

        字符串字面量的确是很方便,但它们并不能满足所有需要使用文本的场景。原因之一在于字符串字面量是不可变的。而另一个原因则在于并不是所有字符串的值都能够在编写代码时确定:假如我们想要获取用户的输入并保存,应该怎么办呢?

        为了应对这种情况,Rust提供了第二种字符串类型 String 这个类型会在堆上分配到自己需要的存储空间,所以它能够处理在编译时未知大小的文本。你可以调用 from 函数根据字符串字面量来创建一个 String 实例: 

let s = String::from("hello");

        这里的双冒号(::)运算符允许我们调用置于 String 命名空间下面的特定 from 函数,而不需要使用类似于 string_from 这样的名字。上面定义的字符串对象能够被声明为可变的: 

let mut s = String::from("hello");s.push_str(", world!"); // push_str() 函数向String空间的尾部添加了一段字面量println!("{}", s); // 这里会输出完整的hello, world!

        你也许会问:为什么 String 是可变的,而字符串字面量不是?这是因为它们采用了不同的内存处理方式。

5. 内存与分配

        对于字符串字面量而言,由于我们在编译时就知道其内容,所以这部分硬编码的文本被直接嵌入到了最终的可执行文件中。

        这就是访问字符串字面量异常高效的原因,而这些性质完全得益于字符串字面量的不可变性。

        不幸的是,我们没有办法将那些未知大小的文本在编译期统统放入二进制文件中,更何况这些文本的大小还可能随着程序的运行而发生改变。

        对于 String 类型而言,为了支持一个可变的、可增长的文本类型,我们需要在堆上分配一块在编译时未知大小的内存来存放数据。这同时也意味着: 

💫 我们使用的内存是由操作系统在运行时动态分配出来的。

💫 当使用完 String 时,我们需要通过某种方式来将这些内存归还给操作系统。

        这里的第一步由我们,也就是程序的编写者,在调用 String::from 时完成,这个函数会请求自己需要的内存空间。

        在大部分编程语言中都有类似的设计:由程序员来发起堆内存的分配请求。然而,对于不同的编程语言来说,第二步实现起来就各有区别了。

        在某些拥有垃圾回收(Garbage Collector,GC)机制的语言中,GC 会代替程序员来负责记录并清除那些不再使用的内存。

        而对于那些没有 GC 的语言来说,识别不再使用的内存并调用代码显式释放的工作就依然需要由程序员去完成,正如我们请求分配时一样。

        按照以往的经验来看,正确地完成这些任务往往是十分困难的。假如我们忘记释放内存,那么就会造成内存泄漏;假如我们过早地释放内存,那么就会产生一个非法变量;假如我们重复释放同一块内存,那么就会产生无法预知的后果。

        为了程序的稳定运行,我们必须严格地将分配和释放操作一一对应起来。与这些语言不同,Rust提供了另一套解决方案:内存会自动地在拥有它的变量离开作用域后进行释放。下面的代码类似于上面示例中的代码,不过我们将字符串字面量换成了 String 类型:

}let s = String::from("hello");  // 从此处开始,变量 s 开始生效// 执行与 s 相关的操作}                                   // 作用域到此处结束,变量 s 失效

        审视上面的代码,有一个很适合用来回收内存给操作系统的地方:变量 s 离开作用域的地方。Rust在变量离开作用域时,会调用一个叫作drop的特殊函数

        String 类型的作者可以在这个函数中编写释放内存的代码。记住,Rust会在作用域结束的地方即 } 处自动调用 drop 函数。 

注意

        在 C++ 中,这种在对象生命周期结束时释放资源的模式有时也被称作 资源获取即初始化(Resource Acquisition Is Initialization, RAII)。假如你使用过类似的模式,那么你应该对Rust中的特殊函数 drop 并不陌生。 

        这种模式极大地影响了Rust中的许多设计抉择,并最终决定了我们现在编写Rust代码的方式。

        在上面的例子中,这套释放机制看起来也许还算简单,然而一旦把它放置在某些更加复杂的环境中,代码呈现出来的行为往往会出乎你的意料,特别是当我们拥有多个指向同一处堆内存的变量时。

        让我们接着来看一看其中一些可能的使用场景。

5.1 变量和数据交互的方式:移动

        Rust中的多个变量可以采用一种独特的方式与同一数据进行交互。让我们看一看下面示例中的代码,这里使用了一个整型作为数据: 

let x = 5;
let y = x;

        这个示例是将变量 x 绑定的整数值重新绑定到变量 y 上。

        你也许能够猜到这段代码的执行效果:将整数值 5 绑定到变量 x 上;然后创建一个 x 值的拷贝,并将它绑定到 上。结果我们有了两个变量 xy,它们的值都是 5

        这正是实际发生的情形,因为整数是已知固定大小的简单值,两个值 5 会同时被推入当前的栈中。现在,让我们看一看这段程序的 String 版本:

let s1 = String::from("hello");
let s2 = s1;

        以上两段代码非常相似,你也许会假设它们的运行方式也是一致的。也就是说,第二行代码可能会生成一个 s1 值的拷贝,并将它绑定到 s2 上。不过,事实并非如此。 

        下图展示了 String 的内存布局,它实际上由 3 部分组成,如图左侧所示:一个指向存放字符串内容的指针(ptr)、一个长度(len)及一个容量(capacity),这部分的数据存储在了栈中。图片右侧显示了字符串存储在堆上的文本内容。

图1:绑定到变量s1上、拥有值“hello”的String的内存布局

        长度字段被用来记录当前 String 中的文本使用了多少字节的内存。而容量字段则被用来记录 String 向操作系统总共获取到的内存字节数量。

        长度和容量之间的区别十分重要,但我们先不去讨论这个问题,简单地忽略容量字段即可。当我们将 s1 赋值给 s2 时,便复制了一次 String 的数据,这意味着我们复制了它存储在栈上的指针、长度及容量字段。

        但需要注意的是,我们没有复制指针指向的堆数据。换句话说,此时的内存布局应该类似于图2。

        由于Rust不会在复制值时深度地复制堆上的数据,所以这里的布局不会像图3中所示的那样。假如Rust依照这样的模式去执行赋值,那么当堆上的数据足够大时,类似于 s2 = s1 这样的指令就会造成相当可观的运行时性能消耗。 

图2:变量s2在复制了s1的指针、长度及容量后的内存布局

图3:当Rust也复制了堆上的数据时,执行完s2 = s1语句后可能产生的内存布局

        前面我们提到过,当一个变量离开当前的作用域时,Rust会自动调用它的 drop 函数,并将变量使用的堆内存释放回收。

        不过,图2中展示的内存布局里有两个指针指向了同一个地址,这就导致了一个问题:当 s2 s1 离开自己的作用域时,它们会尝试去重复释放相同的内存。

        这也就是我们之前提到过的内存错误之一,臭名昭著的二次释放。重复释放内存可能会导致某些正在使用的数据发生损坏,进而产生潜在的安全隐患。

        为了确保内存安全,同时也避免复制分配的内存,Rust在这种场景下会简单地将 s1 废弃,不再视其为一个有效的变量。

        因此,Rust也不需要在 s1 离开作用域后清理任何东西。试图在 s2 创建完毕后使用 s1(如下所示)会导致编译时错误。 

let s1 = String::from("hello");
let s2 = s1;println!("{}, world!", s1);

        假如你在其他语言中接触过浅度拷贝(shallow copy)和深度拷贝(deep copy)这两个术语,那么你也许会将这里复制指针、长度及容量字段的行为视作浅度拷贝。

        但由于Rust同时使第一个变量无效了,所以我们使用了新的术语移动(move)来描述这一行为,而不再使用浅度拷贝。

        在上面的示例中,我们可以说 s1 被移动到了 s2 中。在这个过程中所发生的操作如图4所示。 

图4:s1变为无效之后的内存布局

        这一语义完美地解决了我们的问题!既然只有 s2 有效,那么也就只有它会在离开自己的作用域时释放空间,所以再也没有二次释放的可能性了。

        另外,这里还隐含了另外一个设计原则:Rust永远不会自动地创建数据的深度拷贝。因此在Rust中,任何自动的赋值操作都可以被视为高效的

5.2 变量和数据交互的方式:克隆

        当你确实需要去深度拷贝 String 堆上的数据,而不仅仅是栈数据时,就可以使用一个名为 clone 的方法。

        我们将在后面文章中讨论类型方法的语法,但你应该在其他语言中见过类似的东西。下面是一个实际使用 clone 方法的例子: 

let s1 = String::from("hello");
let s2 = s1.clone();println!("s1 = {}, s2 = {}", s1, s2);

        这段代码在Rust中完全合法,它显式地生成了图3中的行为:复制了堆上的数据。

        当你看到某处调用了 clone 时,你就应该知道某些特定的代码将会被执行,而且这些代码可能会相当消耗资源。你可以很容易地在代码中察觉到一些不寻常的事情正在发生。 

5.3 栈上数据的复制

        上面的讨论中遗留了一个没有提及的知识点。我们在之前的代码示例中曾经使用整型编写出了如下所示的合法代码: 

let x = 5;
let y = x;println!("x = {}, y = {}", x, y);

        这与我们刚刚学到的内容似乎有些矛盾:即便代码没有调用 clonex 在被赋值给 y 后也依然有效,且没有发生移动现象。

        这是因为类似于整型的类型可以在编译时确定自己的大小,并且能够将自己的数据完整地存储在栈中,对于这些值的复制操作永远都是非常快速的。

        这也同样意味着,在创建变量 y 后,我们没有任何理由去阻止变量 x 继续保持有效。换句话说,对于这些类型而言,深度拷贝与浅度拷贝没有任何区别,

        调用 clone 并不会与直接的浅度拷贝有任何行为上的区别。因此,我们完全不需要在类似的场景中考虑上面的问题。

        Rust提供了一个名为 Copytrait,它可以用于整数这类完全存储在栈上的数据类型(我们会在后面文章中详细地介绍 trait)。一旦某种类型拥有了 Copy 这种 trait,那么它的变量就可以在赋值给其他变量之后保持可用性。

        如果一种类型本身或这种类型的任意成员实现了 Drop 这种 trait,那么Rust就不允许其实现 Copy 这种 trait。尝试给某个需要在离开作用域时执行特殊指令的类型实现 Copy 这种 trait 会导致编译时错误。

        那么究竟哪些类型是 Copy 的呢?你可以查看特定类型的文档来确定,不过一般来说,任何简单标量的组合类型都可以是 Copy 的,任何需要分配内存或某种资源的类型都不会是 Copy 的。

        下面是一些拥有 Copy 这种trait的类型:

💫 所有的整数类型,诸如 u32

💫 仅拥有两种值(true false)的布尔类型:bool

💫 字符类型:char

💫 所有的浮点类型,诸如 f64

💫 如果元组包含的所有字段的类型都是 Copy 的,那么这个元组也是 Copy 的。例如,(i32, i32)是 Copy 的,但(i32, String)则不是。

6. 所有权与函数

        将值传递给函数在语义上类似于对变量进行赋值。将变量传递给函数将会触发移动或复制,就像是赋值语句一样。下面示例展示了变量在这种情况下作用域的变化过程:

示例1:函数中所有权和作用域的变化过程

        尝试在调用 takes_ownership 后使用变量 s 会导致编译时错误。这类静态检查可以使我们免于犯错。你可以尝试在main函数中使用 s x 变量,来看一看在所有权规则的约束下能够在哪些地方合法地使用它们。 

7. 返回值与作用域

        函数在返回值的过程中也会发生所有权的转移。示例2像示例1一样详细地标注了这种情况下变量所有权和作用域的变化过程: 

示例2:函数在返回值时所有权的转移过程

         变量所有权的转移总是遵循相同的模式:将一个值赋值给另一个变量时就会转移所有权。

        当一个持有堆数据的变量离开作用域时,它的数据就会被 drop 清理回收,除非这些数据的所有权移动到了另一个变量上。

        在所有的函数中都要获取所有权并返回所有权显得有些烦琐。假如你希望在调用函数时保留参数的所有权,那么就不得不将传入的值作为结果返回。

        除了这些需要保留所有权的值,函数还可能会返回它们本身的结果。当然,你可以利用元组来同时返回多个值,如下面代码所示:

fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len); 
} fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len()会返回当前字符串的长度 (s, length) 
} 

        但这种写法未免太过笨拙了,类似的概念在编程工作中相当常见。幸运的是,Rust针对这类场景提供了一个名为引用的功能,这个我们下篇文章再讲。 

        最后,码字不易,如果大家能给我一个赞,我也会动力满满,万分感谢你们的点赞支持!

相关文章:

008、所有权

所有权可以说是Rust中最为独特的一个功能了。正是所有权概念和相关工具的引入,Rust才能够在没有垃圾回收机制的前提下保障内存安全。 因此,正确地了解所有权概念及其在Rust中的实现方式,对于所有Rust开发者来讲都是十分重要的。在本文中&…...

千里马2023年终总结-android framework实战

背景: hi粉丝朋友们: 2023年马上就过去了,很多学员朋友也都希望马哥这边写个年终总结,因为这几个月时间都忙于新课程halsystracesurfaceflinger专题的开发,差点都忘记了这个事情了,今天特别花时间来写个bl…...

vue3中pinia的使用及持久化(详细解释)

解释一下pinia: Pinia是一个基于Vue3的状态管理库,它提供了类似Vuex的功能,但是更加轻量化和简单易用。Pinia的核心思想是将所有状态存储在单个store中,并且将store的行为和数据暴露为可响应的API,从而实现数据&#…...

安装 yarn、pnpm、功能比较

安装 yarn 官网:https://classic.yarnpkg.com/ 快速、可靠和安全的依赖性管理。 Yarn是您代码的软件包管理器。它允许您使用和共享(例如JavaScript)与来自世界各地的其他开发人员一起编写代码。Yarn是一个新的快速安全可信赖的可以替代 NP…...

计算机专业个人简历范文(8篇)

HR浏览一份简历也就25秒左右,如果你连「好简历」都没有,怎么能找到好工作呢? 如果你不懂得如何在简历上展示自己,或者觉得怎么改简历都不出彩,那请你一定仔细读完。 互联网运营个人简历范文> 男 22 本科 AI简历…...

几个实用网站

论文短语:https://www.phrasebank.manchester.ac.uk/ 翻译:https://www.deepl.com/en/translator 润色:https://quillbot.com/ 榜单:www.paperwithcode.com ****NLP民工的乐园: 几乎最全的中文NLP资源库:****https…...

Pycharm 切换interpreter---python的环境和第三方库问题

这篇回答两个问题: 1.为什么在 pycharm中打开新的project,切换interpreter 之后发现自己之前装的库消失了? 2.为什么 interpreter 切换到python3.8了, terminal 还是在 3.9?? 问题的关键:搞懂什…...

TP-LINK 路由器忘记密码 - 恢复出厂设置

TP-LINK 路由器忘记密码 - 恢复出厂设置 1. 恢复出厂设置2. 创建管理员密码3. 上网设置4. 无线设置5. TP-LINK ID6. 网络状态References 1. 恢复出厂设置 在设备通电的情况下,按住路由器背面的 Reset 按钮直到所有指示灯同时亮起后松开。 2. 创建管理员密码 3. 上网…...

关闭 Elasticsearch 集群的安全性设置

关闭 Elasticsearch 集群的安全性设置,特别是如果您正在使用 X-Pack,涉及到修改 Elasticsearch 的配置。以下是一般步骤,但请注意,这可能会使您的 Elasticsearch 集群面临安全风险,因此建议仅在开发或测试环境中执行此…...

[技术分享]一招解决 MySQL 中 DDL 被阻塞的问题

爱可生开源社区. 爱可生开源社区,提供稳定的MySQL企业级开源工具及服务,每年1024开源一款优良组件,并持续运营维护。 背景 之前碰到客户咨询定位DDL阻塞的相关问题,整理了一下方法,如何解决DDL被阻塞的问题。下面,就这个问题,整理了一下思路: 怎么判断一个 DDL 是…...

Windows搭建Emby媒体库服务器,无公网IP远程访问本地影音文件

文章目录 1.前言2. Emby网站搭建2.1. Emby下载和安装2.2 Emby网页测试 3. 本地网页发布3.1 注册并安装cpolar内网穿透3.2 Cpolar云端设置3.3 Cpolar内网穿透本地设置 4.公网访问测试5.结语 1.前言 在现代五花八门的网络应用场景中,观看视频绝对是主力应用场景之一&…...

自动化测试系列 之 Python单元测试框架unittest

一、概述 什么是单元测试 单元测试是一种软件测试方法,是测试最小的可测试单元,通常是一个函数或一个方法。 在软件开发过程中,单元测试作为一项重要的测试方法被广泛应用。 为什么需要单元测试 单元测试是软件开发中重要的一环&#xf…...

C语言朴素算法

#include <stdio.h> #include <string.h>// 朴素算法&#xff0c;用于字符串匹配 void naiveMatch(char* text, char* pattern) {int textLength strlen(text); // 计算文本串长度int patternLength strlen(pattern); // 计算模式串长度for …...

【力扣题解】P501-二叉搜索树中的众数-Java题解

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【力扣题解】 文章目录 【力扣题解】P501-二叉搜索树中的众数-Java题解&#x1f30f;题目描述&#x1f4a1;题解&#x1f…...

Wnmp本地部署结合内网穿透实现任意浏览器远程访问本地服务

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 文章目录 前言1.Wnmp下载安装2.Wnmp设置3.安装cpolar内网穿透3.1…...

深信服AF防火墙配置SSL VPN

防火墙版本&#xff1a;8.0.85 需提前确认防火墙是是否有SSL VPN的授权&#xff0c;确认授权用户数量 1、确认内外网接口划分 2、网络→SSL VPN&#xff0c;选择内外网接口地址 3、SSL VPN→用户管理→新增一个SSL VPN的用户 4、新增L3VPN资源&#xff0c;类型选择Other&…...

在Spring Cloud中使用Gateway 网关

我们在上述文章中介绍了相关Spring Cloud的五大核心组件&#xff0c;现在我们来了解一下关于Spring Cloud的网关&#xff0c;关于使用网关&#xff0c;我们同时也需要知道他在一个架构中起到的作用&#xff0c;并且&#xff0c;我们需要知道网关具体的相关功能&#xff0c;本篇…...

【Python】配置环境变量

Python配置Windows系统环境变量 打开电脑属性 ——> 高级系统设置 ——> 高级 ——> 环境变量 Python安装目录 D:\Program Files\Python39 winR打开运行&#xff0c;输入cmd打开命令窗口 python -V...

使用.Net nanoFramework 驱动ESP32的OLED显示屏

本文介绍如何使用.Net nanoFramework 驱动ESP32的OLED显示屏。我们将会从最基础的部分开始&#xff0c;逐步深入&#xff0c;让你能够理解并实现整个过程。无论你是初学者还是有一定经验的开发者&#xff0c;这篇文章都会对你有所帮助。 1. 硬件准备 1.1 ESP32开发板 这里我们…...

0基础学习VR全景平台篇第134篇:720VR全景,云台调整节点

相机、云台和脚架全套设备组装完成后需要进行调校才能开始拍摄。这一节&#xff0c;我们将主要介绍云台调整的两个内容&#xff1a;对中心靶、调三点一线。&#xff08;后附调校原理&#xff09; 云台部件名称 一、调节准备 &#xff08;一&#xff09;对于安装好的云台 1.检…...

扫地机器人地图与用户终端的同步

以下内容为本人的学习笔记&#xff0c;如需要转载&#xff0c;请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/APaJheSbgTW3jNssWsp5Ng 地图数据来源于机器人算法模块&#xff0c;一般通过SLAM算法完成建图的过程。 建图过程中&#xff0c;基础数据涉及到各…...

使用机器学习进行语法错误检测/纠正

@francescofranco_39234 一、说明 一般的学习,特别是深度学习,促进了自然语言处理。各种模型使人们能够执行机器翻译、文本摘要和情感分析——仅举几个用例。今天,我们将研究另一个流行的用途:我们将使用Gramformer构建一个用于机器学习语法错误检测和纠正的管道。 阅读本文…...

从0到1快速入门ETLCloud

一、ETLCloud的介绍 ETL是将业务系统的数据经过抽取&#xff08;Extract&#xff09;、清洗转换&#xff08;Transform&#xff09;之后加载&#xff08;Load&#xff09;到数据仓库的过程&#xff0c;目的是将企业中的分散、凌乱、标准不统一的数据整合到一起&#xff0c;为企…...

QT上位机开发(会员管理软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们学习了ini文件的解析办法&#xff0c;通过QSettings类就可以很轻松地访问ini文件里面的数据。除了ini文件之外&#xff0c;另外一种经常出…...

线性代数笔记3 1.1

学习视频&#xff1a; 2.2 矩阵运算&#xff08;二&#xff09;_哔哩哔哩_bilibili 包括内容&#xff1a; p10矩阵运算&#xff08;二&#xff09; p11特殊矩阵 p12逆矩阵&#xff08;一&#xff09; p13逆矩阵&#xff08;二&#xff09;...

2023年12月编程语言排行榜

TIOBE Index for December 2023 December Headline: C# on its way to become programming language of the year 2023 2023年12月的TIOBE指数&#xff1a;12月头条:c#将成为2023年最佳编程语言 Yes, I know, we have been here before. At the end of 2022, it looked like …...

Redis VS Memcached:选择哪个更适合您的应用?

目录 1、前言 2、概念简介 2.1 Redis 2.2 Memcached 3、数据模型 4、持久性 5、分布式能力 6、性能和扩展性 7、如何选择适合您引用的缓存系统 8、结语 1、前言 Redis和Memcached都是常见的内存缓存系统&#xff0c;用于提升应用程序的性能和可扩展性。它们都具有高…...

【HarmonyOS开发】共享包HAR和HSP的创建和使用以及三方库的发布

OpenHarmony提供了两种共享包&#xff0c;HAR&#xff08;Harmony Archive&#xff09;静态共享包&#xff0c;和HSP&#xff08;Harmony Shared Package&#xff09;动态共享包。 HAR与HSP都是为了实现代码和资源的共享&#xff0c;都可以包含代码、C库、资源和配置文件&…...

安装 Node.js、npm

安装 nodejs 安装Node.js的最简单的方法是通过软件包管理器。 Node.js官网&#xff1a;https://nodejs.org/en/download/ cd /usr/local/src/wget -c https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-x64.tar.xz xz -d node-v18.16.0-linux-x64.tar.xz tar -xf node…...

解决报错:找不到显卡

今天做实验碰到一个问题&#xff1a;torch找不到显卡&#xff1a; 打开任务管理器&#xff0c;独显直接没了&#xff0c;一度以为是要去修电脑了&#xff0c;突然想到上次做实验爆显存&#xff0c;屏蔽了gpu用cpu训练&#xff1a; import os os.environ["CUDA_DEVICE_OR…...

免费网站app下载/淘宝流量网站

*) RequestMapping(value"/xxx/{id}",method{RequestMethod.GET}) method 不写的话&#xff0c;默认GET、POST都支持&#xff0c;根据前端方式自动适应 转载于:https://www.cnblogs.com/mySummer/p/11057147.html...

关于配色的网站/广州推广工具

会发现这里这两个方法无法重载 因为在 public Integer method(List stringList)&#xff1b;和 public String method(List integers)&#xff1b;中 参数最终都是在编译时候擦除类型 变成List 这样就无法重载了&#xff0c;重载必须方法签名不同...

外贸网站建设公司案例/18款禁用看奶app入口

思路&#xff1a; 这题应该不止一种解法&#xff0c;其中的一种可以看作是leetcode85https://www.cnblogs.com/wangyiming/p/11059176.html的加强版&#xff1a; 首先对于每一行&#xff0c;分别使用滑动窗口法将这一行划分成符合题目定义的若干合法子段s1, s2, s3, ...。对于每…...

威海城乡建设委员会网站/seo技术团队

芯片封装详细介绍装配工艺一、DIP双列直插式封装DIP(DualIn&#xff0d;LINE PACKAGE)是指采用双列直插形式封装的集成电路芯片&#xff0c;绝大多数中小规模集成电路(IC)均采用这种封装形式&#xff0c;其引脚数一般不超过100个。采用DIP封装的CPU芯片有两排引脚&#xff0c;需…...

网站建设中最基本的决策/百度app

OSI七层参考模型&#xff1a;&#xff08;注意数据发送和接收的顺序不同&#xff0c;发送由7到1&#xff0c;接收由1到7&#xff09; 物理层&#xff1a;高低电平如何传输&#xff1b;数据链路层&#xff1a;提供介质&#xff0c;建立传输线路&#xff1b;网络层&#xff1a;提…...

请人做网站需要多少钱/百度链接提交入口

如果在控制台程序启动类末尾写了: Console.ReadLine(); 则在linux后台运行会抛出异常: at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirectory, Func`2 errorRewriter)at Interop.CheckIo(Int64 result, String path, Boolean isDire…...