也许你已经学习了标准库提供的 String 类型,这是一个 UTF-8 编码的可增长字符串。该类型的结构为:

pub struct String {vec: Vec<u8>,
}

UTF-8 的特性决定了你无法索引 String

let s = "hello";println!("The first letter of s is {}", s[0]); // ERROR!!!

虽然你能切片 String,但对非 ascii 字符,切片是非常危险的,你很难确定边界在哪里。

fn main() {let s = "玩转 Rust 类型系统".to_string();println!("{:?}", &s[7..11]); // Rustprintln!("{:?}", &s[12..13]); // thread 'main' panicked at 'byte index 13 is not a char boundary; it is inside '类' (bytes 12..15) of `玩转 Rust 类型系统`', src/main.rs:5:23
}

怎么办? 你可以自己造一个支持索引、切片安全的字符串类型。Rust 的类型系统允许你这么做。

你可以将 String 内部的 Vec<u8> 换成 Vec<char>,再实现一下 From 这个 tarit。该 tarit 用于类型转换,这里是将 &str 转换为 MyString:

#[derive(Debug)]
pub struct MyString {vec: Vec<char>
}impl<'a> From<&'a str> for MyString {fn from(string: &'a str) -> Self {Self {vec: string.chars().collect()}}
}

这时候你已经可以玩耍你的 MyString 了。

fn main() {let s: MyString = MyString::from("玩转 Rust 类型系统");println!("{:?}", s); // MyString { vec: ['也', '说', ' ', 'R', 'u', 's', 't', ' ', '类', '型', '系', '统'] }
}

上面的 debug 输出可能并不是你想要的,你可以自己实现 Debug

use std::fmt;pub struct MyString {vec: Vec<char>
}impl fmt::Debug for MyString {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {for c in &self.vec {f.write_fmt(format_args!("{}", c))?;}Ok(())}
}

这时候再次运行,可能会输出你想要的结果。你可以定制 Debug 的输出结果:

for (i, c) in self.vec.iter().enumerate() {f.write_fmt(format_args!("{}:{}, ", i, c))?;
}

上面代码将会输出:

0:也, 1:说, 2: , 3:R, 4:u, 5:s, 6:t, 7: , 8:类, 9:型, 10:系, 11:统,

得益于 Rust 的宏系统,你可以简写上面的一部分:

for (i, c) in self.vec.iter().enumerate() {write!(f, "{}:{}, ", i, c)?;
}

write! 会帮你调用 write_fmtwrite_fmt 通常来自 fmt::Write 和 io::Write。比如说,Stdout 实现了 io::Write,你可以用 write! 将字符“写”到标准输出:

fn main() {use std::io::Write;write!(std::io::stdout(), "{:?}n", "玩转 Rust 类型系统").unwrap(); // "玩转 Rust 类型系统"
}

知机识变,得心应手

如果你现在 MyString 所在的 crate 的目录中执行 cargo doc --open 查看 MyString 的文档,会发现除了你上面实现的 impl Debug for MyStringimpl<'a> From<&'a str> for MyString,在 “Blanket Implementations” 这一节中编译器还帮你“一揽子实现”了几个 Tarit。我们现在感兴趣的是:

impl<T, U> Into<U> for T
whereU: From<T>,
{fn into(self) -> U {U::from(self)}
}

Into 是帮助你进行类型转换的。你可以改一下上面创建 MyString 的代码:

fn main() {// let s: MyString = MyString::from("玩转 Rust 类型系统");let s: MyString = "玩转 Rust 类型系统".into();println!("{:?}", s); // 玩转 Rust 类型系统
}

Into 在某些时候非常有用,比如我想实现一个能容纳多种类型的容器:

#[derive(Debug)]
pub enum Value {F32(f32),I32(i32),String(String)
}#[derive(Debug)]
pub struct MyColl {vec: Vec<Value>
}impl MyColl {pub fn new() -> Self {Self {vec: Vec::new()}}pub fn push(&mut self, v: impl Into<Value>) {self.vec.push(v.into())}
}impl From<f32> for Value {fn from(f: f32) -> Value {Value::F32(f)}
}impl From<i32> for Value {fn from(i: i32) -> Value {Value::I32(i)}
}impl From<String> for Value {fn from(s: String) -> Value {Value::String(s)}
}

向该集合 push 元素时,达到了类似于动态语言的效果:

fn main() {let mut coll = MyColl::new();coll.push(123i32);coll.push(456f32);coll.push("hello".to_string());
}

From 类似的还有 FromStr。标准库为一些类型实现了 FromStr,比如:

impl FromStr for f32
impl FromStr for Ipv4Addr

借助于 FromStr,你可以方便的将字符串转换为你需要的类型:

use std::str::FromStr;
use std::net::Ipv4Addr;fn main() {let f = f32::from_str("1.23").unwrap();println!("{:?}", f); // 1.23let addr = Ipv4Addr::from_str("127.0.0.1").unwrap();println!("{:?}", addr); // 127.0.0.1
}

就这? 下面这段代码才是点睛之笔:

impl str {...pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {FromStr::from_str(self)}...
}

有了 parse,你就可以:

use std::net::Ipv4Addr;fn main() {let f: f32 = "1.23".parse().unwrap();println!("{:?}", f); // 1.23let addr: Ipv4Addr = "127.0.0.1".parse().unwrap();println!("{:?}", addr); // 127.0.0.1
}

需要说明的是,From, Into, FromStrparse 都是在库的层面实现了,并没有在编译层面“打洞”。掌握了这些,你可以举一反三构建你自己的类型体系。

机由己发,力从人借

我们把目光转移到 MyString,为 MyString 实现 Deref 和 DerefMut 这两个 tarit:

use std::ops::{Deref, DerefMut};impl Deref for MyString {type Target = Vec<char>;fn deref(&self) -> &Self::Target {&self.vec}
}impl DerefMut for MyString {fn deref_mut(&mut self) -> &mut Self::Target {&mut self.vec}
}

执行 cargo doc 后你会发现 Methods from Deref<Target = Vec<char>> 这一节,像是从 Vec “继承”了一些方法。不过 Rust 可没有继承,这也是利用 tarit。

fn main() {let mut s: MyString = "玩转 Rust 类型系统".into();println!("{:?}", s); // 玩转 Rust 类型系统println!("{:?}", s[0]); // 玩println!("{:?}", s[9]); // 型println!("{:?}", &s[3..7]); // ['R', 'u', 's', 't']s[0] = '哇';println!("{:?}", s); // 哇转 Rust 类型系统
}

现在你已经可以索引和切片 MyString,如果你对从 Vec “借”来的东西不满意,可以自己去实现。

他山之石,可以攻玉

我们再为 MyString 实现一下代器(Iterator)。要实现 Iterator 非常简单,

trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>;
}

