基于 C++ 的 Rust 教程(二):基本所有权系统

· · 科技·工程

:::epigraph[——Rust] 一门赋予每个人 \ 构建可靠且高效软件能力的语言。 :::

这是一篇帮助你在有 C++ 基础的情况下学会 Rust 基本使用的教程。

本文是此系列的第二篇

第一篇回顾

在第一篇,我们学习了:Rust 基本信息、基本程序框架、数据与变量。

本篇内容

在这一篇,你将学习到:基本所有权系统。

所有权系统初步

字符串类型

Rust 中有两种字符串类型:&strString。这两者分别类似 C++ 中的原始字符串类型 const char* 和标准库中的 std::string

字符串字面量是 &str 类型的。

let string = "Hello!";
println!("{type_name}", type_name = get_type_name(&string));  // &str

字符串字面量的用法与 C++ 基本一致,包括各种转义字符。请注意,不在字符串格式化的上下文中的字符串字面量是没有格式化功能的:

let var = 1;
let string = "{{var}} = {var}";
println!("{string}");  // {{var}} = {var}

println!print! 宏就是字符串格式化上下文的一个例子。此外,你还可以使用 format! 宏来格式化字符串。这个宏返回一个 String 类型的值。

let var = 1;
let string = format!("{{var}} = {var}");
println!("type(string) = {type_name}, string = {string:?}", type_name = get_type_name(&string));
// 输出:
// type(string) = alloc::string::String, string = "{var} = 1"

:::info[提示:那个问号] 在上面的格式化字符串中,有一个 {:?} 的写法。这个表示以调试信息格式化。对于字符串,这个的功能就是以字符串在代码中的原始形式(带引号和转义符)格式化。 :::

其中 alloc::string::StringString 类型的全称。关于字符串格式化的内容,我们后面会讲解。回到两种字符串类型上。

&str 类型是不可变的,那么你将其声明为 mut 变量其实也只是能重赋值而已。如果你想要可变的字符串,像 C++ 的 std::string 一样,可以使用 String 类型。

你可以用 String::new 方法创建一个新的空字符串。

let string = String::new();
println!("string = {string:?}");  // string = ""

你还可以用 String::from 方法或 str::to_string 方法来从一个 &str 类型的字符串创建一个 String 类型的字符串。

let str1 = String::from("Hello,");
let str2 = "World!".to_string();
println!("{str1} {str2}");  // Hello, World!

:::info[疑问]{open} 上面我说的是 str::to_string 方法而不是 &str::to_string 方法。难道 &str 不是一个类型吗?

是,但不完全是。后面你就会知道我为什么要这么写。 :::

你可以使用 String::as_str() 来将一个 String 类型的字符串转换为 &str 类型:

let string = String::from("Hi!");
let another = string.as_str();
println!("{type_name}", type_name = get_type_name(&another));  // &str

当然,在某些情况下,给 String 类型的值前面使用借用运算符 &(我们马上讲)可以将其转换为 &str 类型。当然,这个“某些情况下”是编译器推断出当前上下文需要 &str 这个类型,否则编译器会把其认为是 &String 类型。

let string = String::from("Hi!");
let a = &string;
let b: &str = &string;
println!("type(a) = {type_name}", type_name = get_type_name(&a));  // type(a) = &alloc::string::String
println!("type(b) = {type_name}", type_name = get_type_name(&b));  // type(b) = &str

:::info[疑问]{open} &String 是什么类型?别急,我们马上就会见到它。 :::

一个 String 后面可以与一个 &str 类型的字符串进行拼接,并返回一个新的 String。这里由于是 String 重载了操作符(事实上是实现了一个叫 Add 的特征),所以不会被“运算符两端类型必须一致”所限制。此外,这种做法能避免很多问题,我们马上就会知道为什么。

let string = String::from("Hi!");
let another = String::from("Friend!");
let a = string + " " + &another;
// 这里编译器知道需要一个 &str 值,所以 &string 得到的是一个 &str 类型而不是 &String
println!("{a}");  // Hi! Friend!

但是这种字符串拼接方式不支持拼接字符(char)类型。你可以使用 char::to_string 方法先将其转换为 String 类型,再通过 String::as_str& 运算符得到 &str 类型。

