English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Rust 所有权

Computerprogramme müssen während der Laufzeit die von ihnen verwendeten Speicherressourcen verwalten.

Die meisten Programmiersprachen haben die Funktion, Speicherressourcen zu verwalten:

C/C++ Solche Sprachen verwalten das Speicherressourcen hauptsächlich manuell, und Entwickler müssen die Speicherressourcen manuell anfordern und freigeben. Aber um die Entwicklungsleistung zu verbessern, haben viele Entwickler die Gewohnheit, nicht rechtzeitig Speicherressourcen freizugeben, solange es die Funktion des Programms nicht beeinträchtigt. Daher führt die manuelle Verwaltung von Speicherressourcen oft zu Ressourcenverschwendung.

Programme, die in der virtuellen Maschine (JVM) in Java-Sprache geschrieben sind, laufen darin und die JVM verfügt über die Funktion, den Speicherressourcen automatisch zurückzugeben. Aber diese Methode kann oft die Effizienz der Laufzeit verringern, daher versucht die JVM, so wenig Ressourcen wie möglich zurückzugeben, was auch dazu führt, dass das Programm mehr Speicherressourcen belegt.

Das Konzept des Eigentums ist für die meisten Entwickler ein neues Konzept, es ist ein syntaktisches Mechanismus, den Rust für die effiziente Nutzung von Speicher entwickelt hat. Das Konzept des Eigentums wurde geschaffen, um Rust in der Kompilierungsphase effizienter die Nützlichkeit der Speicherressourcen zu analysieren, um eine effektive Speicherverwaltung zu erreichen.

Eigentumsregeln

Eigentum hat folgende drei Regeln:

  • In Rust hat jeder Wert eine Variable, die als ihr Eigentümer bezeichnet wird.

  • Es kann nur ein Eigentümer gleichzeitig geben.

  • Wenn der Eigentümer nicht im Lauf der Programmabwicklung im Bereich ist, wird der Wert gelöscht.

Diese drei Regeln sind die Grundlage des Konzepts des Eigentums.

Nachfolgend werden Konzepte vorgestellt, die mit dem Konzept des Eigentums zusammenhängen.

Bereich der Variablen

Wir beschreiben das Konzept des Bereichs der Variablen mit folgendem Programm:

{
    // Vor der Deklaration ist die Variable s ungültig
    let s = "w3codebox";
    // Hier ist der gültige Bereich der Variable s
}
// Der Bereich der Variablen ist abgeschlossen, die Variable s ist ungültig

Der Bereich der Variablen ist eine Eigenschaft der Variablen, die den gültigen Bereich der Variablen darstellt, der standardmäßig von der Deklaration der Variablen bis zum Ende des Bereichs der Variablen gültig ist.

Speicher und Verteilung

Wenn wir eine Variable definieren und ihr einen Wert zuweisen, liegt der Wert dieser Variable im Speicher. Dies ist ein sehr häufiges Szenario. Wenn jedoch die Länge der zu speichernden Daten unbestimmt ist (z.B. eine Zeichenkette, die von Benutzern eingegeben wird), können wir die Länge der Daten nicht bei der Definition angeben und daher auch nicht im Kompilationsstadium eine festgelegte Länge des Speicherplatzes für die Datenverarbeitung zuweisen. (Manche sagen, dass man den größten möglichen Raum zuweisen kann, um das Problem zu lösen, aber dieser Ansatz ist sehr unzivilisiert). Dies erfordert eine Mechanik, die es dem Programm ermöglicht, im Lauf der Ausführung selbst Speicher zu beantragen - den Heap. Alle in diesem Kapitel erwähnten "Speicherressourcen" beziehen sich auf den Speicherplatz, den der Heap belegt.

Es gibt Verteilung und Freigabe, das Programm kann nicht unendlich eine bestimmte Speicherressource belegen. Daher ist der Schlüsselfaktor, ob Ressourcen verschwendet werden, ob sie rechtzeitig freigegeben werden.

Wir schreiben das Beispielprogramm für die Zeichenkette in einer Sprache wie C äquivalent:

{
    char *s = "w3codebox";
    free(s); // Ressourcenfreigabe von s
}

Es ist offensichtlich, dass in Rust keine free-Funktion verwendet wird, um die Ressourcen der Zeichenkette s freizugeben (ich weiß, dass dies in der Sprache C eine falsche Schreibweise ist, weil "w3Der "codebox" ist nicht im Heap, hier wird angenommen, dass er in () ist). Der Grund, warum Rust die Schritte zur Freigabe nicht explizit angibt, liegt darin, dass der Rust-Compiler automatisch die Schritte zur Freigabe der Ressourcen hinzufügt, wenn der Bereich der Variablen endet.

Diese Mechanik scheint einfach zu sein: Es handelt sich um nichts anderes als um eine Funktion, die Programmierern hilft, an geeigneter Stelle eine Ressourcenfreigabeaufruf hinzuzufügen. Dieses einfache Mechanismus kann jedoch effektiv ein historisch bekanntes Problem lösen, das Programmierern Kopfzerbrechen bereitet.

变量与数据交互的方式

变量与数据交互方式主要有移动(Move)和克隆(Clone)两种:

移动

多个变量可以在 Rust 中以不同的方式与相同的数据交互:

let x = 5;
let y = x;

这个程序将值 5 绑定到变量 x,然后将 x 的值复制并赋值给变量 y。现在栈中将有两个值 5。此情况中的数据是"基本数据"类型的数据,不需要存储到堆中,仅在栈中的数据的"移动"方式是直接复制,这不会花费更长的时间或更多的存储空间。"基本数据"类型有这些:

  • 所有整数类型,例如 i32 、 u32 、 i64 等。

  • 布尔类型 bool,值为 true 或 false 。

  • 所有浮点类型,f32 和 f64。

  • 字符类型 char。

  • 仅包含以上类型数据的元组(Tuples)。

但如果发生交互的数据在堆中就是另外一种情况:

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

第一步产生一个 String 对象,值为 "hello"。其中  "hello" 可以认为是类似于长度不确定的数据,需要在堆中存储。

第二步的情况略有不同(这不是完全真的,仅用来对比参考):

如图所示:两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 "hello" 字符串。在给 s2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。

前面我们说过,当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 s1 und s2 都被释放的话堆区中的 "hello" 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。没错,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用。下面这段程序是错的:

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // 错误!s1 已经失效

所以实际情况是:

s1 名存实亡。

克隆

Rust会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。但如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式——克隆。

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

Laufender Output:

s1 = hello, s2 = hello

这里是真的将堆中的 "hello" 复制了一份,所以 s1 und s2 jeweils einen Wert gebunden und werden bei der Freigabe auch als zwei Ressourcen behandelt.

Natürlich wird das Klonen nur dann verwendet, wenn eine Kopie erforderlich ist, schließlich kostet das Kopieren mehr Zeit.

Beteiligt an den Eigentumsrechten der Funktion

Für Variablen ist dies die komplexeste Situation.

Wie kann man das Eigentumsrecht sicher verwalten, wenn eine Variable als Parameter an eine andere Funktion übergeben wird?

Das folgende Programm beschreibt den Mechanismus der Eigentumsrechte in dieser Situation:

fn main() {
    let s = String::from("hello");
    // s wird gültig erklärt
    takes_ownership(s);
    // Wert von s wird als Parameter in die Funktion übergeben
    // Daher kann es als, dass s bereits移动 wurde, ab hier ist es ungültig
    let x = 5;
    // x wird gültig erklärt
    makes_copy(x);
    // Wert von x wird als Parameter in die Funktion übergeben
    // Aber x ist ein Basistyp, daher gültig
    // Hier kann x immer noch verwendet werden, aber s nicht
} // Funktion endet, x ist ungültig, dann ist s. Aber s wurde jedoch移动, daher muss es nicht freigegeben werden
fn takes_ownership(some_string: String) { 
    // Ein String-Parameter some_string wird übergeben, gültig
    println!("{}", some_string);
} // Funktion endet, Parameter some_string wird hier freigegeben
fn makes_copy(some_integer: i32) { 
    // Ein i32 Parameter some_integer wird übergeben, gültig
    println!("{}", some_integer);
} // Funktion endet, Parameter some_integer ist ein Basistyp, muss nicht freigegeben werden

Wenn eine Variable als Parameter in eine Funktion übergeben wird, ist der Effekt gleich dem einer Bewegung.

Mechanismus der Eigentumsrechte der Rückgabewerte der Funktion

fn main() {
    let s1 = gives_ownership();
    // gives_ownership移动es seinen Rückgabewert nach s1
    let s2 = String::from("hello");
    // s2 Wird gültig erklärt
    let s3 = takes_and_gives_back(s2);
    // s2 Wird als Parameter移动, s3 Eigentumsrecht des Rückgabewerts erhalten
} // s3 Ungültiges wird freigegeben, s2 Wird移动, s1 Ungültiges wird freigegeben.
fn gives_ownership() -> String {
    let some_string = String::from("hello");
    // some_string wird gültig erklärt
    return some_string;
    // some_string wird als Rückgabewert aus der Funktion herausge移动
}
fn takes_and_gives_back(a_string: String) -> String { 
    // a_string wird gültig erklärt
    a_string  // a_string wird als Rückgabewert aus der Funktion herausgegeben
}

Das Eigentum des als Rückgabewert eines Funktionen übergebenen Variablen wird aus der Funktion herausgeht und an den Ort der aufrufenden Funktion zurückgegeben, anstatt direkt ungültig gelöst zu werden.

Referenz und Pacht

Referenz (Reference) ist C++ ein für Entwickler bekanntes Konzept.

Wenn du das Konzept von Zeigern kennst, kannst du es als einen Zeiger betrachten.

Substanzmäßig ist "Referenz" eine indirekte Art des Zugriffs auf Variablen.

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    println!("s1 ist {}, s2 ist {}", s1, s2);
}

