UMBRELLA

未雨绸缪,举重若轻

Rust 中,枚举也可以包含数据,甚至是不同类型的数据。例如,RustResult<String, io::Error> 类型是一个枚举,这样的值要么是包含字符串的 Ok值,要么是包含 io::ErrorErr 值。

只要 value 只有一种可能,枚举就很有用。使用它们的代价是你必须安全地访问数据,使用模式匹配就可以完成。Rust 模式有点像正则表达式,它们用于检测一个值是否是想要的,他们也可以将结构体或tuple中的多个字段提取到局部变量中。

枚举

来看一个标准库中枚举示例 std::cmp::Ordering,它有三种可能的值:Ordering::LessOrdering::EqualOrdering::Greater,称为变量或者构造函数:

1
2
3
4
5
6
#[repr(i8)]
pub enum Ordering {
Less,
Equal,
Greater,
}

我们在使用的时候可以直接导入:

1
2
3
4
use std::cmp::Ordering;

//或者
use std::cmp::Ordering::{self, *};

如果导入当前模块的枚举的构造函数可以使用 self

1
2
3
4
5
6
7
enum Pet {
Orca,
Giraffe,
...
}

use self::Pet::*;
阅读全文 »

编程中可能经常遇到要用相同的逻辑处理不同的类型,即使这个类型是还没出世的自定义类型。这种能力对于 Rust 来说并不新鲜,它被称为多态性,诞生于 1970 年代的编程语言技术,到现在为止仍然普遍。Rust 支持具有两个相关特性的多态性:Trait 和 泛型。

TraitRust 对接口或抽象基类的对照实现,它们看起来就像 JavaC# 中的接口:

1
2
3
4
5
6
7
8
trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;

fn flush(&mut self) -> Result<()>;

fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
...
}

FileTcpStream 以及 Vec<u8> 都实现了 std::io::Write,这3个类型都提供了 .write().flush() 等等方法,我们可以使用 write 方法而不用关心它的实际类型:

1
2
3
4
5
6
use std::io::Write;

fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
out.write_all(b"hello world\n")?;
out.flush()
}

&mut dyn Write 的意思是任何实现了 Write 的可变引用,我们可以调用 say_hello 并且给他传递这样一个引用:

1
2
3
4
5
6
7
use std::fs::File;
let mut local_file = File::create("hello.txt")?;
say_hello(&mut local_file)?; // works

let mut bytes = vec![];
say_hello(&mut bytes)?; // also works
assert_eq!(bytes, b"hello world\n");

泛型函数就像 C++ 中模板函数,一个泛型函数或者类型可以用于许多不同类型的值:

1
2
3
4
5
6
7
8
9
/// Given two values, pick whichever one is less.
fn min<T: Ord>(value1: T, value2: T) -> T {

if value1 <= value2 {
value1
} else {
value2
}
}

<T: Ord> 意思是 T 类型必须实现 Ord,这称为边界,因为它设置了 T 可能是哪些类型,编译器为实际使用的每种类型 T 生成自定义机器代码。

阅读全文 »

我们可以为自定义的类型实现加减乘除操作,只要实现标准库的一些 Trait,这称之为运算符重载。下图是可以重载的运算符和需要对应实现的 Trait 列表:

阅读全文 »

Rust 中的 Trait 可以分为三类:

  1. 语言扩展 Trait:主要用于运算符重载,我们可以将常用的运算符使用在自己的类型之中,只要相应的 Trait 即可,例如 EqAddAssignDereDrop 以及 FromInto 等;

  2. 标记类型 Trait:这些 Trait 主要用于绑定泛型类型变量,以表达无法以其他方式捕获的约束,这些包括 SizedCopy

  3. 剩下的主要是一些为解决常遇到的问题,例如:DefaultAsRefAsMutBorrowBorrowMutTryFromTryInto

Drop

Rust 中当一个值离开作用域时就会对它的内存进行清理,但是所有权转移不会,这类似于 C++ 中的析构函数。在 Rust 中我们也可以对析构的过程进行自定义,只要实现 std::ops::Drop 即可,在值需要清理的时候会自动调用 drop 函数,不能显示调用:

1
2
3
pub trait Drop {
fn drop(&mut self);
}

通常不需要实现 std::ops::Drop,除非定义了一个拥有 Rust 不知道的资源的类型。 例如,在 Unix 系统上,Rust 的标准库在内部使用以下类型来表示操作系统文件描述符:

1
2
3
struct FileDesc {
fd: c_int,
}

FileDescfd 字段只是程序完成时应该关闭的文件描述符的编号,c_inti32 的别名。标准库为 FileDesc 实现 Drop 如下:

1
2
3
4
5
6
7
impl Drop for FileDesc {
fn drop(&mut self) {
if self.close_on_drop {
unsafe { ::libc::close(self.fd); }
}
}
}