let string = String::from("Hi");
let a = string + &'!'.to_string();
// 方法调用的优先级高于 & 运算符,所以这里是 &('!'.to_string()) 而不是 (&'!').to_string()
println!("{a}");  // Hi!

我们可以使用自增运算符 += 来进行自拼接。不过要注意,由于需要改变原值,我们需要可变变量。

let mut string = String::from("Hi");
string += ", friend!";
println!("{string}");  // Hi, friend!

String 类型重载了 += 运算符(事实上是实现了 AddAssign 特征)。上面的代码其实相当于 String::push_str 方法。

let mut string = String::from("Hi");
string.push_str(", friend!");
println!("{string}");  // Hi, friend!

此外,可以使用 String::push 方法来自拼接一个字符:

let mut string = String::from("Hi");
string.push('!');
println!("{string}");  // Hi!

有关字符串的更多操作,可以自行搜索。

神秘的问题

接下来,我们尝试编写了以下代码:

fn main() {
    let a = 1;
    let b = a;
    println!("a = {a}, b = {b}");

    let a_str = String::from("Hi!");
    let b_str = a_str;
    println!("a_str = {a_str:?}, b_str = {b_str:?}");
}

恭喜你,成功地报错啦!

:::error[报错信息]

error[E0382]: borrow of moved value: `a_str`
 --> main.rs:8:24
  |
6 |     let a_str = String::from("Hi!");
  |         ----- move occurs because `a_str` has type `String`, which does not implement the `Copy` trait
7 |     let b_str = a_str;
  |                 ----- value moved here
8 |     println!("a_str = {a_str:?}, b_str = {b_str:?}");
  |                        ^^^^^ 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)
help: consider cloning the value if the performance cost is acceptable
  |
