457 lines
22 KiB
Markdown
457 lines
22 KiB
Markdown
|
||
|
||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||
|
||
|
||
25 类型系统:如何围绕trait来设计和架构系统?
|
||
你好,我是陈天。
|
||
|
||
Trait,trait,trait,怎么又是 trait?how old are you?
|
||
|
||
希望你还没有厌倦我们没完没了地聊关于 trait 的话题。因为 trait 在 Rust 开发中的地位,怎么吹都不为过。
|
||
|
||
其实不光是 Rust 中的 trait,任何一门语言,和接口处理相关的概念,都是那门语言在使用过程中最重要的概念。软件开发的整个行为,基本上可以说是不断创建和迭代接口,然后在这些接口上进行实现的过程。
|
||
|
||
在这个过程中,有些接口是标准化的,雷打不动,就像钢筋、砖瓦、螺丝、钉子、插座等这些材料一样,无论要构筑的房子是什么样子的,这些标准组件的接口在确定下来后,都不会改变,它们就像 Rust 语言标准库中的标准 trait 一样。
|
||
|
||
而有些接口是跟构造的房子息息相关的,比如门窗、电器、家具等,它们就像你要设计的系统中的 trait 一样,可以把系统的各个部分联结起来,最终呈现给用户一个完整的使用体验。
|
||
|
||
之前讲了trait 的基础知识,也介绍了如何在实战中使用 trait 和 trait object。今天,我们再花一讲的时间,来看看如何围绕着 trait 来设计和架构系统。
|
||
|
||
由于在讲架构和设计时,不免要引入需求,然后我需要解释这需求的来龙去脉,再提供设计思路,再介绍 trait 在其中的作用,但这样下来,一堂课的内容能讲好一个系统设计就不错了。所以我们换个方式,把之前设计过的系统捋一下,重温它们的 trait 设计,看看其中的思路以及取舍。
|
||
|
||
用 trait 让代码自然舒服好用
|
||
|
||
在[第 5 讲],thumbor 的项目里,我设计了一个 SpecTransform trait,通过它可以统一处理任意类型的、描述我们希望如何处理图片的 spec:
|
||
|
||
// 一个 spec 可以包含上述的处理方式之一(这是 protobuf 定义)
|
||
message Spec {
|
||
oneof data {
|
||
Resize resize = 1;
|
||
Crop crop = 2;
|
||
Flipv flipv = 3;
|
||
Fliph fliph = 4;
|
||
Contrast contrast = 5;
|
||
Filter filter = 6;
|
||
Watermark watermark = 7;
|
||
}
|
||
}
|
||
|
||
|
||
SpecTransform trait 的定义如下(代码):
|
||
|
||
// SpecTransform:未来如果添加更多的 spec,只需要实现它即可
|
||
pub trait SpecTransform<T> {
|
||
// 对图片使用 op 做 transform
|
||
fn transform(&mut self, op: T);
|
||
}
|
||
|
||
|
||
它可以用来对图片使用某个 spec 进行处理。
|
||
|
||
但如果你阅读 GitHub 上的源码,你可能会发现一个没用到的文件 imageproc.rs 中类似的 trait(代码):
|
||
|
||
pub trait ImageTransform {
|
||
fn transform(&self, image: &mut PhotonImage);
|
||
}
|
||
|
||
|
||
这个 trait 是第一版的 trait。我依旧保留着它,就是想在此展示一下 trait 设计上的取舍。
|
||
|
||
当你审视这段代码的时候会不会觉得,这个 trait 的设计有些草率?因为如果传入的 image 来自不同的图片处理引擎,而某个图片引擎提供的 image 类型不是 PhotonImage,那这个接口不就无法使用了么?
|
||
|
||
hmm,这是个设计上的大问题啊。想想看,以目前所学的知识,怎么解决这个问题呢?什么可以帮助我们延迟 image 是否必须是 PhotonImage 的决策呢?
|
||
|
||
对,泛型。我们可以使用泛型 trait 修改一下刚才那段代码:
|
||
|
||
// 使用 trait 可以统一处理的接口,以后无论增加多少功能,只需要加新的 Spec,然后实现 ImageTransform 接口
|
||
pub trait ImageTransform<Image> {
|
||
fn transform(&self, image: &mut Image);
|
||
}
|
||
|
||
|
||
把传入的 image 类型抽象成泛型类型之后,延迟了图片类型判断和支持的决策,可用性更高。
|
||
|
||
但如果你继续对比现在的 ImageTransform和之前写的 SpecTransform,会发现,它们实现 trait 的数据结构和用在 trait 上的泛型参数,正好掉了个个。
|
||
|
||
你看,PhotonImage 下对于 Contrast 的 ImageTransform 的实现:
|
||
|
||
impl ImageTransform<PhotonImage> for Contrast {
|
||
fn transform(&self, image: &mut Image) {
|
||
effects::adjust_contrast(image, self.contrast);
|
||
}
|
||
}
|
||
|
||
|
||
而同样的,PhotonImage 下对 Contract 的 SpecTransform 的实现:
|
||
|
||
impl SpecTransform<&Contrast> for Photon {
|
||
fn transform(&mut self, op: &Contrast) {
|
||
effects::adjust_contrast(&mut self.0, op.contrast);
|
||
}
|
||
}
|
||
|
||
|
||
这两种方式基本上等价,但一个围绕着 Spec 展开,一个围绕着 Image 展开:-
|
||
|
||
|
||
那么,哪种设计更好呢?
|
||
|
||
其实二者并没有功能上或者性能上的优劣。
|
||
|
||
那为什么我选择了 SpecTransform 的设计呢?在第一版的设计我还没有考虑 Engine的时候,是以 Spec 为中心的;但在把 Engine 考虑进去后,我以 Engine 为中心重新做了设计,这样做的好处是,开发新的 Engine 的时候,SpecTransform trait 用起来更顺手,更自然一些。
|
||
|
||
嗯,顺手,自然。接口的设计一定要关注使用者的体验,一个使用起来感觉自然顺手舒服的接口,就是更好的接口。因为这意味着使用的时候,代码可以自然而然写出来,而无需看文档。
|
||
|
||
比如同样是 Python 代码:
|
||
|
||
df[df["age"] > 10]
|
||
|
||
|
||
就要比:
|
||
|
||
df.filter(df.col("age").gt(10))
|
||
|
||
|
||
要更加自然舒服。前面的代码,你看一眼别人怎么用,自己就很快能写出来,而后者,你需要先搞清楚 filter 函数是怎么回事,以及col()、gt() 这两个方法如何使用。
|
||
|
||
我们再来看来两段 Rust 代码。这行使用了 From/Into trait 的代码:
|
||
|
||
let url = generate_url_with_spec(image_spec.into());
|
||
|
||
|
||
就要比:
|
||
|
||
let data = image_spec.encode_to_vec();
|
||
let s = encode_config(data, URL_SAFE_NO_PAD);
|
||
let url = generate_url_with_spec(s);
|
||
|
||
|
||
要简洁、自然得多。它把实现细节都屏蔽了起来,只让用户关心他们需要关心的逻辑。-
|
||
所以,我们在设计 trait 的时候,除了关注功能,还要注意是否好用、易用。这也是为什么我们在介绍 KV server 的时候,不断强调,trait 在设计结束之后,不要先着急撰写实现 trait 的代码,而是最好先写一些对于 trait 使用的测试代码。
|
||
|
||
你在写这些测试代码的使用体验,就是别人在使用你的 trait 构建系统时的真实体验,如果它用起来别扭、啰嗦,不看文档就不容易用对,那这个 trait 本身还有待进一步迭代。
|
||
|
||
用 trait 做桥接
|
||
|
||
在软件开发的绝大多数时候,我们都不会从零到一完完全全设计和构建系统的所有部分。就像盖房子,不可能从一抔土、一块瓦片开始打造。我们需要依赖生态系统中已有的组件。
|
||
|
||
作为架构师,你的职责是在生态系统中找到合适的组件,连同你自己打造的部分,一起粘合起来,形成一个产品。所以,你会遇到那些接口与你预期不符的组件,可是自己又无法改变那些组件来让接口满足你的预期,怎么办?
|
||
|
||
此刻,我们需要桥接。
|
||
|
||
就像要用的电器是二相插口,而附近墙上的插座只有三相插口,我们总不能修改电器或者墙上的插座,使其满足对方吧?正确的做法是购置一个多项插座来桥接二者。
|
||
|
||
在 Rust 里,桥接的工作可以通过函数来完成,但最好通过 trait 来桥接。继续看[第 5 讲]thumbor 里的另一个 trait Engine(代码):
|
||
|
||
// Engine trait:未来可以添加更多的 engine,主流程只需要替换 engine
|
||
pub trait Engine {
|
||
// 对 engine 按照 specs 进行一系列有序的处理
|
||
fn apply(&mut self, specs: &[Spec]);
|
||
// 从 engine 中生成目标图片,注意这里用的是 self,而非 self 的引用
|
||
fn generate(self, format: ImageOutputFormat) -> Vec<u8>;
|
||
}
|
||
|
||
|
||
通过 Engine 这个 trait,我们把第三方的库 photon和自己设计的 Image Spec 连接起来,使得我们不用关心 Engine 背后究竟是什么,只需要调用 apply 和 generate 方法即可:
|
||
|
||
// 使用 image engine 处理
|
||
let mut engine: Photon = data
|
||
.try_into()
|
||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||
engine.apply(&spec.specs);
|
||
let image = engine.generate(ImageOutputFormat::Jpeg(85));
|
||
|
||
|
||
这段代码中,由于之前为 Photon 实现了 TryFrom,所以可以直接调用 try_into() 来得到一个 photon engine:
|
||
|
||
// 从 Bytes 转换成 Photon 结构
|
||
impl TryFrom<Bytes> for Photon {
|
||
type Error = anyhow::Error;
|
||
|
||
fn try_from(data: Bytes) -> Result<Self, Self::Error> {
|
||
Ok(Self(open_image_from_bytes(&data)?))
|
||
}
|
||
}
|
||
|
||
|
||
就桥接 thumbor 代码和 photon crate 而言,Engine 表现良好,它让我们不但很容易使用 photon crate,还可以很方便在未来需要的时候替换掉 photon crate。
|
||
|
||
不过,Engine 在构造时,所做的桥接还是不够直观和自然,如果不仔细看代码或者文档,使用者可能并不清楚,第3行代码,如何通过 TryFrom/TryInto 得到一个实现了 Engine 的结构。从这个使用体验来看,我们会希望通过使用 Engine trait,任何一个图片引擎都可以统一地创建 Engine结构。怎么办?
|
||
|
||
可以为这个 trait 添加一个缺省的 create 方法:
|
||
|
||
// Engine trait:未来可以添加更多的 engine,主流程只需要替换 engine
|
||
pub trait Engine {
|
||
// 生成一个新的 engine
|
||
fn create<T>(data: T) -> Result<Self>
|
||
where
|
||
Self: Sized,
|
||
T: TryInto<Self>,
|
||
{
|
||
data.try_into()
|
||
.map_err(|_| anyhow!("failed to create engine"))
|
||
}
|
||
// 对 engine 按照 specs 进行一系列有序的处理
|
||
fn apply(&mut self, specs: &[Spec]);
|
||
// 从 engine 中生成目标图片,注意这里用的是 self,而非 self 的引用
|
||
fn generate(self, format: ImageOutputFormat) -> Vec<u8>;
|
||
}
|
||
|
||
|
||
注意看新 create 方法的约束:任何 T,只要实现了相应的 TryFrom/TryInto,就可以用这个缺省的 create() 方法来构造 Engine。
|
||
|
||
有了这个接口后,上面使用 engine 的代码可以更加直观,省掉了第3行的try_into()处理:
|
||
|
||
// 使用 image engine 处理
|
||
let mut engine = Photon::create(data)
|
||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||
engine.apply(&spec.specs);
|
||
let image = engine.generate(ImageOutputFormat::Jpeg(85));
|
||
|
||
|
||
桥接是架构中一个非常重要的思想,我们一定要掌握这个思想的精髓。
|
||
|
||
再举个例子。比如现在想要系统可以通过访问某个 REST API,得到用户自己发布的、按时间顺序倒排的朋友圈。怎么写这段代码呢?最简单粗暴的方式是:
|
||
|
||
let secret_api = api_with_user_token(&user, params);
|
||
let data: Vec<Status> = reqwest::get(secret_api)?.json()?;
|
||
|
||
|
||
更好的方式是使用 trait 桥接来屏蔽实现细节:
|
||
|
||
pub trait FriendCircle {
|
||
fn get_published(&self, user: &User) -> Result<Vec<Status>, FriendCircleError>;
|
||
...
|
||
}
|
||
|
||
|
||
这样,我们的业务逻辑代码可以围绕着这个接口展开,而无需关心它具体的实现是来自 REST API,还是其它什么地方;也不用关心实现做没做 cache、有没有重传机制、具体都会返回什么样的错误(FriendCircleError 就已经提供了所有的出错可能)等等。
|
||
|
||
使用 trait 提供控制反转
|
||
|
||
继续看刚才的Engine 代码,在 Engine 和 T 之间通过 TryInto trait 进行了解耦,使得调用者可以灵活处理他们的 T:
|
||
|
||
pub trait Engine {
|
||
// 生成一个新的 engine
|
||
fn create<T>(data: T) -> Result<Self>
|
||
where
|
||
Self: Sized,
|
||
T: TryInto<Self>,
|
||
{
|
||
data.try_into()
|
||
.map_err(|_| anyhow!("failed to create engine"))
|
||
}
|
||
...
|
||
}
|
||
|
||
|
||
这里还体现了trait 在设计中,另一个很重要的作用,控制反转。
|
||
|
||
通过使用 trait,我们可以在设计底层库的时候告诉上层:我需要某个满足 trait X 的数据,因为我依赖这个数据实现的 trait X 方法来完成某些功能,但这个数据具体怎么实现,我不知道,也不关心。
|
||
|
||
刚才为 Engine 新构建的 create 方法。T 是实现 Engine 所需要的依赖,我们不知道属于类型 T 的 data 是如何在上下文中产生的,也不关心 T 具体是什么,只要 T 实现了 TryInto 即可。这就是典型的控制反转。
|
||
|
||
使用 trait 做控制反转另一个例子是[第 6 讲]中的 Dialect trait(代码):
|
||
|
||
pub trait Dialect: Debug + Any {
|
||
/// Determine if a character starts a quoted identifier. The default
|
||
/// implementation, accepting "double quoted" ids is both ANSI-compliant
|
||
/// and appropriate for most dialects (with the notable exception of
|
||
/// MySQL, MS SQL, and sqlite). You can accept one of characters listed
|
||
/// in `Word::matching_end_quote` here
|
||
fn is_delimited_identifier_start(&self, ch: char) -> bool {
|
||
ch == '"'
|
||
}
|
||
/// Determine if a character is a valid start character for an unquoted identifier
|
||
fn is_identifier_start(&self, ch: char) -> bool;
|
||
/// Determine if a character is a valid unquoted identifier character
|
||
fn is_identifier_part(&self, ch: char) -> bool;
|
||
}
|
||
|
||
|
||
我们只需要为自己的 SQL 方言实现 Dialect trait:
|
||
|
||
// 创建自己的 sql 方言。TyrDialect 支持 identifier 可以是简单的 url
|
||
impl Dialect for TyrDialect {
|
||
fn is_identifier_start(&self, ch: char) -> bool {
|
||
('a'..='z').contains(&ch) || ('A'..='Z').contains(&ch) || ch == '_'
|
||
}
|
||
|
||
// identifier 可以有 ':', '/', '?', '&', '='
|
||
fn is_identifier_part(&self, ch: char) -> bool {
|
||
('a'..='z').contains(&ch)
|
||
|| ('A'..='Z').contains(&ch)
|
||
|| ('0'..='9').contains(&ch)
|
||
|| [':', '/', '?', '&', '=', '-', '_', '.'].contains(&ch)
|
||
}
|
||
}
|
||
|
||
|
||
就可以让 sql parser 解析我们的 SQL 方言:
|
||
|
||
let ast = Parser::parse_sql(&TyrDialect::default(), sql.as_ref())?;
|
||
|
||
|
||
这就是 Dialect 这个看似简单的 trait 的强大用途。
|
||
|
||
对于我们这些使用者来说,通过Dialect trait,可以很方便地注入自己的解析函数,来提供我们的 SQL 方言的额外信息;对于 sqlparser 这个库的作者来说,通过 Dialect trait,他不必关心未来会有多少方言、每个方言长什么样子,只需要方言的作者告诉他如何 tokenize 一个标识符即可。
|
||
|
||
控制反转是架构中经常使用到的功能,它能够让调用者和被调用者之间的关系在某个时刻调转过来,被调用者反过来调用调用者提供的能力,二者协同完成一些事情。
|
||
|
||
比如 MapReduce 的架构:用于 map 的方法和用于 reduce 的方法是啥,MapReduce 的架构设计者并不清楚,但调用者可以把这些方法提供给 MapReduce 架构,由 MapReduce 架构在合适的时候进行调用。
|
||
|
||
当然,控制反转并非只能由 trait 来完成,但使用 trait 做控制反转会非常灵活,调用者和被调用者只需要关心它们之间的接口,而非具体的数据结构。
|
||
|
||
用 trait 实现 SOLID 原则
|
||
|
||
其实刚才介绍的用 trait 做控制反转,核心体现的就是面向对象设计时SOLID原则中的,依赖反转原则DIP,这是一个很重要的构建灵活系统的思想。
|
||
|
||
在做面向对象设计时,我们经常会探讨 SOLID 原则:
|
||
|
||
|
||
SRP:单一职责原则,是指每个模块应该只负责单一的功能,不应该让多个功能耦合在一起,而是应该将其组合在一起。
|
||
OCP:开闭原则,是指软件系统应该对修改关闭,而对扩展开放。
|
||
LSP:里氏替换原则,是指如果组件可替换,那么这些可替换的组件应该遵守相同的约束,或者说接口。
|
||
ISP:接口隔离原则,是指使用者只需要知道他们感兴趣的方法,而不该被迫了解和使用对他们来说无用的方法或者功能。
|
||
DIP:依赖反转原则,是指某些场合下底层代码应该依赖高层代码,而非高层代码去依赖底层代码。
|
||
|
||
|
||
虽然 Rust 不是一门面向对象语言,但这些思想都是通用的。
|
||
|
||
在过去的课程中,我一直强调 SRP 和 OCP。你看[第 6 讲]的 Fetch/Load trait,它们都只负责一个很简单的动作:
|
||
|
||
#[async_trait]
|
||
pub trait Fetch {
|
||
type Error;
|
||
async fn fetch(&self) -> Result<String, Self::Error>;
|
||
}
|
||
|
||
pub trait Load {
|
||
type Error;
|
||
fn load(self) -> Result<DataSet, Self::Error>;
|
||
}
|
||
|
||
|
||
以 Fetch 为例,我们先实现了 UrlFetcher,后来又根据需要,实现了 FileFetcher。
|
||
|
||
FileFetcher 的实现并不会对 UrlFetcher 的实现代码有任何影响,也就是说,在实现 FileFetcher 的时候,已有的所有实现了 Fetch 接口的代码都是稳定的,它们对修改是关闭的;同时,在实现 FileFetcher 的时候,我们扩展了系统的能力,使系统可以根据不同的前缀(from file:// 或者 from <http://>)进行不同的处理,这是对扩展开放。
|
||
|
||
前面提到的 SpecTransform/Engine trait,包括 [21 讲]中 KV server 里涉及的 CommandService trait:
|
||
|
||
/// 对 Command 的处理的抽象
|
||
pub trait CommandService {
|
||
/// 处理 Command,返回 Response
|
||
fn execute(self, store: &impl Storage) -> CommandResponse;
|
||
}
|
||
|
||
|
||
也是 SRP 和 OCP 原则的践行者。
|
||
|
||
LSP 里氏替换原则自不必说,我们本文中所有的内容都在践行通过使用接口,来使组件可替换。比如上文提到的 Engine trait,在 KV server 中我们使用的 Storage trait,都允许我们在不改变代码核心逻辑的前提下,替换其中的主要组件。
|
||
|
||
至于 ISP 接口隔离原则,我们目前撰写的 trait 都很简单,天然满足接口隔离原则。其实,大部分时候,当你的 trait 满足 SRP 单一职责原则时,它也满足接口隔离原则。
|
||
|
||
但在 Rust 中,有些 trait 的接口可能会比较庞杂,此时,如果我们想减轻调用者的负担,让它们能够在需要的时候才引入某些接口,可以使用 trait 的继承。比如 AsyncRead/AsyncWrite/Stream 和它们对应的 AsyncReadExt/AsyncWriteExt/StreamExt 等。这样,复杂的接口被不同的 trait 分担了并隔离开。
|
||
|
||
小结
|
||
|
||
接口设计是架构设计中最核心的环节。好的接口容易使用,很难误用,会让使用接口的人产生共鸣。当我们说一段代码读起来/写起来感觉很舒服,或者很不舒服、很冗长、很难看,这种感觉往往就来自于接口给人的感觉,我们可以妥善使用 trait 来降低甚至消除这种不舒服的感觉。
|
||
|
||
当我们的代码和其他人的代码共存时,接口在不同的组件之间就起到了桥接的作用。通过桥接,甚至可以把原本设计不好的代码,先用接口封装成我们希望的样子,然后实现这个简单的包装,之后再慢慢改动原先不好的设计。
|
||
|
||
这样,由于系统的其它部分使用新的接口处理,未来改动的影响被控制在很小的范围。在第 5 讲设计 thumbor 的时候我也提到,photon 库并不是一个设计良好的库,然而,通过 Engine trait 的桥接,未来即使我们 fork 一下 photon 库,对其大改,并不会影响 thumbor 的代码。
|
||
|
||
最后,在软件设计时,我们还要注意 SOLID 原则。基本上,好的 trait,设计一定是符合 SOLID 原则的,从 Rust 标准库的设计,以及之前讲到的 trait,结合今天的解读,想必你对此有了一定的认识。未来在使用 trait 构建你自己的接口时,你也可以将 SOLID 原则作为一个备忘清单,随时检查。
|
||
|
||
思考题
|
||
|
||
Rust 下有一个处理 Web 前端的库叫 yew。请 clone 到你本地,然后使用 ag 或者 rgrep(eat our own dogfood)查找一下所有的 trait 定义,看看这些 trait 被设计的目的和意义,并且着重阅读一下它最核心的 Component trait,思考几个问题:
|
||
|
||
|
||
Component trait 可以做 trait object 么?
|
||
|
||
关联类型 Message 和 Properties 的作用是什么?
|
||
|
||
作为使用者,该如何用 Component trait?它的 lifecycle 是什么样子的?
|
||
|
||
如果你之前有前端开发的经验,比较一下 React/Vue/Elm component 和 yew component 的区别?
|
||
|
||
yew on master via 🦀 v1.55.0
|
||
❯ rgrep “pub trait” “*/.rs”
|
||
examples/router/src/generator.rs
|
||
155:1 pub trait Generated: Sized {
|
||
packages/yew/src/html/component/mod.rs
|
||
|
||
42:1 pub trait Component: Sized + 'static {
|
||
|
||
packages/yew/src/html/component/properties.rs
|
||
|
||
6:1 pub trait Properties: PartialEq {
|
||
|
||
examples/boids/src/math.rs
|
||
128:1 pub trait WeightedMean: Sized {
|
||
152:1 pub trait Mean: Sized {
|
||
packages/yew/src/functional/mod.rs
|
||
|
||
69:1 pub trait FunctionProvider {
|
||
|
||
packages/yew/src/html/conversion.rs
|
||
|
||
5:1 pub trait ImplicitClone: Clone {}
|
||
18:1 pub trait IntoPropValue<T> {
|
||
|
||
packages/yew/src/html/listener/mod.rs
|
||
|
||
27:1 pub trait TargetCast
|
||
|
||
136:1 pub trait IntoEventCallback {
|
||
packages/yew/src/scheduler.rs
|
||
|
||
11:1 pub trait Runnable {
|
||
|
||
packages/yew-router/src/routable.rs
|
||
|
||
16:1 pub trait Routable: Sized + Clone {
|
||
|
||
packages/yew-agent/src/pool.rs
|
||
|
||
60:1 pub trait Dispatched: Agent + Sized + 'static {
|
||
78:1 pub trait Dispatchable {}
|
||
|
||
packages/yew/src/html/component/scope.rs
|
||
508:1 pub trait SendAsMessage {
|
||
packages/yew-macro/src/stringify.rs
|
||
|
||
16:1 pub trait Stringify {
|
||
|
||
packages/yew-agent/src/lib.rs
|
||
|
||
22:1 pub trait Agent: Sized + 'static {
|
||
82:1 pub trait Discoverer {
|
||
92:1 pub trait Bridge<AGN: Agent> {
|
||
98:1 pub trait Bridged: Agent + Sized + 'static {
|
||
|
||
packages/yew-agent/src/utils/store.rs
|
||
|
||
20:1 pub trait Store: Sized + 'static {
|
||
|
||
138:1 pub trait Bridgeable: Sized + ‘static {
|
||
packages/yew-macro/src/html_tree/mod.rs
|
||
178:1 pub trait ToNodeIterator {
|
||
packages/yew/src/virtual_dom/listeners.rs
|
||
|
||
42:1 pub trait Listener {
|
||
|
||
packages/yew-agent/src/worker/mod.rs
|
||
|
||
17:1 pub trait Threaded {
|
||
24:1 pub trait Packed {
|
||
|
||
|
||
|
||
感谢你的阅读,如果你觉得有收获,也欢迎你分享给身边的朋友,邀他一起讨论。今天你已经完成Rust学习的第25次打卡啦,我们下节课见!
|
||
|
||
|
||
|
||
|