只需要实现 next 方法即可。这里还需要一个变量去记录迭代位置。

impl MyString {pub fn iter(&self) -> Iter {Iter {s: &self.vec,pos: 0}}
}use std::iter::{Iterator, IntoIterator};pub struct Iter<'a> {s: &'a Vec<char>,pos: usize
}impl<'a> Iterator for Iter<'a> {type Item = &'a char;fn next(&mut self) -> Option<Self::Item> {self.s.get(self.pos).map(|c| { self.pos += 1; c })}
}

现在你就可以遍历字符串了:

for c in s.iter() {println!("{:?}", c);
}

如果你想使用 for c in &s 这种形式,还需要为 MyString 实现 IntoIterator 这个 trait:

impl<'a> IntoIterator for &'a MyString {type Item = &'a char;type IntoIter = Iter<'a>;fn into_iter(self) -> Self::IntoIter {self.iter()}
}

不过,你也可以“借用” sliceIter。由于 IterMutIntoIter 实现复杂,也可以借来一用。

IterIterMutIntoIter 的命名时约定俗成的。

最终的代码为:

impl MyString {pub fn iter(&self) -> std::slice::Iter<char> {self.vec.iter()}pub fn iter_mut(&mut self) -> std::slice::IterMut<char> {self.vec.iter_mut()}pub fn into_iter(self) -> std::vec::IntoIter<char> {self.vec.into_iter()}
}use std::iter::{Iterator, IntoIterator};impl<'a> IntoIterator for &'a MyString {type Item = &'a char;type IntoIter = std::slice::Iter<'a, char>;fn into_iter(self) -> Self::IntoIter {self.vec.iter()}
}impl<'a> IntoIterator for &'a mut MyString {type Item = &'a mut char;type IntoIter = std::slice::IterMut<'a, char>;fn into_iter(self) -> Self::IntoIter {self.vec.iter_mut()}
}impl IntoIterator for MyString {type Item = char;type IntoIter = std::vec::IntoIter<char>;fn into_iter(self) -> Self::IntoIter {self.vec.into_iter()}
}

