rust 权限,rust 所有权机制

这是我对这些事情的描述。 一旦掌握了它,所有这些在直观上都是显而易见的且美丽的,并且您不知道之前缺少了哪一部分。我不会从头开始教您,也不会重复《Rust教程书》

这是我对这些事情的解释。 一旦你掌握了它的窍门,一切都是那么直观、明显和美丽,我不知道我之前错过了什么部分。

我不会从头开始教您,也不会重复《Rust教程书》(尽管可能是这种情况)。如果您还没有阅读相应的章节,请立即阅读。 本文无意取代《Rust教程书》,而是对其进行补充。

我建议阅读这篇精彩的文章。 我们实际上正在讨论类似的主题,但重点关注它们的其他方面。

我们来谈谈资源。 资源是有价值的、“重的”、可以获取和释放(或销毁)的东西,例如套接字、打开的文件、信号量、锁和堆内存区域。 传统上,所有这些东西都是通过调用一个函数来创建的,该函数返回某种对资源本身的引用(“内存指针,文件描述符”)。当程序决定使用该资源完成时,必须显式关闭这些引用。

这种方法存在问题。 首先,很容易忘记释放一些资源,导致所谓的泄漏。 更糟糕的是,人们可能会尝试访问已经免费(免费使用)的资源。 如果幸运的话,您可能会收到一条错误消息,该消息将帮助您识别并修复错误。 否则,它们所拥有的引用(即使逻辑上无效)位于已被其他资源占用的“位置”中,即在已存储其他内容的内存中,在其他位置中它可能指向打开的文件正在使用的文件描述符。 尝试通过无效引用访问过时的资源可能会损坏其他资源或导致程序完全崩溃。

我所说的这些问题并不是假设的。 它们一直在发生。 例如,查看Google Chrome 发布博客。使用空引用会导致许多错误和崩溃,并且需要花费大量时间和精力(以及金钱)来识别和修复它们。

这并不是说开发商愚蠢而忘记了。 逻辑流程本身很容易出错。资源应该释放,但不应该强迫。 此外,您通常不会注意到忘记释放资源,因为它几乎没有明显的影响。

实现一个简单的目标可能需要设计具有复杂逻辑的复杂解决方案。 很难不迷失在庞大的代码库中,难怪总是会出现错误。 其中大多数都很容易找到。 然而,虽然这些与资源相关的错误很难检测到,但如果在野外利用它们可能会极其危险。

当然,像Rust 这样的新语言无法解决错误。 但它能做的,也是它成功做的,是影响你的思维方式,并为其提供一个结构,以减少发生此类错误的可能性。

Rust 提供了一种安全、干净的资源管理方式。 而且你无法以任何其他方式管理它。 是的,这很难,但这就是我们的目标。

这些限制很大有几个原因。

· 它让你处于正确的心态。一旦您有了一些Rust 经验,您经常会尝试将相同的概念应用于其他语言,即使这些概念没有内置到语法中。

· 保护您的代码。 除了极少数情况外,所有“安全”的Rust 代码都保证不会受到我们在这里讨论的错误的影响。

· Rust 感觉就像具有垃圾收集功能的高级语言一样舒适(当我们说JavaScript 很有趣时,我们是在开玩笑吗?),但它与任何其他低级编译语言一样快速和原生。

考虑到这一点,让我们来看看Rust 的一些好处。

所有权

Rust 对于哪些代码段拥有资源有非常明确的规则。 在最简单的情况下,它是创建表示资源的对象的代码块。 在块结束时,对象被销毁并释放资源。 这里重要的区别是对象不是某种“容易被遗忘”的“弱引用”。 在内部,对象只是完全相同引用的包装器,但在外部,它看起来就像它所代表的资源。 当您删除它时(即,当您到达拥有它的代码末尾时),资源会自动且可预测地释放。不存在“忘记做某事”这样的事情。它以可预测且完全指定的方式自动发生。

(此时您可能想知道为什么我要解释像这样琐碎和明显的事情,而不是仅仅解释聪明人称之为RAII。是的,您是对的。让我们继续。)

这个概念适用于临时对象。 例如,假设您需要将一些文本写入文件。 专用代码块(例如函数)打开文件,检索文件对象(包装文件描述符),对其执行某些操作,然后在块末尾删除文件对象并删除文件。描述符已关闭。

但在很多情况下这个概念行不通。 资源可以传递给其他人,在多个“用户”之间共享,甚至在线程之间共享。

我们来看看这些。 首先,您将资源传递给另一个人(转让所有权),以便该人拥有该资源,可以用它做任何他们想做的事情,更重要的是,还负责释放它。

