10玩rust_有趣的 Rust 类型系统: Trait
也许你已经学习了标准库提供的 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_fmt
,write_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 MyString
和 impl<'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
, FromStr
和 parse
都是在库的层面实现了,并没有在编译层面“打洞”。掌握了这些,你可以举一反三构建你自己的类型体系。
机由己发,力从人借
我们把目光转移到 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()}
}
不过,你也可以“借用” slice
的 Iter
。由于 IterMut
和 IntoIter
实现复杂,也可以借来一用。
Iter
,IterMut
和IntoIter
的命名时约定俗成的。
最终的代码为:
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相关推荐
- 10玩rust_C++工程师的Rust迁移之路(5)- 继承与组合 - 下
2020-11-25 更新: 修正了C++ 20中的concept语法 在上一篇文章 https://zhuanlan.zhihu.com/p/76740667 中,我介绍多态.静态分发和动态分发的概 ...
- 墙裂推荐!10个非常有趣的Python库!
这是"菜鸟学Python",第"500"篇原创 大家好,我是菜鸟哥!学Python就等你啦! 爆款文案 1).卧槽!Pdf转Word用Python轻松搞定! 2 ...
- 推荐10款超级有趣的HTML5小游戏
HTML5的发展速度比任何人的都想像都要更快.更加强大有效的和专业的解决方案已经被开发......甚至在游戏世界中!这里跟大家分享有10款超级趣味的HTML5游戏,希望大家能够喜欢! Kern Typ ...
- 真心话大冒险HTML5小游戏,真心话大冒险游戏怎么玩才有趣
说到真心话大冒险,这应该是年轻人都喜欢玩的一个游戏,下面小编为大家整理了关于真心话大冒险的游戏玩法,欢迎大家阅读. 真心话大冒险游戏怎么玩才有趣 包厢里几个人围坐在一起,分散着坐在包厢里的各个角落,会 ...
- 10个非常有趣的Python库,可以玩上一整天
Python语法简单,功能强大,可以干很多事情,原因就是因为它有强大的库支持,有很多很多现成的轮子可以用,你只要负责搭建应用即可. 1. Python假消息生产器 这个库叫Faker很有趣,可以创建一 ...
- Linux 还能这么玩,10 个非常有趣的命令!
Linux当中有很多比较有趣的命令,可以动手看看,很简单的. 1.rev命令 一行接一行地颠倒所输入的字符串. 运行: $rev 如输入:shiyanlou shiyanlou 2.asciiview ...
- 10 条真心有趣的 Linux 命令
在终端工作是一件很有趣的事情.今天,我们将会列举一些有趣得为你带来欢笑的Linux命令. 1. rev 创建一个文件,在文件里面输入几个单词,rev命令会将你写的东西反转输出到控制台. # rev ...
- Rust:Trait 详解
Rust语言里不同的type(比如 struct, enum等)可以调用的函数主要包括本身实现的方法.此外,Rust也提供了trait来定义不同type所需的"common behavior ...
- Rust 常用 trait 实现
1. Eq & PartialEq 符号:==.!= 区别:Eq 相比于 PartialEq 还需额外满足反身性,即 a == a.对于浮点类型,Rust 只实现了 PartialEq 而不是 ...
最新文章
- 张锋在美赢得“基因剪刀”专利判决!此前与诺奖得主纠纷多年
- 威胁报告:mDNS 反射式 DDoS 攻击
- tableau应用实战案例(二)-TABLEAU调用中国地图和Python获取地址的经纬度
- WebAssembly生态将完善网络安全性
- 大数据_Hbase-数据存储介绍---Hbase工作笔记0002
- 学无止境,我还在进步
- 阿特拉斯开发协议--与ATLAS 扭力控制器交互
- Adobe FLASH CS6 安装错误解决方法
- ESP32 485光照度
- 如何查看hadoop集群的四个配置文件(core-site.xml hdfs-site.xml mapred-site.xml yarn-site.xml )
- dell服务器硬件检测cable,DELL服务器硬件报错解决方法——错误代码寄解决和处理办法...
- java后台导出pdf,基础用法和样例
- win10中查看wifi密码
- Ceph Crush-Map与Ceph调优及其日常管理
- 计算机域名是什么域名?
- 太原工业学院计算机实训中心,法学实训实验中心
- 一沉担千斤,一默解万愁
- 一个程序员老总的年终总结2010版
- Converged Containers and Applications
- 脉冲编码器的工作原理