现在你可以用下面三种方式遍历 MyString:

for c in &s {println!("{:?}", c);
}for c in &mut s {println!("{:?}", c);
}for c in s {println!("{:?}", c);
}

这就完了?并没有!这一节的重点并不是教你如何实现 Iterator,而是想说 Iterator 下面有众多方法:

pub trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>;fn size_hint(&self) -> (usize, Option<usize>) { ... }fn count(self) -> usize { ... }fn last(self) -> Option<Self::Item> { ... }fn advance_by(&mut self, n: usize) -> Result<(), usize> { ... }fn nth(&mut self, n: usize) -> Option<Self::Item> { ... }fn step_by(self, step: usize) -> StepBy<Self> { ... }fn chain<U>(self, other: U) -> Chain<Self, <U as IntoIterator>::IntoIter>whereU: IntoIterator<Item = Self::Item>,{ ... }fn zip<U>(self, other: U) -> Zip<Self, <U as IntoIterator>::IntoIter>ⓘwhereU: IntoIterator,{ ... }fn map<B, F>(self, f: F) -> Map<Self, F>ⓘwhereF: FnMut(Self::Item) -> B,{ ... }fn for_each<F>(self, f: F)whereF: FnMut(Self::Item),{ ... }...

你只要实现 next 方法,就可以免费、无偿使用这些方法。与 Iterator 类似的还有 io::Write 和 io::Read 等。

这种思路虽然在其他语言中也经常见到,但在 Rust 中的实现是非常清晰明了的。

trait Log {fn print(&self, s: &str);fn info(&self, s: &str) {self.print(&format!("INFO: {}", s))}fn debug(&self, s: &str) {self.print(&format!("DEBUG: {}", s))}fn error(&self, s: &str) {self.print(&format!("ERROR: {}", s))}
}struct MyPrint;impl Log for MyPrint {fn print(&self, s: &str) {println!("{}", s);}
}fn main() {let print = MyPrint;print.info("hello"); // INFO: helloprint.debug("hello"); // DEBUG: helloprint.error("hello"); // ERROR: hello
}

上面的代码演示了如何实现一个简易的日志组件。

本文到这里就结束了。我并不想把 trait 的功能一一列举一遍,而是以实现 MyString 为导向,选取了几个比较有趣的例子。

