Skip to content

Commit 33dd74a

Browse files
committed
Add new post [Rust通过Trait扩展已有类型.org]
1 parent 76cb167 commit 33dd74a

File tree

1 file changed

+188
-0
lines changed

1 file changed

+188
-0
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
+++
2+
title = "Rust通过Trait扩展已有类型"
3+
date = 2024-12-04T18:04:00+08:00
4+
lastmod = 2024-12-04T19:21:59+08:00
5+
tags = ["rust", "programming"]
6+
categories = ["rust", "programming"]
7+
draft = false
8+
toc = true
9+
+++
10+
11+
## <span class="section-num">1</span> Swift extension {#swift-extension}
12+
13+
可扩展性是一个语言非常关键的特性,以Swift 为例,它有一个相当好用的特性,名为 [extension](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/extensions/), 它可以非常便利地扩展已有的类型, 例如给已有类型增加 computed property, 实例方法, 新增构造器又或是实现新的 Protocol.
14+
15+
已有的类型既可以是你自己的代码,或者是第三方的代码,甚至是标准库的代码, 以标准库的 `String` 类型为例:
16+
17+
```swift
18+
extension String {
19+
var isPalindrome: Bool {
20+
let reversed = String(self.reversed())
21+
return self == reversed
22+
}
23+
func greet() -> Void {
24+
print("Hello \(self)")
25+
}
26+
}
27+
28+
let word = "racecar"
29+
print(word.isPalindrome) // Outputs: true
30+
word.greet() // Outputs: Hello racecar
31+
```
32+
33+
又或者让 `String` 实现新的 Protocol, 如:
34+
35+
```swift
36+
extension String: YourOwnProtocol {
37+
38+
}
39+
```
40+
41+
换言之,如果你对已有的类型不满意,你可以直接扩展已有的类型,添加上你想要的属性,方法或者实现你期望的接口。
42+
43+
44+
## <span class="section-num">2</span> Rust 的扩展能力 {#rust-的扩展能力}
45+
46+
Rust 也部分支持Swift extension 特性,如让已有的类型实现新的Trait.
47+
48+
还是以 `String` 为例子, 我们希望给 `String` 实现一个 `Greet` 的接口:
49+
50+
```rust
51+
// Define a trait with the desired functionality
52+
trait Greet {
53+
fn greet(&self) -> String;
54+
}
55+
56+
// Implement the trait for an existing type
57+
impl Greet for String {
58+
fn greet(&self) -> String {
59+
format!("Hello, {}!", self)
60+
}
61+
}
62+
63+
fn main() {
64+
let name = String::from("Rust");
65+
println!("{}", name.greet()); // Outputs: "Hello, Rust!"
66+
}
67+
```
68+
69+
这样我们就给 `String` 添加上 `greet` 方法,不足之处在于,需要定义一个额外的 `trait=,没有像 Swift 那样的 =extension` 语法糖可以用.
70+
71+
72+
### <span class="section-num">2.1</span> 实际例子 {#实际例子}
73+
74+
上面的 `Greet` 接口可能过于简单,让我们来看下实际项目的例子, 在[测试技能进阶(三): Property Based Testing](https://ramsayleung.github.io/zh/post/2024/%E6%B5%8B%E8%AF%95%E6%8A%80%E8%83%BD%E8%BF%9B%E9%98%B6%E4%B8%89_property_based_testing/) 一文中,我提到了使用 [Quickcheck](https://github.com/BurntSushi/quickcheck) 库在Rust实现 Property Based Testing.
75+
76+
假如有 Book struct, 我们只要实现 quickcheck 的 Arbitrary 接口,quickcheck 就会按照我们指定的规则来生成随机测试数据:
77+
78+
```rust
79+
use quickcheck::{Arbitrary, Gen};
80+
81+
#[derive(Debug, Clone, PartialEq)]
82+
struct Book {
83+
isbn: String,
84+
title: String,
85+
author: String,
86+
publication_year: u16,
87+
}
88+
89+
impl Arbitrary for Book {
90+
fn arbitrary(g: &mut Gen) -> Self {
91+
Book {
92+
// isbn必须以`ISBN` 开头,后接任意的大于等于0,小于uint32.max_value
93+
// 的整型
94+
isbn: format!("ISBN-{}", u32::arbitrary(g)),
95+
title: String::arbitrary(g), // 任意的字符串
96+
author: String::arbitrary(g), // 任意的字符串
97+
publication_year: *g.choose(&[2014_u16, 2022_u16, 2025_u16]).unwrap(), // 2014,2022或2025年出版的书
98+
}
99+
}
100+
}
101+
```
102+
103+
quickcheck 的 `Gen` 结构体有一个非常顺手的函数 `gen_range` ,用于生成指定的范围的数据, 但是作者在[1.0](https://github.com/BurntSushi/quickcheck/blob/aa968a94650b5d4d572c4ef581a7f5eb259aa0d2/src/arbitrary.rs#L72)之后,就不向外暴露这个接口了,不然我们就可以通过 `g.gen_range(b'a'...b'z') as char)` 来指定我们想要的数据.
104+
105+
既然这么好用的函数没有了,我们可以通过 `Trait` 的扩展能力,把这个 `gen_range` 函数带回来.
106+
107+
思路很简单,就是定义一个 `GenRange` Trait, 然后再让 `Gen` 实现这个 `Trait`.
108+
109+
```rust
110+
use core::ops::Range;
111+
112+
use num_traits::sign::Unsigned;
113+
pub use quickcheck::*;
114+
115+
pub trait GenRange {
116+
fn gen_range<T: Unsigned + Arbitrary + Copy>(&mut self, _range: Range<T>) -> T;
117+
}
118+
119+
impl GenRange for Gen {
120+
fn gen_range<T: Unsigned + Arbitrary + Copy>(&mut self, range: Range<T>) -> T {
121+
<T as Arbitrary>::arbitrary(self) % (range.end - range.start) + range.start
122+
}
123+
}
124+
```
125+
126+
通过上面的代码, 我们就可以在 `Book``arbitrary` 函数中使用 `gen_range` 了:
127+
128+
```rust
129+
impl Arbitrary for Book {
130+
fn arbitrary(g: &mut Gen) -> Self {
131+
Book {
132+
// isbn必须以`ISBN` 开头,后接任意的大于等于0,小于uint32.max_value
133+
// 的整型
134+
isbn: format!("ISBN-{}", u32::arbitrary(g)),
135+
title: String::arbitrary(g), // 任意的字符串
136+
author: String::arbitrary(g), // 任意的字符串
137+
publication_year: g.gen_range(2014...2026), // 2014-2025年出版的书
138+
}
139+
}
140+
}
141+
```
142+
143+
144+
### <span class="section-num">2.2</span> 使用已有Trait扩展已有类型 {#使用已有trait扩展已有类型}
145+
146+
上面提到的例子都是通过定义一个新的 `Trait`, 然后让已有类型实现这个新Trait, 那么是否可以让已有类型实现已有的Trait 呢?
147+
148+
事实上, 由于Orphan Rule的限制, Rust 并不允许已有类型实现已有接口, 以下的代码是无法编译通过的:
149+
150+
```rust
151+
use std::fmt;
152+
153+
// Implement the external trait for the wrapper
154+
impl fmt::Display for String {
155+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156+
write!(f, "String: {}", self)
157+
}
158+
}
159+
```
160+
161+
所谓的 `Orphan Rule` 限制指的是,如果允许已有类型实现已有接口, 那么 `lib1``lib2` 都实现了 `impl fmt::Display for String`, 编译器并不知道应该使用哪个lib的实现.
162+
163+
对此,Rust 官方也提供了[指引](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types),我们可以通过定义一个 `Wrapper` 类来实现我们的诉求:
164+
165+
```rust
166+
use std::fmt;
167+
168+
// Define a newtype wrapper
169+
struct MyString(String);
170+
171+
// Implement the external trait for the wrapper
172+
impl fmt::Display for MyString {
173+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174+
write!(f, "MyString: {}", self.0)
175+
}
176+
}
177+
178+
fn main() {
179+
let s = MyString("Hello".to_string());
180+
println!("{}", s); // Outputs: MyString: Hello
181+
}
182+
```
183+
184+
185+
## <span class="section-num">3</span> 参考 {#参考}
186+
187+
- [Using the Newtype Pattern to Implement External Traits on External Types](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types)
188+
- [add back a way to put a bound on numbers generated](//github.com/BurntSushi/quickcheck/issues/267)

0 commit comments

Comments
 (0)