Rust的trait系统的使用实例教程

350 阅读4分钟

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,例如IP0.0.0.0to_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。Ipv4AddrIpv6Addr 结构。

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()

多么好的一个工具。小小的特性让我的生活更轻松。