10玩rust_有趣的 Rust 类型系统: Trait相关推荐

  1. 10玩rust_C++工程师的Rust迁移之路(5)- 继承与组合 - 下

    2020-11-25 更新: 修正了C++ 20中的concept语法 在上一篇文章 https://zhuanlan.zhihu.com/p/76740667 中,我介绍多态.静态分发和动态分发的概 ...

  2. 墙裂推荐!10个非常有趣的Python库!

    这是"菜鸟学Python",第"500"篇原创 大家好,我是菜鸟哥!学Python就等你啦! 爆款文案 1).卧槽!Pdf转Word用Python轻松搞定! 2 ...

  3. 推荐10款超级有趣的HTML5小游戏

    HTML5的发展速度比任何人的都想像都要更快.更加强大有效的和专业的解决方案已经被开发......甚至在游戏世界中!这里跟大家分享有10款超级趣味的HTML5游戏,希望大家能够喜欢! Kern Typ ...

  4. 真心话大冒险HTML5小游戏,真心话大冒险游戏怎么玩才有趣

    说到真心话大冒险,这应该是年轻人都喜欢玩的一个游戏,下面小编为大家整理了关于真心话大冒险的游戏玩法,欢迎大家阅读. 真心话大冒险游戏怎么玩才有趣 包厢里几个人围坐在一起,分散着坐在包厢里的各个角落,会 ...

  5. 10个非常有趣的Python库,可以玩上一整天

    Python语法简单,功能强大,可以干很多事情,原因就是因为它有强大的库支持,有很多很多现成的轮子可以用,你只要负责搭建应用即可. 1. Python假消息生产器 这个库叫Faker很有趣,可以创建一 ...

  6. Linux 还能这么玩,10 个非常有趣的命令!

    Linux当中有很多比较有趣的命令,可以动手看看,很简单的. 1.rev命令 一行接一行地颠倒所输入的字符串. 运行: $rev 如输入:shiyanlou shiyanlou 2.asciiview ...

  7. 10 条真心有趣的 Linux 命令

    在终端工作是一件很有趣的事情.今天,我们将会列举一些有趣得为你带来欢笑的Linux命令. 1. rev 创建一个文件,在文件里面输入几个单词,rev命令会将你写的东西反转输出到控制台. # rev  ...

  8. Rust:Trait 详解

    Rust语言里不同的type(比如 struct, enum等)可以调用的函数主要包括本身实现的方法.此外,Rust也提供了trait来定义不同type所需的"common behavior ...

  9. Rust 常用 trait 实现

    1. Eq & PartialEq 符号:==.!= 区别:Eq 相比于 PartialEq 还需额外满足反身性,即 a == a.对于浮点类型,Rust 只实现了 PartialEq 而不是 ...

最新文章

  1. 张锋在美赢得“基因剪刀”专利判决!此前与诺奖得主纠纷多年
  2. 威胁报告:mDNS 反射式 DDoS 攻击
  3. tableau应用实战案例(二)-TABLEAU调用中国地图和Python获取地址的经纬度
  4. WebAssembly生态将完善网络安全性
  5. 大数据_Hbase-数据存储介绍---Hbase工作笔记0002
  6. 学无止境,我还在进步
  7. 阿特拉斯开发协议--与ATLAS 扭力控制器交互
  8. Adobe FLASH CS6 安装错误解决方法
  9. ESP32 485光照度
  10. 如何查看hadoop集群的四个配置文件(core-site.xml hdfs-site.xml mapred-site.xml yarn-site.xml )
  11. dell服务器硬件检测cable,DELL服务器硬件报错解决方法——错误代码寄解决和处理办法...
  12. java后台导出pdf,基础用法和样例
  13. win10中查看wifi密码
  14. Ceph Crush-Map与Ceph调优及其日常管理
  15. 计算机域名是什么域名?
  16. 太原工业学院计算机实训中心,法学实训实验中心
  17. 一沉担千斤,一默解万愁
  18. 一个程序员老总的年终总结2010版
  19. Converged Containers and Applications
  20. 脉冲编码器的工作原理

热门文章

  1. python符号求导
  2. 解释为什么用梯度下降而不是直接求导数为0的解
  3. python if elif else 区别
  4. 深度学总结:skip-gram pytorch实现
  5. MyBatis批量插入几千条数据慎用foreach
  6. TVM将深度学习模型编译为WebGL
  7. ARMed解决方案对DSP的战争
  8. 为什么edge AI是一个无需大脑的人
  9. 计算机视觉一些项目实战技术
  10. FCN与U-Net语义分割算法