Rust的trait系统有一个经常被谈论的特性,但我没有看到在应用代码中经常使用。为不属于你的类型实现你的traits。你可以在标准库中看到这一点,也可以在一些库中看到(hello itertools),但我看到开发者在编写应用程序时却不敢这么做。不过,这实在是太有趣了,太有用了!
我开始更多地定义和实现其他类型的traits,我感觉我的代码变得更清晰、更有意图。让我们看看我做了什么。
单行的traits
我的任务是写一个DNS解析器,阻止HTTP对localhost的调用。由于我在 hyper(你们都应该这样做),我实现了一个作为中间件的Tower服务。在这个中间件中,我做了实际的检查,以解决IP地址。
let addr = req.as_str();
let addr = (addr, 0).to_socket_addrs();
if let Ok(addresses) = addr {
for a in addresses {
if a.ip().eq(&Ipv4Addr::new(127, 0, 0, 1)) {
return Box::pin(async { Err(io::Error::from(ErrorKind::Other)) });
}
}
}
这还不错,但还有潜在的混乱空间,而且主要在条件中。
- 我们可能想检查更多可能解析到localhost的IP,例如IP
0.0.0.0。to_socket_addr可能不会解析到0.0.0.0,但同一段代码可能会在其他一些地方结束,这可能是个麻烦。 - 也许我们想把其他不是localhost的IP也排除在外。这个条件会有歧义。
- 我们忘记了IP v6地址的存在 🫢
所以,虽然这很好,但我想有一些东西,我为未来的事情做更多的准备。
我创建了一个IsLocalhost 特质。它定义了一个函数is_localhost ,它接收一个自身的引用并返回一个bool 。
pub(crate) trait IsLocalhost {
fn is_localhost(&self) -> bool;
}
在Rust的std::net ,有两个结构可以直接检查IP地址是否为localhost。Ipv4Addr 和Ipv6Addr 结构。
impl IsLocalhost for Ipv4Addr {
fn is_localhost(&self) -> bool {
Ipv4Addr::new(127, 0, 0, 1).eq(self) || Ipv4Addr::new(0, 0, 0, 0).eq(self)
}
}
impl IsLocalhost for Ipv6Addr {
fn is_localhost(&self) -> bool {
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).eq(self)
}
}
检查一个IP是否为localhost,正好发生在定义IP的地方。std::net 有一个枚举IpAddr ,用来区分V4和V6。让我们也为IpAddr ,实现IsLocalhost 。
impl IsLocalhost for IpAddr {
fn is_localhost(&self) -> bool {
match self {
IpAddr::V4(ref a) => a.is_localhost(),
IpAddr::V6(ref a) => a.is_localhost(),
}
}
}
有了这个枚举,我们就可以确保我们不会忘记V6的IP地址。吁。接着是SocketAddr ,这是我们从to_socket_addr 得到的原始结构。让我们也为它实现IsLocalhost 。
impl IsLocalhost for SocketAddr {
fn is_localhost(&self) -> bool {
self.ip().is_localhost()
}
}
很好!一路走来,都是乌龟。而且我们处理的是哪种结构并不重要。我们可以在任何地方检查localhost。
调用to_socket_addr 时,我们不是直接得到一个SocketAddr ,而是一个IntoIter<SocketAddr> ,沿着整个 IP 地址的路线往下走,直到我们到达实际的服务器。我们要检查这些is_localhost ,所以我们要看我们从迭代器得到的集合是否有localhost。又是一个特征!
pub(crate) trait HasLocalhost {
fn has_localhost(&mut self) -> bool;
}
impl HasLocalhost for IntoIter<SocketAddr> {
fn has_localhost(&mut self) -> bool {
self.any(|el| el.is_localhost())
}
}
就这样了。我非常喜欢最后一个实现,因为它使用了迭代器方法和闭包。在这个单行代码中,这变得如此美妙的可读性。
让我们改变一下原来的代码。
let addr = req.as_str();
let addr = (addr, 0).to_socket_addrs();
if let Ok(true) = addr.map(|mut el| el.has_localhost()) {
return Box::pin(async { Err(io::Error::from(ErrorKind::Other)) });
}
变化不大,但发生的事情变得非常明显。在条件中说,我们正在检查localhost,而不是其他。我们要解决的问题变得清晰了。另外,我们也可以在其他地方做localhost检查,因为结构给了我们这些信息。❤️
懒惰的打印机
我经常在其他类型上使用带有实现的单行特质。这是我在开发时经常使用的一个实用特质。我是从JavaScript来的,所以我最可靠的调试器是stdout。我经常做Debug prints,但我总是很笨拙地写println!("{:?}", whatever); 。这就需要一个新的特质!
trait Print {
fn print(&self);
}
...我为每个实现Debug 的类型都实现了这个特性。
impl<T: std::fmt::Debug> Print for T {
fn print(&self) {
println!("{:?}", self);
}
}
太棒了!
"Hello, world".print();
vec![0, 1, 2, 3, 4].print();
"You get the idea".print()
多么好的一个工具。小小的特性让我的生活更轻松。