7 |     let b_str = a_str.clone();
  |                      ++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0382`.

:::

报错信息指出,我们不能借用一个已经被移动的值。

但是如果我们将代码写成这样呢?

fn main() {
    let a = 1;
    let b = a;
    println!("a = {}, b = {}", a, b);

    let a_str = "Hi!";
    let b_str = a_str;
    println!("a_str = {a_str:?}, b_str = {b_str:?}");
}

代码神奇地通过了!

你也许正对这些代码感到疑惑,毕竟下面的代码在 C++ 中是完全能够通过的:

const std::string a_str = "Hi!";
const std::string b_str = a_str;
std::println("a_str = \"{}\", b_str = \"{}\"", a_str, b_str);

在这里,我想要祝贺你:学了这么久,你终于体验到 Rust 的所有权系统啦!上面的代码与 Rust 的所有权系统有直接的关系,我们也终于能开始学习这 Rust 中“最难啃的骨头”了!

所有权转移

上面的这段代码:

let a_str = String::from("Hi!");
let b_str = a_str;

实际上执行了移动的操作。也就是说,a_str 变量主动将其对数据 String::from("Hi!") 的所有权转移给了 b_str

那么现在,b_str 拥有对于该字符串的所有权,但是 a_str 没有了。所以,现在 a_str无效的

为什么对于整数,我们这样写不会转移所有权呢?

let a = 1;
let b = a;

因为 Rust 中所有的基本类型都是实现了 Copy 特征的。这个特性更改赋值时的默认行为:复制而不是转移所有权。这样,即便写出上面的代码,由于 i32 是基本类型,实现了 Copy 特征,它在赋值给另一个变量时,会复制一遍数据。

基本类型自动实现 Copy 特征是一个明智的选择。基本类型的复制几乎没有性能损耗,在计算机底层上都是一条指令的事情。如果连基本类型都要遵守严格的所有权系统,Rust 将会变得非常难用。

然而,字符串这类比较复杂的类型,复制的开销会非常大。此时,我们就需要遵循所有权系统来保障安全与效率。

字符串 String 类型没有实现 Copy 特征,但实现了 Clone 特征。你可以手动复制:

let a_str = String::from("Hi!");
let b_str = a_str.clone();

:::info[提示:关于 CopyClone 特征] Copy 特征是隐式的复制,覆盖掉了默认转移所有权的行为。而 Clone 特征是显式的复制,默认的所有权转移的行为依旧存在,但你可以手动调用 .clone() 方法来复制数据。 :::

现在,我们还有一个问题:为什么下面的代码也是字符串,但好像“没有转移所有权”?

let a_str = "Hi!";
let b_str = a_str;
// 现在两者均有效

回想一下,字符串字面量的类型是 &str,也就是 Rust 中最“原始”的字符串。当时我们还留了一个问题:& 是什么意思?

下一节,我们来解答这些问题。

不可变借用

根据我们一开头就讲述的所有权系统基本规则,一个没有数据所有权的变量想要使用数据,必须获取借用。借用分为可变借用和不可变借用。我们先看不可变借用。

借用的语法类似于 C++ 中的引用和指针。请看:

let a = 1;
let b = &a;

此处,表示 b 获取了 a 的数据的借用。这样,b 的生命周期要小于等于 a 的生命周期,借用才有效。当然,在我们这个例子中,它肯定是有效的。

b 的类型是什么呢?我们来看一下:

let a = 1;
let b = &a;
println!("{type_name}", type_name = get_type_name(&b));

输出是:

&i32

这说明什么?说明在 Rust 中,借用实际上是一种类型。有点类似于 C++ 中的引用或者指针,需要把相关的符号写到类型名称里。

你还可以借用一个借用:

let a = 1;
let b = &a;
let c = &b;
println!("{type_name}", type_name = get_type_name(&c));  // &&i32

此外,你可以在声明变量的使用使用 ref 关键字来声明一个借用(虽然它是引用 Reference 的意思):

let a = 1;
let ref b = a;
println!("{type_name}", type_name = get_type_name(&b));  // &i32

:::info[回答上面的疑问:&String 类型] 现在我们知道了,&String 类型与 &i32 类型类似。&i32i32 的借用类型,那么 &String 就是 String 的借用类型。 :::

我们可以像普通变量一样使用借用的变量:

let a = 1;
let b = &a;
println!("{b}");  // 1

与 C/C++ 的指针类似,我们可以使用星号 * 来解引用(实际上应叫做“解借用”,但是“解引用”更顺口,大多教程里也这么叫,Rust 语言中也用 Deref 特征来实现这个操作符)。解引用之后,如果原值实现了 Copy 特征,则会自动复制。如果没有,就会报错:你是借用的别人的东西,你无权转让其所有权。例子:

// 有 `Copy` 特征的例子
let a = 1;
let b = &a;
let c = *b;
println!("type(c) = {type_name}, c = {c}", type_name = get_type_name(&c));
// 输出:
// type(c) = i32, c = 1
// 无 `Copy` 特征的例子
let a = String::from("Hello!");
let b = &a;
let c = *b;  // Error!
println!("type(c) = {type_name}, c = {c:?}", type_name = get_type_name(&c));

:::error[报错信息]

error[E0507]: cannot move out of `*b` which is behind a shared reference
 --> src/main.rs:8:13
  |
8 |     let c = *b;
  |             ^^ move occurs because `*b` has type `String`, which does not implement the `Copy` trait
  |
help: consider removing the dereference here
  |
8 -     let c = *b;
8 +     let c = b;
  |
help: consider cloning the value if the performance cost is acceptable
  |
8 -     let c = *b;
8 +     let c = b.clone();
  |

:::

:::info[思考题:借用类型与 Copy 特征] 我们说过,实现了 Copy 特征的数据在普通的赋值时是自动复制的。现在,在 Rust 中,有这样一条规则:

所有借用类型自动实现 Copy 特征。

现在请尝试解释这句话的含义,并推断下面的程序是否能通过编译。若不能,请给出可能的报错原因;若能,请给出程序的输出。关键的代码行已经高亮,省略了主函数与 get_type_name 函数的定义:

let a = 1;
let b = &&a;  // 取两次借用,即借用的借用
let c = b;
let d = *b;
println!("type(c) = {type_name}, c = {c:?}", type_name = get_type_name(&c));
println!("type(d) = {type_name}, d = {d:?}", type_name = get_type_name(&d));

随后,在计算机上(或在线编辑器上)编译并运行,看看和你的预期是否相同。

现在还无法解释这道题是没关系的,等你学习更多的 Rust 知识后,慢慢培养起了 Rust 编程的思维习惯,自然就能在某一时刻豁然开朗了。 :::

不可变借用是不能够修改数据的。尝试运行以下代码:

let a = 1;
let b = &a;
*b = 2;  // Error!

:::error[报错信息]

error[E0594]: cannot assign to `*b`, which is behind a `&` reference
 --> src/main.rs:4:1
  |
4 | *b = 2;  // Error!
  | ^^^^^^ `b` is a `&` reference, so the data it refers to cannot be written
  |
help: consider changing this to be a mutable reference
  |
3 | let b = &mut a;
  |          +++

For more information about this error, try `rustc --explain E0594`.

:::

想要通过借用更改原数据,你需要使用可变借用。

可变借用

既然有不可变借用,那么一定也有可变借用。一个数据的可变借用可以更改原数据。可变借用和可变变量类似,只需要一个 mut 关键字即可。

let mut a = 1;
let b = &mut a;

:::warning[注意!]{open} 获取一个数据的可变借用,首先要原数据的所有者是可变的!

可以想一想,如果所有者都不希望数据改变,它怎么会借给别人让别人去修改数据呢? :::

在这段代码中,所有者 a 给了 b 一个可变借用。

类似上面我们使用 ref 来定义不可变借用变量一样,我们可以这样定义可变借用:

let mut a = 1;
let ref mut b = a;

需要注意的是,ref 必须在 mut 之前,就像 & 符号必须在 mut 之前一样。

使用可变借用,就可以让你通过借用来修改值:

let mut a = 1;
let b = &mut a;
*b = 3;
println!("{a}");  // 3

所有权系统安全

我们之前说过,Rust 之所以能够在编译期找到安全问题,就是依靠独有的所有权系统。那么,所有权系统必须有一些规则来保障安全。下面,我们就来学习一下这些规则。

作用域与生命周期

首先从生命周期开始。Rust 语言与其他语言有一点不同,生命周期和作用域并不是相同的:

事实上生命周期还有“实际生命周期”(或“存活时间”)和“预期生命周期”两种,我们后文讲的都是第一种。第二种需要使用生命周期标注 'a 来实现。例如,'static 标注表示静态生命周期,是整个程序中最大的生命周期,表示从程序开始运行到结束的整个过程。Rust 要求数据的实际生命周期不小于预期生命周期。悄悄告诉你,字符串字面量的生命周期就是静态生命周期哦。

Rust 官方对生命周期的定义是:引用(借用)的有效范围。不过这个定义有点过于抽象了,不太适合初学者。因此作者重新给其下了一个至少在本文中适用的定义。

可以发现,作用域和生命周期的不同,根本来自于 Rust 的所有权系统是要区分变量和数据的。

Rust 的作用域和 C++ 的基本一致,因此我们重点介绍 Rust 的作用域。

:::info[提示:文字描述]{open} 生命周期的完整表述应该是:“变量 var 所有或借用的数据的生命周期”,但后文为了行文方便,简写为“变量 var 的生命周期”。

在你日后使用 Rust 时,直接写“某个变量的生命周期”是不影响他人理解的,但是你必须记住是数据拥有生命周期。这点很重要,因为它和所有权系统概念的理解相挂钩。 :::

我们先来看一个例子:

fn main() {
    // a b c 的作用域开始
    let a = 1;  // a 生命周期开始
    let b = 2;  // b 生命周期开始
    println!("{a}");  // 最后一次使用 a,a 生命周期结束
    let c = 3 + b;  // c 生命周期开始
    println!("{c}");  // 最后一次使用 c,c 生命周期结束
    println!("{b}");  // 最后一次使用 b,b 生命周期结束
    // a b c 的作用域结束
}

或者,这么写会更清晰一些(但是也更麻烦):

:::info[提示:等宽字体对齐] 不是所有的等宽字体都支持中文。为了保证各个等宽字体显示出来的对齐效果相同,我们使用英语。请你熟悉:

fn main() {             // ==== a b c Scope ============+
                        //                              |
    let a = 1;          // ---- a Lifetime -----+       |
                        //                      |       |
    let b = 2;          // ---- b Lifetime -----|---+   |
                        //                      |   |   |
    println!("{a}");    // ---------------------+   |   |
                        //                          |   |
    let c = 3 + b;      // ---- c Lifetime -----+   |   |
                        //                      |   |   |
    println!("{c}");    // ---------------------+   |   |
                        //                          |   |
    println!("{b}");    // -------------------------+   |
                        //                              |
}                       // =============================+

但是这种方法需要花费很大的功夫,因此后文中,只有少数难以用文字直观说明的地方,才会使用这种图示的形式。

:::info[提示:连写等宽字体] 上面的代码用带有连写特性的等宽字体(如 Cascadia Code、JetBrains Mono、Fira Code 等)会看起来更舒畅。例如,这是作者使用 Maple Mono Normal NF CN 字体在 Zed 代码编辑器中的显示效果:

:::

了解作用域和生命周期之后,我们就可以来逐步分析一下 Rust 所有权系统的规则了。

同名变量

在 Rust 中,是允许在一个作用域中重复声明同名变量的:

let a = 1;
println!("{a}");
let a = "Hello!";
println!("{a}");

这只是给变量 a 绑定了两次数据而已。变量 a 的作用域不变,但是两次绑定的数据的生命周期是不交叉的——当第二次声明变量 a 的时候,原来的整型数据 1 也就到达生命周期的末尾了,而字符串数据的生命周期又开始了。

超出数据生命周期的借用

在 Rust 中,不允许在数据的生命周期之外进行借用,就好像你不能借用一个已经过期的东西。例如,下面的代码是会报错的:

let b;
{
    let a = 1;
    b = &a;
}
println!("{b}");  // Error

:::error[报错信息]

error[E0597]: `a` does not live long enough
 --> main.rs:5:13
  |
4 |         let a = 1;
  |             - binding `a` declared here
5 |         b = &a;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `a` dropped here while still borrowed
7 |     println!("{b}");
  |                - borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0597`.

