Rust 笔记 -- 结构体、枚举、模块系统、集合
The Rust Programming Language
Rust 编程语言笔记。
来源:The Rust Programming Language By Steve Klabnik, Carol Nichols 。
翻译参考:Rust 语言术语中英文对照表
结构体
初始化
Rust 的结构体类似于 C,使用关键字 struct
声明。
struct User {
active: bool,
sign_in_count: u32,
username: String,
email: String
}
结构体中的每个元素称为“字段”(field),字段是可变的(mutable),使用 .
来访问字段的值。
创建实例
为了使用结构体,需要根据结构体创建一个实例(instance),并给该结构体的字段赋值,赋值的顺序可以不同于结构体定义的顺序。
使得字段可变,必须给实例添加 mut
关键字,Rust 不允许给某一个或几个字段添加 mut
关键字。
struct User {
active: bool,
sign_in_count: u32,
username: String,
email: String
}
fn main() {
let mut user1 = User {
active: false,
sign_in_count: 1,
username: String::from("someusername"),
email: String::from("someuseremail"),
};
user1.email = "anotheremail";
}
可以使用 结构体更新语法 ..
来从其他实例来创建新实例:
struct User {
active: bool,
sign_in_count: u32,
username: String,
email: String
}
fn main() {
let user1 = User {
active: false,
sign_in_count: 1,
username: String::from("someusername"),
email: String::from("someuseremail"),
};
/*
// regular
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
*/
let user_2 = User {
active: true,
..user1
}
}
上面的代码表示,除了字段 active
之外,user_2
的其他字段值和 user1
相等。
注:..user1
后没有 ,
,而且必须放在最后一行。
元组结构体
元组结构体(tuple struct) 类似于元组。可以理解为给元组分配了有意义的名称,但是并没有确切的字段,只有字段的类型。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let red = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
上面的两个实例虽然有着相同的字段类型以及字段值,但依然是不同的实例。
每个结构体实例的类型就是其定义的类型,即便它们有完全相同的字段且字段的类型一致。
类单元结构体
类单元结构体(unit-like struct)指的是不含任何数据的结构体。类似于不含成员的元组--单元(unit) ()
。
struct ULS;
fn main() {
let subject = ULS;
}
函数 VS 方法
关联和区别
在一些编程语言中,函数(function)和方法(method)通常有着相同的含义。在 Rust 中,两者的关联和区别如下:
- 关联
- 都使用
fn
关键字声明 - 都有参数和返回值
- 都可以被调用
- 都使用
- 区别
- 方法的第一个参数永远是
self
,表示被调用的方法作用的实例(instance) - 方法通常被定义在一个结构体、枚举或者 对象的上下文(context)下,而函数通常没有具体的上下文
- 方法的第一个参数永远是
Rust 使用方法的原因是提高代码的组织性。impl
紧紧关联着作用的结构体。
定义方法
方法使用 fn
关键字声明,通常写在 impl(implementation)
块(block)中。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("The are of rectangle {}", rect1.area());
}
方法的第一个参数是 self
,其实是 self: Self
的简洁表示。如果不希望方法带走 ownership,应该使用 &self
,如果希望更改数据,使用 &mut self
。
类似函数,方法同样使用
.
运算符调用。与 C、C++ 等语言不同,Rust 不支持使用->
运算符来调用方法,而是通过被称为 自动引用和解引用 的方式来调用方法。大致原理为:当调用object.method()
时,Rust 会自动添加&
,&mut
,*
,因此object
匹配了方法的签名以下两行代码作用相同:
p1.distance(&p2); (&p1).distance(&p2);
getters
如果对结构体实现了同名的字段和方法,那么 object.field
表示访问字段,object.method()
表示调用方法。
通常,调用同名的方法表示希望获取其同名的字段的值,这类方法被称为 getters。一些编程语言会自动实现 getters,但是 Rust 并非如此。
关联函数
定义在 impl
块下的函数都被称为 关联函数(Associated Function),因为它们作用于 impl
后的结构体。
也可以定义第一个参数不为 self
的关联函数,这类函数通过 ::
作用,例如:String::from()
。
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
let sq = Rectangle::square(3);
所以 ::
语法同时用于关联函数和模块(module)的命名空间。
多个参数的方法
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Rust 允许使用多个 impl
块声明方法,但是在本例中,两个方法放在一个 impl
中可读性更好。
枚举
枚举类型(enumerations / enums)定义穷举所有可能的值的数据类型。
定义枚举
枚举类型使用 enum
关键字来声明,使用 ::
来获取枚举的值。
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
fn route(ip_kind: IpAddrKind) {} // 函数声明并传入枚举类型
route(IpAddrKind::V4);
route(IpAddrKind::V6); // 调用函数并传入枚举类型
枚举类型的一个好处是:枚举值可以是不同类型的。
例如:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
impl 方法
类似于结构体,可以使用 impl
给枚举类型定义方法:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// something to do
}
}
let m = Message::Write(String::from("Hello"));
m.call();
Option 枚举类型
Option
是一种标准库定义的枚举类型。
在编程语言的设计中,排除和包含特性一样重要。例如:Rust 就排除了 null
。在其他实现 null
的编程语言中,变量总是有 null
和 non-null
两种状态,null
表示没有值。
Rust 使用 Option<T>
枚举类型来表示 null
,其定义为:
enum Option<T> {
None,
Some(T),
}
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
不能把类型不同的值做算术运算,
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
上述代码编译器会报错。
模式匹配
在 Rust 中,使用 match
来对某个值和一系列模式进行匹配。模式可以是字面量、变量以及其他类型。
每个匹配(arm)都由模式和代码组成,每个 arm 之间用 ,
分隔。模式和代码之间用 =>
相连。
如果代码为多行,需要放入括号 {}
。代码由表达式组成,如果匹配成功,该表达式的值作为整个 match
的返回值。
例如:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
/*
Coin::Penny => {
println!("That's an penny!");
1
},
*/
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
match
和 if
的不同在于:if
的条件结果必须是 bool
,而 match
可以是任意类型。
模式绑定值
match
的匹配(arm)还有一个常用的特性是把值绑定给匹配的模式。
例如:
enum UsState {
Alabama,
Alaska,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coine) -> u8 {
match coin {
Coin::Peeny => 1,
Coin::Nickle => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}", state);
25
}
}
}
上面的代码中,如果调用 value_in_cents(Coin::Quarter(UsState::Alaska))
, coin
将会匹配到 Coin::Quarter(Alaska)
这一行,此时 state
的值就和 UsState::Alaska
绑定了。
匹配和 Option
如下例:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five); // 6
let none = plus_one(None); // None
一个不漏
match
还有一个特性是:必须匹配(arm)所有可能的模式。
例如上面的代码,如果改为:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
编译器会报错,因为没有处理 None
模式的匹配。
所以 match
在 Rust 中是 穷尽(exhaustive) 的。
_ 占位符来匹配所有的模式
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
// _ => reroll()
// _ => () // do nothing
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
fn reroll() {}
在上述代码中,使用 other
代表其他同种处理的匹配。或者使用 _
占位符来表示。
if let
if let
语法提供了一种更简洁的方式来处理某种模式匹配成功并忽略其他选项的情况。
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
// same as
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
模块系统
Rust 的模块系统(Module System)包括:
- 包(package):构建、测试、共享 crates。
- crates:可生成库(library)或者可执行程序的模块树。
- 模块(module):控制路径的组织方式、作用域以及私有性。
- 路径(path):命名一个实体的方式,例如:结构体、函数、模块。
包
包 是一系列 crates 的集合。包中有名为 Cargo.toml
的文件定义了如何构建这些 crates。
使用 cargo new
命令后,Rust 会在当前目录创建一个包。
包中至少要包含一个 crate。包可以包含多个二进制 crates,但是最多只能包含一个库 crate。
Cargo 是最常用的包,其默认把 src/main.rs
和 src/lib.rs
作为二进制 crate 和库 crate,并把两者作为 crate root。当使用 rustc
时,Rust 把这两个文件(如果存在)编译。
Crate
在 Rust 中,crate 指的是编译器所编译的源文件,是编译器一次编译时的最小单位。
crate 包含多个模块。
crate 分为二进制 crate(binary crate)和库(library crate)两种:
-
二进制 crate 是由 Rustaceans 所编写的代码,每个二进制 crate 必须包含一个
main
函数。 -
库 crate 不含
main
函数,不能被编译为可执行程序,而是作为一种共享方式在项目中。
两种 crate 分别在 src
路径下以 main.rs
和 lib.rs
两种文件名称存在。
crate root 指的是 Rust 编译器编译的源文件,以及 crate 的根模块。
模块
总览
假设有以下文件结构:
backyard
|--Cargo.lock
|--Cargo.toml
|--src
|--garden
| |--vegetable.rs
|--garden.rs
|--main.rs
// src/main.rs
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {:?}!", plant);
}
// garden.rs
pub mod vegetables;
// vegetables.rs
#[derive(Debug)]
pub struct Asparagus {}
- backyard 是 crate 目录
src/main.rs
是 crate rootpub mod garden
表示 garden 是一个模块,可见性为pub
。这行代码表示把garden.rs
里的内容引入pub mod vegetables
作用同上
所以,模块的工作原理:
-
从 crate root 开始:当编译 crate 时,编译器首先找到 crate root 文件(通常是
main.rs
或者lib.rs
)来编译 -
定义模块:在 crate root 文件中,可以用
mod
关键字声明新的模块,例如:mod garden
,编译器会在以下目录寻找该模块的代码:- 行内
src/garden.rs
src/garden/mod.rs
-
定义子模块:在 除 crate root 的文件里还可以定义子模块,例如:
mod vegetables
,编译器会在其父模块的目录下寻找子模块的代码:- 行内
src/garden/vegetables.rs
src/garden/vegetables/mod.rs
-
模块中的路径:一旦声明模块后,可以通过路径引入模块。例如:在
vegetables
模块内声明了Asparagus
,引入路径为:crate::garden::vegetables::Asparagus
-
私有 vs 共有:默认情况下,子模块的内容对父模块是私有的,使用
pub
关键字使其公有化 -
use
关键字:使用use
来简化引用。
优势
模块的优势:
- 提高代码的可读性和可重用性
- 隐私性
路径
类似于文件系统,有绝对路径和相对路径两种方式来表示层级关系:
- 绝对路径:指的是从 crate root 开始的完整路径。对于外部 crate 来说,绝对路径以 crate 的名称为开始;对内部 crate 来说,绝对路径以字面量
crate
开始 - 相对路径:从当前模块开始,通常包含
self
、super
等关键字
绝对路径和相对路径都使用 ::
表示层级间的分隔符。
绝对和相对路径各有优劣,可以根据个人偏好进行选择。在 Rust 中,一般使用绝对路径,原因是这样使得依赖相对独立。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
私有、公有
在 Rust 乃至整个计算机领域都有内部实现对外部不可见的原则。
私有的概念通常和作用域(scope)相关。例如:定义在函数 A 内部定义了子函数 B。对该子函数 B 来说,函数 A 对其是可见的。但对函数 A 的其他部分来说,子函数 B 是不可见的,
通常来讲,Rust 把项(items)设置为私有(privacy),或者称为不可见的。如果调用了不可见的对象,编译器会弹出错误。
在 Rust 中,默认对父模块私有的项(items)包括模块、函数、方法、结构体、枚举、常量。
可以使用 pub
关键字使项(items)变为可见、公有的。
注:把某个外部对象标识为 pub
并不意味着其内部对象也被标识为 pub
(枚举类型除外,如果枚举类型使用了 pub
,那么枚举的所有结果默认也为 pub
),例如:
fn main() {
pub fn outer_function() {
fn inner_function() {
// --snip--
}
}
outer_function(); // OK
inner_function(); // Error, because the function sign has no **pub**
}
pub enum IPAddr {
V4(String), // also **pub**
V6(String), // also **pub**
}
最佳实践
一般来说,一个包同时包含二进制 crate
src/main.rs
以及库 cratesrc/lib.rs
。两者默认都含有包名。常用的范式是:在
src/lib.rs
中定义模块树,这样,在二进制 crate 中调用任何公有的项(items)都可以以该包名为开始作为路径。
super
使用 super
关键字来引用父级路径,这类似于文件系统中的 ..
。
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
use、as
use
使用 use
关键字来引入路径。
Rust 的惯例是:在调用某个函数时,其路径应该引入到父级。虽然引入到当前级效果相同,但是前者使得函数定义更加清晰。例如:
use crate::galaxy::solar_system::earth;
earth();
use crate::galaxy::solar_system;
solar_system.earth(); // same thing, but this one is better.
再导出
再导出(re-exporting) 使得当前作用域引入的对象也可以用于其他作用域。因为默认情况下,使用 use
关键把某个名称引入当前作用域后,该名称对其他作用域是私有的。
使用 pub use
实现再导出:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
- 如果没有再导出,外部代码想调用
add_to_waitlist()
函数必须使用路径:restaurant::front_of_house::hosting::add_to_waitlist()
- 再导出后,使用
restaurant::hosting::add_to_waitlist()
即可
嵌套路径
为了避免使用多个 use
的多行引入导致代码可读性变差,可以把同一父对象下的子对象用花括号在同一行中。
use std::{io, fmt};
// same as
use std::io;
use std::fmt;
如果同时引入了父对象和其子对象,使用 self
关键字表示该父对象。
use std::io::{self, Result};
// same as
use std::io;
use std::io::Result;
Glob 运算符
如果要引入全部对象,使用全局 glob 运算符 *
。
use std::io::*;
as
假如引入的对象名称过长,可以使用 as
关键字来通过别名来引入。
use this_is_a_very_long_function_name as lfn;
lfn(); // much simpler
集合
Rust 的标准库中包含了一系列常用的数据结构被称为集合(collection)。最常用的是:
- 动态数组 Vector
- 字符串 String
- 哈希表 HashMap
这些结构的特点是:存储在堆中,可变长,使用泛型实现。这意味着在编译时,编译器并不知道这些结构的大小。
初始化集合的通用方法是 ::new()
动态数组
动态数组中的元素在内存中紧挨着彼此存储。
动态数组只能存储同种类型的数据,但是可以借助枚举来存储不同类型的数据。
初始化
动态数组 Vec<T>
的初始化,可以用 ::new()
来初始化一个空动态数组,也可以使用 宏(macro) vec![]
显式把动态数组的成员列出来初始化动态数组:
fn main() {
let v = vec![1, 2, 3, 4, 5];
let ano_v: Vec<i32> = Vec::new();
}
注:在第二种声明中指明了存储元素的类型,否则 Rust 并不知道 Vector 要存储什么类型的数据。
读写
写
使用 push
方法给动态数组添加元素:
let mut v = Vec::new();
v.push(1);
v.push(2);
读
使用 .get()
或者括号索引 []
的方式来访问动态数组元素:
let third: &i32 = &v[2]; // 3
let two: Option<&i32> = v.get(2); // Some(2)
如上面的代码所示,使用 .get()
方法得到的是 Option<T>
数据类型,而不是动态数组元素 <T>
的类型。
由于 .get()
方法得到的是 Option<T>
类型,因此可以使用 match
来对取得的值做判断。
let two = v.get(2);
match two {
Some(two) => println!("The element is {}", two),
None => println!("No such element"),
}
越界
两种访问方式对于动态数组越界有着不同的处理方式:
let third = &v[100]; // index out of bound
let two = v.get(100); // None
使用 []
索引访问可以通过编译,但在运行时会出现 index out of bound
索引越界的错误;使用 .get()
方法会得到 None
。
下面的例子说明了动态数组的工作方式:
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("{}", first); // Error
上面的代码不能通过编译,原因是:由于动态数组的元素在内存中是紧挨着彼此存储的。因此,给动态数组中添加新元素时,如果当前的内存空间位置不能容下新加入的元素,就需要分配一块新的内存空间并拷贝旧的元素到该空间。而引用访问动态数组元素也许会导致访问到已经被解除分配的空间。
遍历
使用 for in
来遍历动态数组元素:
for i in &v {
println!("{}", i);
}
使用 mut
引用来遍历并变动态数组元素:
for i in &mut v {
*i += 50;
}
字符串
两类字符串的比较
Rust 语言的核心中只有一种字符串:字符串切片 str
,通常以 &str
形式出现。
而 String
类型由 Rust 标准库实现的。
两者都是 UTF-8 编码的。
初始化
字符串 String
的初始化,可以用 ::new()
来初始化一个空字符串,也可以用 ::from()
显式初始化字符串,或者先声明字符串字面量,然后转化为 String
类型的字符串:
fn main() {
let mut s = String::from("Hello");
let mut ano_s = String::new();
let s_in_stack: &str = "Hello World";
let s_in_heap: String = s_in_stack.to_string();
}
读写
写
使用 push_str()
把字符串拼接至另一字符串尾:
s.push_str(", World");
println!("{}", s) // Hello, World
使用 push()
拼接一个字符到字符串尾:
let mut s = String::from("lo");
s.push('l');
println!("{}", s) // "lol"
使用 +
来拼接已有字符串:
let s1 = String::from("Hello, ");
let s2 = String::from("World");
let s = s1 + &s2;
println!("{}", s1); // Error
println!("{}", s2); // "World"
println!("{}", s); // "Hello, World"
注:拼接之后,s1
的 ownership 被转移给 s
,所以 s1
不能再被使用。这是因为 add
函数的签名:
fn add(self, s: &str) -> String {
决定了 self
位置的变量的 ownership 被夺取。第二个变量需要使用 &
引用形式,而不是直接把两个字符串的值相加。
这里编译器使用 强制转换(coerce) 把 String
类型转化为 &str
,当调用 add
时,Rust 会使用 解引用强制转换(deref coercio n) 把 &s2
转化为 &s2[..]
。
如果不希望 s1
的 ownership 发生变化,可以使用 format!
宏(macro) 来拼接字符串:
let s1 = String::from("Hello, ");
let s2 = String::from("World");
let s = format!("{s1}{s2}");
println!("{}", s1); // "Hello, "
println!("{}", s2); // "World"
println!("{}", s); // "Hello, World"
读
Rust 不支持索引访问字符串中的字符。
let s1 = String::from("Code");
println!("{}", s1[0]); // Error
上面的代码将不能通过编译,原因和 String
类型的内部实现有关:
String
类型是对 Vec<u8>
的包装,所以:
let hello = String::from("Hola");
let ano_hello = String::from("Здравствуйте");
hello
的 len
为 4
,因为在 UTF-8 编码中每个字符占用 1
个字节,而 ano_hello
的 len
为 24
,而非 12
,因为在 UTF-8 编码中,每个 Unicode scalar 值占用 2
个字节。因此,如果使用索引访问,将返回无意义的值。
可以使用 [..]
创建字符串切片:
let ano_hello = "Здравствуйте";
// let ano_hello = String::from("Здравствуйте"); // also Ok
let s = &ano_hello[0..4]; // Зд
s
是 ano_hello
的前 4
个字节,而非字符。
类型转换
使用 to_string
把其他类型转化为字符串:
let i: i32 = 2;
let i_s: String = i.to_string(); // "2";
遍历
使用 .chars()
获得字符串的序列,并用 for in
来遍历以输出字符串的字符:
let s = String::from("Hello");
for c in s.chars() {
println!("{}", c);
}
// H
// e
// l
// l
// o
类似地,使用 .bytes()
或者字符对应的字节序列,并用 for in
来遍历以输出字符串的字符:
let s = String::from("Hello");
for b in s.bytes() {
println!("{}", b);
}
// 72
// 101
// 108
// 108
// 111
哈希表
使用哈希表 HashMap<K, V>
前需要用 use
关键字引入:
use std::collections::HashMap;
哈希表的键类型为 String i32
,键和值必须为相同类型。
初始化
可以用 ::new()
来初始化一个空哈希表:
fn main() {
let hm = HashMap::new();
}
读写
写
使用 .insert()
添加键值对到哈希表(注意:mut
关键字):
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 30);
scores.insert(String::from("Black"), 20);
读
使用 .get()
根据键获取值:
println!("the blue: {:?}", scores.get("Blue").unwrap()); // 10
println!("the blue: {:?}", scores.get("Yellow")); // Some(30)
println!("the blue: {:?}", scores.get("Red").copied().unwrap_or(0)); // None
和动态数组类似,获取的值是 Option<&V>
类型,可以使用 unwrap()
获取 <T>
类型。
遍历
使用元组遍历哈希表:
for (key, value) in &scores {
println!("{} {}", key, value);
}
// One Possible Outcome:
// Blue 10
// Yellow 30
// Black 20
注:遍历的结果是无序的。
更新
哈希表的更新有几种不同的方式:
如果给同一个键添加多个值,结果是只保留最后一个值:
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(String::from("One"), 1);
map.insert(String::from("One"), 2);
println!("{:?}", map); // {"One": 2};
}
如果不存在键,使用 entry()
和 or_insert()
添加键值对:
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(String::from("One"), 1);
map.entry(String::from("One")).or_insert(2);
map.entry(String::from("Two")).or_insert(2);
println!("{:?}", map); // {"One": 1, "Two": 2};
}
entry()
API 返回 Entry
枚举类型,该枚举类型返回指定的键是否存在,or_insert()
构建在 Entry
之上,如果键存在就不做变,如果不存在就添加该键值对。