这里,libc::closeC 库关闭函数的 Rust 名称,Rust 仅能在 unsafe 块中调用 C 函数。

如果一个类型实现了 Drop,它就不能实现 Copy,如果类型可 Copy,则意味着简单的逐字节复制足以生成该值的独立副本,但是在相同的数据上多次调用相同的 drop 方法通常是错误的。

标准库预包含的 drop 函数可以显示删除一个值:

1
2
3
let v = vec![1, 2, 3];

drop(v); // explicitly drop the vector
阅读全文 »

迭代器是产生一系列值的值,通常用于循环操作。Rust 的标准库提供了遍历vector、字符串、哈希表和其他集合的迭代器,还提供了从输入流生成文本行、网络连接、用于多线程之间值传递的迭代器,Rust 的迭代器灵活、富有表现力且高效。

Rust 中,std::iter::Iteratorstd::iter::IntoIterator 是实现迭代器的基础。

1
2
3
4
5
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
...
}

迭代器可以是任何实现了 Iterator 的值,Item 是每次迭代产生的值,next 要么返回 Some(v)v 是下一个值,要么返回 None 表示迭代结束。

想要被迭代的类型也可以实现 std::iter::IntoIterator,它的 into_iter 返回一个迭代器:

1
2
3
4
5
pub trait IntoIterator {
type Item;
type IntoIter: Iterator;
fn into_iter(self) -> Self::IntoIter;
}

我们常用的 for 循环仅仅是先调用 into_iter 生成一个迭代器,然后重复调用迭代器的 next 方法,直到 next 返回 None,预示着迭代结束。

阅读全文 »

UML 是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开放方法。UML展现了一系列最佳工程实践,这些最佳实践在对大规模,复杂系统进行建模方面,特别是在软件架构层次已经被验证有效。

阅读全文 »

SOLID 设计原则并非单纯的一个原则,它实际上包含5个设计原则:单一职责原则、开闭原则、里氏替换替换原则,接口隔离原则和依赖反转原则。

