Rust如何解决浮点数f32和f64类型无法作为元素插入HashSet和HashMap问题——Eq和Hash trait问题

我发现f32f64无法作为元素插入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需要实现 EqHashtrait,而 f32f64(一下统称“浮点数”)只实现了PartialEqtrait,而非Eq

EqPartialEq

Eqtrait 的签名:

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::NANf32::Infinite无法与正常浮点数进行比较,所以未实现Eqtrait。

然而,手动为浮点数实现Eq又会报错:

impl Eq for f32 {} // no way

绕开限制

不过我们可以绕开这一机制:用NewType模式:

#[derive(Debug)]
pub struct F32(f32);

然后手动为F32实现PartialEqEqHash

#![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实现DerefDeref,这样就可以自动解引用F32f32

//! #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没有实现 OrdPartialOrdtrait,因此无法进行比较和排序。你可以自己实现,这里就不展开了。

我们接着用同样的方法为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);
                }
            }
        )+
    };
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值