:::

因为数据 1 的生命周期最大与变量 a 的作用域相同。因此,当 a 的作用域结束时,数据 1 的生命周期也会结束,于是数据 1 就被释放掉了(Drop)。报错信息也很形象,说“借用的值活得不够久”。当然,如果你不在生命周期结束后使用借用 b,那编译器也懒得管你。

同一个生命周期中的多个借用

如果在数据的生命周期中出现多个借用,那这些借用应该满足怎样的规则呢?

首先看第一条:所有借用不得超出数据的生命周期。这个在上面已经描述过了。

第二条:一个数据在其生命周期内允许同时存在多个不可变借用

这个“同时”是什么意思呢?指的是生命周期有重合部分。借用也是一种数据,因此借用也是有生命周期的,而且编译器保证借用的生命周期一定要小于等于其所借用的数据的生命周期

从物质上看,好像不太能理解为什么允许同时存在多个不可变借用(就像两个人不能同时借同一块橡皮)。但放在计算机中,如果一个数据固定不变,同时有多个地方读取数据其实是安全的。

下面是一个例子:

let number = 1;
let a = &number;
let b = &number;
println!("{a} {b}");  // 1 1

上面的代码的第 2 行和第 3 行分别创建了一个不可变借用。第 4 行是体现同时的地方——在同一时刻使用,那么借用的生命周期必然有重叠。