Laufender Output:

s1 ist hello, s2 ist hello

Der &-Operator kann die "Referenz" eines Variablen abrufen.

Wenn der Wert einer Variable referenziert wird, wird die Variable selbst nicht als ungültig erachtet. Denn "Referenz" kopiert den Wert der Variable nicht im Stack:

Das Prinzip der Übermittlung von Funktionsparameter ist gleich:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("Die Länge von '{}' ist {}.", s1, len);
}
fn calculate_length(s: &String, len); -> usize {
    s.len()
}

Laufender Output:

Die Länge von 'hello' ist 5.

Eine Referenz erhält kein Eigentum am Wert.

kann nur den Eigentum des Werts pachten (Borrow).

sich selbst auch ein Typ ist und einen Wert hat, der die Position anderer Werte aufzeichnet, aber die Referenz hat kein Eigentum an dem所指Wert:

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = s1;
    println!("{}", s)2);
}

Dieses Programm ist falsch, weil s2 gepachtete s1 hat das Eigentum bereits zu s verschoben3, daher s2 kannst du den Pachtgebrauch von s nicht fortsetzen1 das Eigentum. Wenn du s verwenden musst2 Um diesen Wert zu verwenden, muss neu gepachtet werden:

fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1;
    let s3 = s2;
    s2 = &s3; // wieder von s3 Pacht-Eigentum
    println!("{}", s)2);
}

Dieses Programm ist korrekt.

Da eine Referenz kein Eigentum hat, auch wenn sie das Eigentum pachtet, besitzt sie nur das Nutzungsrecht (das ist wie ein gemietetes Haus).

Wenn versucht wird, Rechte aus der Pacht zu nutzen, um Daten zu ändern, wird dies verhindert:

fn main() {
    let s1 = String::from("run");
    let s2 = &s1; 
    println!("{}", s)2);
    s2.push_str("oob"); // Fehler, Änderung des Pachtwerts verboten
    println!("{}", s)2);
}

in diesem Programm s2 Versuch, s zu ändern1 wird verhindert, dass der Pächter den Wert des Eigentümers ändern kann.

Natürlich, es gibt auch eine veränderliche Pachtart, wie wenn du ein Haus mietest und der Eigentümer dir erlaubt, die Struktur des Hauses zu ändern, und er erklärt dies in deinem Mietvertrag, dass du das Haus neu einrichten darfst:

fn main() {
    let mut s1 = String::from("run");
    // s1 ist veränderlich
    let s2 = &mut s1;
    // s2 ist eine veränderliche Referenz
    s2.push_str("oob");
    println!("{}", s)2);
}

这段程序就没有问题了。我们用 &mut 修饰可变的引用类型。

与不可变引用相比,可变引用除了权限不同以外,不允许多重引用,但不可变引用可以:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);

这段程序不正确,因为它多重可变引用了 s。

Rust 对可变引用的这种设计主要出于对并发状态下发生数据访问冲突的考虑,在编译阶段就避免了这种事情的发生。

由于发生数据访问冲突的必要条件之一是数据被至少一个使用者写且同时被至少一个其他使用者读或写,所以在一个值被可变引用时不允许再次被任何引用。

悬垂引用(Dangling References)

这是一个换了个名字的概念,如果放在有指针概念的编程语言里它就指的是那种没有实际指向一个真正能访问的数据的指针(注意,不一定是空指针,还有可能是已经释放的资源)。它们就像失去悬挂物体的绳子,所以叫"悬垂引用"。

在 Rust 语言中不允许出现"悬垂引用",如果存在,编译器会发现它。

下面是一个悬垂引用的典型案例:

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

显然,随着 dangle 函数的结束,其局部变量的值本身没有被作为返回值,被释放了。但它的引用却被返回,这个引用所指向的值已经不能确定的存在,因此不允许其出现。