Rust 很好地支持了这一点。事实上,当您将资源提供给其他人时,这是默认完成的。

fn print_sum(v: Veci32) { println!(\'{}\’, v[0] + v[1]) //这里v 被删除并释放}fn main() { let mut v=Vec:new (); to 1.1000 { v.push(i) } //此时,v //使用超过4000 字节的内存//——— — ———- – //将所有权转移到print_sum: print_sum(v); //不再拥有或控制v //尝试访问v 现在会导致编译时错误println!(\’We \’rened\’); //此处不会发生释放。 //因为print_sum 负责一切。 资源从旧位置(例如局部变量)到新位置(函数参数)。 从性能角度来看,这只是一个“弱引用”问题,因此一切都运行得更快。 但作为代码,看起来您实际上已将整个资源移动到新位置。

移动与复制不同。 在内部,两者都是为了复制数据(如果Rust 允许复制资源,在这种情况下它们将是“弱引用”)。但是,移动后,原始变量的内容被认为不再有效。或者说很重要。 Rust 实际上假装该变量“逻辑上未初始化”,即填充了一些垃圾,就像您刚刚创建的变量一样。 禁止使用此类变量(除非用新值重新初始化)。 一旦资源被删除,就不会发生资源的重新分配。拥有资源的人有责任在终止时清理它。

导航不限于传递参数。 您可以将其移至变量。 为此,请转到返回值或从返回值到变量或函数参数。 基本上,显式或隐式赋值随处可见。

正如我们稍后将看到的,移动语义是一种处理资源的完全合理的方式,但对于简单的旧原始(数字)变量(一个int 值到另一个)来说这是一场灾难(想象一下无法复制!)。 幸运的是,Rust 具有“复制”功能。 实现此功能的类型(由所有原始类型使用)在赋值时使用复制语义;所有其他类型都使用移动语义。 这很简单。 如果你想复制你的类型,你可以实现复制特征。

fn print_sum(a: i32, b: i32) { println!(\'{}\’, a + b); //复制的a 和b 现已被删除并释放}fn main() { let a=35; //将副本的所有权转移给print_sum: print_sum(a, b) //这里我们保留对原始a 和b 变量的完全控制println! (\’ } and {}\’, a, b);原来的a 和b 现在被删除并释放} 现在,为什么移动语义有用?没有它们,一切都会很完美。 嗯,不完全是。 有时这是最合乎逻辑的事情。 考虑一个分配字符串缓冲区并将其返回给其调用者的函数(如下所示)。 所有权被转移,函数不再关心缓冲区的命运,调用者可以完全控制缓冲区,包括释放它的责任。

(C中也是这样,strdup这样的函数分配内存,交给用户,并期望用户管理并最终分配它,不同的是它只是一个指针,它最多能做的就是request/提醒您在完成后释放()。上面链接的文档几乎没有任何作用,是该语言的组成部分)。

另一个例子是这样的迭代器适配器。这会消耗获得的迭代器,因此稍后访问它是没有意义的。

相反的问题是,在这种情况下,您将需要对同一资源进行多次引用。 最明显的用例是进行多线程处理时。 否则,如果所有操作都按顺序执行,则移动语义几乎总是可以工作。 尽管如此,一直来回移动东西还是很不方便。

在某些情况下,即使您的代码按照严格的顺序执行,也会感觉有多个事情同时发生。 想象一下迭代一个向量。 当循环完成时,迭代器可以将涉及的向量的所有权转移给用户,但无法访问循环内的向量。也就是说,除非代码中存在每个迭代设备的迭代器。它变得一团糟。 似乎也没有一种方法可以遍历树而不破坏堆栈上的树,并且如果您想做其他事情,可以稍后重建它。

而且你将无法进行多线程处理。 而且很不方便。 即使它很丑。 值得庆幸的是,还有一个很酷的Rust 概念可以帮助我们。 借入即入!

借用

借用有多种推理方法。

· 您可以对某个资源有多个引用,同时遵循“单一所有者、单一责任”的概念。

· 引用类似于C 中的指针。

· 引用也是对象。 可修改的引用被移动,不可变的引用被复制。 当引用被删除时,借用结束(根据生命周期规则,请参阅下一节)。

· 在最简单的情况下,引用的行为“就像”来回移动所有权,但没有明确这样做。

我最后想说的是:

//不借用fn print_sum1(v: Veci32) – Veci32 { println!(\'{}\’, v[0] + v[1]); //返回v 作为返回所有权的方式//顺便说一下,最后一个您必须在行中使用\’return\’ //由于Rust 是基于表达式的v}//使用借用的显式引用fn print_sum2(vr: Veci32) { println!(\'{} \’, (*vr)[0] + ( *vr)[1]); //引用vr 被删除//因此借用结束}//实际上应该这样做fn print_sum3(v: Veci32) { println!(\'{}\’, v[ 0] + v[1]); //与print_sum2}fn main() { let mut v=Vec:new(); //1.1000 为i 创建一个资源{ v.push(i) ; //此时,v //使用超过4000 字节的内存//将所有权转移给print_sum 并在完成后将其收回v=print_sum1(v); //现在再次获取所有权和控制权。 v println!(\'(1) still v: {}, {},\’, v[0], v[1]); //获取(借用)对v 的引用并将该引用传递给Ill把它给你。 print_sum2 print_sum2(v); //v 仍然完全是我们的println!(\'(2) 仍然是v: {}, {},\’, v[0], v[1]);这里却是一样的print_sum3( v); println!(\'(3) still v: {}, {},\’, v[0], v[1]); //这里v 被删除并释放。让我们看看这里发生了什么。 首先,所有权可以随时转让,但我们认为这不是我们想要的。

第二个更有趣。 获取对向量的引用并将其传递给函数。就像在C 中一样,您显式取消引用它以获取其后面的对象。 由于没有复杂的生命周期信息,因此当引用被删除时,借用就会结束。 与第一个示例类似,但有重要区别。主函数始终负责向量。借用向量时只有一些限制。 在这个例子中,主函数在借用向量时甚至没有机会观察它,所以这没什么大不了的。

第三个功能结合了第一个功能(无需取消引用)和第二个功能(无需混淆所有权)的最佳部分。 这是通过Rust 的自动引用规则实现的。 这些有点复杂,但在大多数情况下,您可以编写代码,就像引用只是您引用的对象一样,类似于C++ 引用。

令人惊讶的是,这是另一个例子:

//通过(不可变)referencefn count_occurrences(v: Veci32, val: i32) 获取v – usize { v.into_iter().filter(|x| x==val).count()}fn main() { let v=vec ![2, 9, 3, 1, 3, 2, 5, 5, 2]; //借用v 来迭代v 中的项{ //第一个借用仍然有效//第二个借用在这里! res=count_occurrences(v, item); println!(\'{} 重复了{} 次\’, item, res); 你不必担心会发生什么。借用向量(同样,无需移动向量)。 该循环还借用了一个向量,因此两个借用同时有效。 循环结束后,主函数删除向量。

(这有点乏味。我提到多线程是引用的主要原因,但我给出的示例都是单线程的。如果你真的感兴趣,请参阅此处和此处的Rust 中的多线程)。

当涉及垃圾收集时,检索和删除引用似乎以相同的方式工作。 不是这种情况。 一切都发生在编译时。 为此,Rust 需要另一个神奇的概念。 请考虑以下示例代码。

fn middle_name(full_name: str) – str { full_name.split_whitespace().nth(1).unwrap()}fn main() { let name=String:from(\’哈利·詹姆斯·波特\’); let res=middle_name(name); (res, \’James\’);} 上面的代码有效,但下面的代码无效。

//这不会编译fn middle_name(full_name: str) – str { full_name.split_whitespace().nth(1).unwrap()}fn main() { let res; { let name=String:from(\’Harry James Potter \’) ; res=middle_name(name); }assert_eq!(res, \’James\’);} 首先,让我们澄清一下关于字符串类型的困惑。 字符串是拥有的字符串缓冲区,&str(字符串切片)是其他人的字符串或其他内存“视图”(这里并不重要)。

为了更清楚地说明这一点,让我们用纯C 语言编写这样的代码:

(不相关的注释:在C 中,您无法“看到”字符串的中间,因为您必须更改字符串以标记它的结尾,因此您只能在那里搜索姓氏。))

#include stdlib.h#include stdio.h#include string.hconst char *last_name(const char *full_name){ return strrchr(full_name, \’ \’) + 1;}int main() { const char *buffer=strcpy(malloc (80), \’哈利·波特\’); const char *res=last_name(buffer); printf(\’%s\\n\’, res); 在使用结果之前,缓冲区被销毁并释放。 这是使用后的简单使用示例。如果printf 实现没有立即将内存用于其他目的,则此C 代码将成功编译并运行。 然而,不那么简单的例子仍然可能导致崩溃、错误和安全漏洞。 这正是我们在介绍所有权之前所说的。

我什至无法用Rust 编译它(我说的是上面的Rust 代码)。 这种静态分析机制内置于语言中并且终身可用。

生命周期

Rust 中的资源有生命周期。 它们从被创造的那一刻起直到被丢弃的那一刻都是活着的。 生命周期通常被认为是范围或块,但正如我们所见,这并不是真正准确的表示,因为资源可以在块之间移动。 您不能引用尚未创建或已删除的对象。我们稍后将解释如何执行此要求。 否则,这都是显而易见的,与所有权的概念没有太大区别。

这是困难的部分。 引用和其他对象也有生命周期,这些生命周期可能与它们所代表的借用的生命周期(称为它们的关联生命周期)不同。

让我改变这一点。 借用的持续时间比它控制的引用的持续时间要长。 通常,这是因为可能有另一个引用借用同一对象或其一部分(例如上例中的字符串切片),具体取决于借用是否处于活动状态。

事实上,每个引用都会记住它所代表的借用的生命周期。也就是说,每个引用都有一个额外的生命周期。与“借用检查”相关的所有内容一样,这是在编译时完成的,并且没有任何运行时开销。 与其他情况不同,在某些情况下您需要明确指定有效期详细信息。

考虑到这一点,让我们深入研究以下内容:

fn middle_name\’a(full_name: \’a str) – \’a str { full_name.split_whitespace().nth(1).unwrap()}fn main() { let name=String:from(\’哈利·詹姆斯·波特\’); );assert_eq!(res, \’James\’); //不会编译: /* let name=String:from(\’Harry James Potter\’) }assert_eq!( res, \’James\’);不需要显式指定生命周期,因为生命周期很短,并且Rust 编译器可以自动确定生命周期(有关更多信息,请参阅省略生命周期)。 无论如何,我在这里这样做是为了向您展示它们是如何工作的。

证明函数在称为a 的生命周期内是通用的。也就是说,对于具有关联生命周期的输入引用,它返回具有相同关联生命周期的另一个引用。 (让我再次提醒您,关联生命周期是指借用生命周期,而不是引用生命周期。)

其实它的含义可能还没有完全清楚,那么我们就从相反的角度来看一下。 返回的引用保存在res 变量中,并且在main() 的整个范围内有效。 这是引用的生命周期,因此借用(其关联的生命周期)至少持续同样长的时间。 这意味着与函数的输入参数关联的生命周期必须相同,因此我们可以得出结论,应该在整个函数中借用名称。 事实正是如此。

在“使用后使用”示例(此处已注释)中, res 在整个函数中持续存在,但名称只是“寿命不长”并且无法借用整个函数。 当我尝试编译这段代码时,我得到了确切的错误。

发生的情况是,Rust 编译器尝试使借用的生命周期尽可能短,最好在引用被删除后立即终止(这类似于“最简单的情况”)。 结果有效性对原始借用生命周期的限制(例如,“这个借用的持续时间与那个借用的持续时间一样长”)导致生命周期越来越长。 一旦满足所有约束,该过程就会停止。如果不满足约束,就会发生错误。

哦,你不能通过说你的函数返回一个具有完全不相关生命周期的借用值来欺骗Rust。因为无关的生命周期可能会大很多倍,导致函数内出现相同的“生命周期不足”错误。比输入的长度。 (这是一个谎言。实际上,错误是不同的,但最好将它们视为相同的错误。)

让我们看这个例子:

fn search\’a, \’b(needle: \’a str, haystack: \’b str) – Option\’b str { //这里想象一个聪明的算法//返回原始字符串的切片let len=need. if haystack.chars().nth(0)==Needle.chars().nth(0) { Some(haystack[.len]) } else if haystack.chars().nth(1) 针。 chars().nth(0) { Some(haystack[1.len+1]) } else { None }}fn main() { let haystack=\’你好小女孩\’;

let needle = String::from(\”ello\”); res = search(&needle, haystack); } match res { Some(x) => println!(\”found {}\”, x), None => println!(\”nothing found\”) } // outputs \”found ello\”}搜索功能接受两个引用,这些引用具有完全不相关的关联生存期。 尽管大海捞针上有一个约束,但我们唯一需要做的就是在函数本身执行时借用必须有效。 完成后,借用立即结束,我们可以安全地重新分配关联的内存,同时仍保持函数结果不变。
haystack用字符串文字初始化。 这些是类型为&\’static str的字符串片段-始终是\”活动\”的\”借阅\”。 因此,我们能够在需要时保持res变量不变。 这是例外,因为借款可以持续的时间尽可能短。 您可以将其视为对\”借用的字符串\”的另一个限制-字符串文字借用必须在程序的整个执行时间中持续存在。
最后,我们返回的不是引用本身,而是一个内部对象的复合对象。 这是完全支持的,不会影响我们的生命周期的逻辑。
因此,在此示例中,该函数接受两个参数,并且在两个生存期内都是通用的。 让我们看看如果我们强制生命周期相同会发生什么:
fn the_longest<\’a>(s1: &\’a str, s2: &\’a str) -> &\’a str { if s1.len() > s2.len() { s1 } else { s2 }}fn main() { let s1 = String::from(\”Python\”); // explicitly borrowing to ensure that // the borrow lasts longer than s2 exists let s1_b = &s1; { let s2 = String::from(\”C\”); let res = the_longest(s1_b, &s2); println!(\”{} is the longest if you judge by name\”, res); }}我在内部代码块之外进行了明确的借用,因此借用必须持续到main()的其余部分。 这显然不同于&s2。 如果只接受两个具有相同关联生存期的参数,为什么可以调用该函数?
事实证明,关联的生命周期是强制类型的主题。 与大多数语言(至少是我所熟知的语言)不同,Rust中的原始(整数)值不会强制转换-您必须始终将其强制转换。 您仍然可以在一些不太明显的地方找到强制,例如这些关联的生存期和带有类型擦除的动态调度。
我将把这段C ++代码进行比较:
struct A { int x;};struct B: A { int y;};struct C: B { int z;};B func(B arg){ return arg;}int main() { A a; B b; /* this works fine: * a B value is a valid A value * to put it another way, you can use a B value * whenever an A value is expected */ a = b; /* on the other hand, * this would be an error: */ // b = a; // this works just fine C arg; A res = func(arg); return 0;}派生类型强制为其基本类型。 当我们传递C的实例时,它强制转换为B,然后返回,强制转换为A,然后存储在res变量中。
同样,在Rust中,更长的借用可能会变短。 它不会影响借用本身,而只会在需要较短借入的地方被接受。 因此,您可以为函数传递寿命比预期更长的借用(它将被强制执行),并且可以强制将借还的返回的时间更短。
再考虑一下这个示例:
fn middle_name<\’a>(full_name: &\’a str) -> &\’a str { full_name.split_whitespace().nth(1).unwrap()}fn main() { let name = String::from(\”Harry James Potter\”); let res = middle_name(&name); assert_eq!(res, \”James\”); // won\’t compile: /* let res; { let name = String::from(\”Harry James Potter\”); res = middle_name(&name); } assert_eq!(res, \”James\”); */}人们通常会想知道这样的函数声明是否意味着参数的关联生存期必须(至少)与返回值一样长,反之亦然。
答案现在应该很明显。 在功能上,两个寿命完全相同。 但是由于强制,您可以通过较长的借用时间,甚至有可能在获得结果后缩短结果的关联寿命。 因此正确的答案是-参数必须至少与返回值一样长。
而且,如果您创建一个通过引用接受多个参数的函数,并声明它们必须具有相等的关联生命周期(如在我们之前的示例中一样),则将给出该函数的实际参数将被强制为其中最短的生命周期。 这只是意味着结果不能超过借用的任何参数。
这与我们之前讨论的反向约束规则很好地配合。 被呼叫者不在乎-它只是获得并返回相同生命周期的借贷。 另一方面,调用者确保参数的关联生存期永远不会比结果的生存期短,可以通过扩展它们来实现。

随机附加说明

· 您不能移出借用的值,因为借用结束后该值必须保持有效。 即使您在下一行移回某些内容,也无法将其移出。 但是有mem :: replace可以让您同时执行两项操作。
· 如果您要拥有一个指针(类似于C ++中的unique_ptr),则可以使用Box类型。
· 如果您希望进行一些基本的引用计数(例如C ++中的shared_ptr和weak_ptr),则可以使用此标准模块。
· 如果您确实确实需要解决Rust所施加的限制,则始终可以使用不安全的代码。
(本文翻译自Sergey Bugaev的文章《Understanding Rust: ownership, borrowing, lifetimes》,参考:https://medium.com/@bugaevc/understanding-rust-ownership-borrowing-lifetimes-ff9ee9f79a9c)

原创文章,作者:小条,如若转载,请注明出处:https://www.sudun.com/ask/85115.html

(0)
小条's avatar小条
上一篇 2024年6月1日 上午11:01
下一篇 2024年6月1日 上午11:09

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注