接下来看第三条:一个数据在其生命周期内不能同时存在多个可变借用

这个应该很好理解:如果有多个地方同时修改一个数据,那不就乱套了吗?

当然,“同时”的意思是生命周期有重合。例如,下面的代码会报错,因为两个可变借用的生命周期有重叠:

let mut number = 1;
let a = &mut number;  // ---- a Lifetime -------+
let b = &mut number;  // ---- b Lifetime ----+  |
*a = 2;               // --------------------|--+
*b = 3;               // --------------------+

:::error[报错信息]

error[E0499]: cannot borrow `number` as mutable more than once at a time
 --> main.rs:4:13
  |
3 |     let a = &mut number;
  |             ----------- first mutable borrow occurs here
4 |     let b = &mut number;
  |             ^^^^^^^^^^^ second mutable borrow occurs here
5 |     *a = 2;
  |     ------ first borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0499`.

:::

最后两行就是两个可变借用的生命周期重叠的地方。

当然,如果两个可变借用的生命周期不交叉,就不满足“同时”的条件,那么就能够编译通过了。例如,下面的代码就是合法的:

let mut number = 1;
let a = &mut number;
*a = 2;
let b = &mut number;
*b = 3;

最后一条:一个数据在其生命周期内不能同时存在可变借用和不可变借用

怎么理解呢?不可变借用期待其借用的数据在其使用时是不变的,而可变借用又表示它可能修改数据。如果两者同时存在,即“同时”读取和修改数据,就不能保证语言的安全性和代码的正确性了。

例如,这个代码就是会报错的:

let mut number = 1;
let a = &number;
let b = &mut number;
println!("{a}");
*b = 2;
println!("{number}");

:::error[报错信息]

error[E0502]: cannot borrow `number` as mutable because it is also borrowed as immutable
 --> main.rs:4:13
  |
3 |     let a = &number;
  |             ------- immutable borrow occurs here
4 |     let b = &mut number;
  |             ^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{a}");
  |                - immutable borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

:::

:::info[报错信息中的生命周期提示] 如果你仔细读了报错信息,你会发现报错信息中是有一定的生命周期提示的。

报错信息在 A 处指出“在这里进行不可变借用”,在 B 处指出“在这里进行可变借用”,在 C 处又指出“不可变借用稍后在这使用”,并且 B 在 A 和 C 之前。这其实就是在告诉你:可变借用和不可变借用的生命周期存在重叠,也就是“同时”的意思。 :::

注意,格式化字符串获取的也是数据的不可变借用,因此下面的代码也会报错:

let mut number = 1;
let a = &mut number;
println!("{number}");
*a = 2;

:::error[报错信息]

error[E0502]: cannot borrow `number` as immutable because it is also borrowed as mutable
 --> main.rs:4:16
  |
3 |     let a = &mut number;
  |             ----------- mutable borrow occurs here
4 |     println!("{number}");
  |                ^^^^^^ immutable borrow occurs here
5 |     *a = 2;
  |     ------ mutable borrow later used here
  |
  = 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)

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

:::

所有权系统总结

终于把 Rust 中最难啃的骨头啃下来啦!这里我们在总结复习一下 Rust 所有权系统中的一些规则。作者在这里把规则再细化了一些,这样每一条都比较短,比“长难句”更容易理解和记忆。当然,记忆这些规则还是需要靠理解——理解为什么 Rust 认为这样做有违背数据安全和内存安全的原则。

  1. 变量的作用域指的是变量可以使用的范围。和 C++ 一样,是变量被声明时所在的花括号范围。
  2. 数据的生命周期是数据从创建到最后一次使用的范围(官方定义是“引用的有效范围”)。
  3. 一个数据同时只能被一个变量拥有,该变量称之为所有者,拥有该数据的所有权。
  4. 一个变量同时只能拥有一个数据的所有权。
  5. 可以通过所有权转移(数据移动)的方式将一个数据的所有权从一个变量手中转移给另一个变量,原变量失去原数据的所有权,不得再操作数据,除非拥有新的数据。
  6. 借用的生命周期必须在其所借用的数据的生命周期范围内。
  7. 不允许借用过期的数据。
  8. 不可变借用可以借用不可变变量或可变变量所拥有的数据。
  9. 可变借用只能借用可变变量所拥有的数据。
  10. 在数据的生命周期内,可以同时存在多个不可变借用。
  11. 在数据的生命周期内,不可以同时存在多个可变借用。
  12. 在数据的生命周期内,不可以同时存在可变借用和不可变借用。

当然,现在不能完全理解也没关系,先继续学下去吧。随着 Rust 代码的阅读量和编写量上来以后,你会慢慢就将这些规则内化于心的。

第三篇预告

在第三篇,你将学习到:控制流与函数、表达式型语法。

版权许可协议

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

本文章使用 CC BY-SA 4.0(Creative Commons Attribution-ShareAlike 4.0 International License,知识共享 署名-相同方式共享 4.0 国际许可协议)进行许可。

您可以自由地:

惟须遵守以下条件: