我发现f32
和f64
无法作为元素插入HashSet<T>
和HashMap<K, V>
:
let mut floats = HashSet::new();
floats.insert(0.0); // 必报错
让我们来看一下源代码:
impl<T, S> HashSet<T, S>
where
T: Eq + Hash,
S: HashBuilder,
{
...
}
可以看到,元素类型T
需要实现 Eq
和Hash
trait,而 f32
和f64
(一下统称“浮点数”)只实现了PartialEq
trait,而非Eq
。
Eq
和PartialEq
Eq
trait 的签名:
pub trait Eq: PartialEq {}
PartialEq
的签名:
pub trait PartialEq<Rhs = Self>
where
Rhs: ?Sized,
{
// Required method
fn eq(&self, other: &Rhs) -> bool;
}
Eq
是基于PartialEq
的,区别就是实现了Eq
的类型可以任意比较(如i32
),PartialEq
只能部分比较,换句话说,无法与某一些进行比较,就比如 f32::NAN
和f32::Infinite
无法与正常浮点数进行比较,所以未实现Eq
trait。
然而,手动为浮点数实现Eq
又会报错:
impl Eq for f32 {} // no way
绕开限制
不过我们可以绕开这一机制:用NewType模式:
#[derive(Debug)]
pub struct F32(f32);
然后手动为F32
实现PartialEq
、Eq
和Hash
:
#![allow(unused)]
use std::collections::HashSet;
use std::cmp::{PartialEq, Eq};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::convert::AsRef;
impl PartialEq for F32 {
#[inline]
fn eq(&self, other: &Self) -> bool {
// 浮点数大致分为两类:正常浮点数(非零、非NaN、非无限)和不正常浮点数
if self.0 == 0.0 { // 如果为零
other.0 == 0.0 // 另一个是否为零?
} else {
// 如果另一个不为零,如果有一方非正常,返回 false,反之则进行正常浮点数比较
(self.0.is_normal() && other.0.is_normal()) || self.0 == other.0
}
}
}
impl Eq for F32 {}
impl Hash for F32 {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
注意:封装浮点数是有性能损失的,所以以上实现的方法注解上
#[inline]
内联优化。
又不是#[inline(always)]
,不必担心内联不恰当问题,编译器自己会变通
可以为F32
实现Deref
和Deref
,这样就可以自动解引用F32
为f32
//! #Example
//! ```
//! assert_eq!(*F32(0.0), 0.0);
//! ```
impl Deref for F32 {
type Target = f32;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
//! #Example
//! ```
//! assert_eq!(F32(0.0).is_normal(), false);
//! ```
impl AsRef<f32> for F32 {
#[inline]
fn as_ref(&self) -> &f32 {
&self.0
}
}
这样子就大工道成了:
let mut floats = HashSet::new();
floats.insert(F32(0.0)); // OK
不过,这样还是有个缺点,由于
F32
没有实现Ord
、PartialOrd
trait,因此无法进行比较和排序。你可以自己实现,这里就不展开了。
我们接着用同样的方法为f64
做同样的事情,复制粘贴太低效了,我这里用展开宏:
float_impl! {
F32(f32),
F64(f64),
}
macro_rules! float_impl {
(
$(
$outer:ident($inner:ty),
)*
) => {
$(
#[derive(Debug)]
pub struct $outer($inner);
impl PartialEq for $outer {
#[inline]
fn eq(&self, other: &Self) -> bool {
if self.0 == 0.0 {
other.0 == 0.0
} else {
(self.0.is_normal() && other.0.is_normal()) || self.0 == other.0
}
}
}
impl Deref for $outer {
type Target = $inner;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<$inner> for $outer {
#[inline]
fn as_ref(&self) -> &$inner {
&self.0
}
}
impl Eq for $outer {}
impl Hash for $outer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
)+
};
}