单一职责原则(SRP

SRP(Single Responsibility Principle) 这个原则的意思是一个类或者一个模块只负责完成一个功能。所以,这里有两种理解方式,一种理解是把模块看做比类更加抽象的概念,类也可以看做是模块。另一种理解是把类看做是比类更加粗粒度的代码块,模块中包含多个类。

单一职责原则定义非常简单,不难理解。一个类只负责完成一个职责或者功能,也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个雷包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。举例来讲,如果一个类包含了用户和订单的一些操作,而用户和订单又是独立的业务领域模型,我们将它们放到一起就违反了单一职责原则,我们就需要进行拆分。

不同的应用场景、不同阶段的需求背景、不同的业务层面,对于同一个类的职责是否单一,可能会有不用的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,代码行数过度,函数或者属性过多都可能是违反单一职责原则的表象。

例如,下面的 UserInfo 类,这个类里面除了用户的基本信息,还有地址信息。或许一个观点是都属于用户的基本信息应该放在一起,另一个观点是可以拆分出 UserAddress 类,UserInfo 只保留除 Address 之外的其他信息,拆分之后两个类的职责更加单一。是否应该拆分,取决于具体情况,如果实际中地址信息和基本信息总是同时出现,那放在一起没有问题。但是如果地址信息单独在其他模块中使用,就应该单独抽象成 UserAddress

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserInfo {
private long userId;
private String username;
private String email;
private String telephone;
private long createTime;
private long lastLoginTime;
private String avatarUrl;
private String provinveOfAddress;
private String cityOfAddress;
private String regionOfAddress;
private String detailedAddress;
}

单一职责原则指导设计粒度较小的类,职责清晰的类,类的依赖以及被依赖的其他类也很会变少,从而降低代码的耦合性,实现高内聚、低耦合。但是如果拆分的过细,可能会适得其反,影响代码的可维护性。

阅读全文 »

几年前在面试的时候,还经常被面试官问 OOP 的四个特征是什么以及他们背后代表的意思,几年过去了,除了不支持面向对象的语言之外,面向对象编程思想已经深入到了每个开发者的灵魂,只是做的好与不好罢了。

面向对象编程中有两个非常基础的概念,类和对象,面向对象编程是一种编程范式或者说编程风格,它以类或者对象作为组织代码的基本单元,并将封装,继承,抽象,多态作为代码设计和实现的基石,不像面向过程编程语言,以函数为程序中的基本单元。

面向对象编程只是一种编程思想,可以用不同的语言进行实现,即使我们用面向对象语言,也完全可以写出面向过程风格的代码。至于什么是面向对象编程语言,并没有严格的定义,只要它能实现 OOP 的四大特性,那它就是面向对象编程语言,例如:RustC++GOJavaPython 以及 PHP 等,

面向对象编程的前提是面向对象分析(OOA)和面向对象设计(OOD),这样才能进行面向对象编程(OOP),具备完整的面向对象编程的思维。面向对象分析和设计两个阶段的产物应该是类的设计,包括应用程序应该被分为哪些类,每个类该有哪些属性和方法,类与类之间如何交互等等,它们比较贴近代码,非常具体,容易落地实现。

OOAOOD 的过程中,我们会经常用到 UML(Unified Model Language) 工具辅助我们进行工作。UML 是一种比较复杂的工具,除了包括我们常见的类图,还有用例图,顺序图,活动图,状态图,组件图等,即使是类图,类之间的关系就有泛化,实现,关联,聚合,组合以及依赖等,熟练掌握难度比较大,即便你掌握了,你同事不一定掌握,沟通成本依然很高,大多时候,我们会用草图实现我们的设计过程。

阅读全文 »

设计模式是软件设计中常见问题的典型解决方案。它们就像能根据需求进行调整的预制蓝图,可用于解决代码中反复出现的设计问题。

设计模式与方法或库的使用方式不同,很难直接在自己的程序中套用某个设计模式。模式并不是一段特定的代码,而是解决特定问题的一般性概念。可以根据模式来实现符合自己程序实际所需的解决方案。

人们常常会混淆模式和算法, 因为两者在概念上都是已知特定问题的典型解决方案。但算法总是明确定义达成特定目标所需的一系列步骤,而模式则是对解决方案的更高层次描述,同一模式在两个不同程序中的实现代码可能会不一样。

算法更像是菜谱:提供达成目标的明确步骤。而模式更像是蓝图:可以看到最终的结果和模式的功能,但需要自己确定实现步骤。

设计模式从分类上来讲,可以分为创建型、结构型和行为型。

阅读全文 »

如果我们正在开发一个聊天室,并且使用线程处理每个连接,我们的代码可能看起来像下面这个样子:

1
2
3
4
5
6
7
8
9
use std::{net, thread};
let listener = net::TcpListener::bind(address)?;
for socket_result in listener.incoming() {
let socket = socket_result?;
let groups = chat_group_table.clone();
thread::spawn(|| {
log_error(serve(socket, groups));
});
}

对于每个新连接,这都会产生一个运行 serve 函数的新线程,该线程能够专注于管理单个连接的处理。

这很好用,但是如果突然用户达到成千上万时,线程堆栈增长到 100 KiB 或这更多时,这可能要花费几个GB的内存。线程对于在多个处理器之间分配工作是非常好的一种形式,但是它们的内存需求使得我们在使用时要非常小心。

不过可以使用 Rust 异步任务在单个线程或工作线程池上并发运行许多独立活动。异步任务类似于线程,但创建速度更快,并且内存开销比线程少一个数量级。在一个程序中同时运行数十万个异步任务是完全可行的。当然,应用程序可能仍会受到网络带宽、数据库速度、计算或工作固有内存要求等其他因素的限制,但内存开销远没有线程那么多。

一般来说,异步 Rust 代码看起来很像普通的多线程代码,除了涉及到的 I/O 操作,互斥锁等阻塞操作需要稍微的不同处理。之前代码的异步版本如下所示:

1
2
3
4
5
6
7
8
9
10
11
use async_std::{net, task};

let listener = net::TcpListener::bind(address).await?;
let mut new_connections = listener.incoming();
while let Some(socket_result) = new_connections.next().await {
let socket = socket_result?;
let groups = chat_group_table.clone();
task::spawn(async {
log_error(serve(socket, groups).await);
});
}

这使用 async_stdnettask模块,并在可能阻塞的调用之后添加 .await。但整体结构与基于线程的版本相同。

本节的目标不仅是帮助编写异步代码,而且还以足够详细的方式展示它的工作原理,以便可以预测它在应用程序中的表现,并了解它最有价值的地方。

  • 为了展示异步编程的机制,我们列出了涵盖所有核心概念的最小语言特性集:futures、异步函数、await 表达式、task 以及 block_onspawn_local executor

  • 然后我们介绍异步代码块和 spawn executor。这些对于完成实际工作至关重要,但从概念上讲,它们只是我们刚刚提到的功能的变体。在此过程中,我们会可能会遇到一些异步编程特有的问题,但是需要学习如何处理它们;

  • 为了展示所有这些部分的协同工作,我们浏览了聊天服务器和客户端的完整代码,前面的代码片段是其中的一部分;

  • 为了说明原始 futuresexecutors 是如何工作的,我们提供了 spawn_blockingblock_on 的简单但功能性的实现;

  • 最后,我们解释了 Pin 类型,它在异步接口中不时出现,以确保安全使用异步函数和 futures

阅读全文 »
0%