首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >停止使用==和!=来判断浮点数是否相等

停止使用==和!=来判断浮点数是否相等

作者头像
郑子铭
发布2024-12-20 17:00:19
发布2024-12-20 17:00:19
30500
代码可运行
举报
运行总次数:0
代码可运行

写代码的小伙伴都知道,在计算机里,我们使用浮点数来表示小数.然而,由于浮点数在计算机中的表示方式,直接使用==!=来判断两个浮点数是否相等可能会导致意想不到的结果.

为什么不能直接比较浮点数

浮点数在计算机中是以二进制形式存储的,这种表示方式会导致精度问题.例如,十进制的小数0.1在二进制中是一个无限循环小数,计算机只能存储其近似值.因此,两个看似相等的浮点数在计算机中可能并不完全相等.

代码语言:javascript
代码运行次数:0
运行
复制

double a = 0.1 + 0.2;
double b = 0.3;
Console.WriteLine(a == b);// 输出: False

在上面的例子中,a 和 b 看似应该相等,但由于浮点数的精度问题,a 并不完全等于 b.

正确的比较方法

为了正确比较两个浮点数,我们可以使用一个小的误差范围(epsilon)来判断它们是否“足够接近”.这个误差范围可以根据具体的应用场景来选择.

代码语言:javascript
代码运行次数:0
运行
复制

double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 1e-10;
Console.WriteLine(Math.Abs(a - b)< epsilon);// 输出: True

在这个例子中,我们使用 Math.Abs(a - b) < epsilon 来判断 a 和 b 是否足够接近,从而避免了直接比较浮点数带来的问题.

进一步优化我们的比较算法

如何选择一个合理的 epsilon 值

选择合适的 epsilon 值(即比较浮点数时的精度)取决于具体的应用场景和浮点数的范围.以下是一些指导原则,可以帮助你选择合适的 epsilon 值:

  • 基于数值范围:
    • 对于较小的数值范围(例如 0 到 1),可以选择较小的 epsilon 值,例如 1e-6 或 1e-7.
    • 对于较大的数值范围(例如 0 到 1000),可以选择较大的 epsilon 值,例如 1e-5 或 1e-4.
  • 基于应用需求:
    • 如果应用对精度要求较高(例如科学计算或金融应用),应选择较小的 epsilon 值.
    • 如果应用对精度要求较低(例如图形渲染或物理模拟),可以选择较大的 epsilon 值.
  • 基于相对误差:
    • 使用相对误差来比较浮点数,可以避免数值范围对比较结果的影响.相对误差的计算公式如下:
代码语言:javascript
代码运行次数:0
运行
复制

/// <summary>
/// 比较两个浮点数是否相等.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="epsilon">精度默认: 0.000001</param>
/// <returns></returns>
publicstaticboolAreAlmostEqual(double a,double b,double epsilon = 1e-6)
{
return Math.Abs(a - b)< epsilon * Math.Max(Math.Abs(a), Math.Abs(b));
}

同时我们也衍生出如何判断浮点数 a 是否大于 b 的算法.

代码语言:javascript
代码运行次数:0
运行
复制

/// <summary>
/// 判断浮点数 a 是否大于浮点数 b.
/// </summary>
/// <param name="a">浮点数 a</param>
/// <param name="b">浮点数 b</param>
/// <param name="epsilon">精度默认: 0.000001</param>
/// <returns>如果 a 大于 b,返回 true;否则返回 false</returns>
publicstaticboolIsGreaterThan(double a,double b,double epsilon = 1e-6)
{
return a > b &&!AreAlmostEqual(a, b, epsilon);
}
让其支持跟多类型的浮点数

众所周知,浮点数除了 double 以外还有 float 以及 decimal 类型.所以我们需要将我们的类型进行扩展一下.

  • 于是我们得到了如下的代码.
代码语言:javascript
代码运行次数:0
运行
复制

/// <summary>
/// 比较两个浮点数是否相等.使用相对误差.
/// <remarks>
///     <para>
///     使用方式:
///     <code>
///     <![CDATA[
///     double num1 = 0.123456;
///     double num2 = 0.1234567;
///     bool result = AreAlmostEqual<double>(num1, num2);
///     Console.WriteLine(result); // Output: True
///   ]]>
///   </code>
///     </para>
/// </remarks>
/// </summary>
/// <typeparam name="T">浮点数类型: <see langword="float"/>, <see langword="double"/> 和 <see langword="decimal"/></typeparam>
/// <param name="a">浮点数1</param>
/// <param name="b">浮点数2</param>
/// <param name="epsilon">精度默认: 0.000001</param>
/// <returns></returns>
publicstaticboolAreAlmostEqual<T>(thisT a,T b,double epsilon = ModuleConstants.Epsilon)whereT:struct,IComparable,IConvertible,IFormattable
{
if(typeof(T)==typeof(float))
{
var floatA = a.ConvertTo<float>();
var floatB = b.ConvertTo<float>();
return Math.Abs(floatA - floatB)< epsilon * Math.Max(Math.Abs(floatA), Math.Abs(floatB));
}
if(typeof(T)==typeof(double))
{
var doubleA = a.ConvertTo<double>();
var doubleB = b.ConvertTo<double>();
return Math.Abs(doubleA - doubleB)< epsilon * Math.Max(Math.Abs(doubleA), Math.Abs(doubleB));
}
// ReSharper disable once InvertIf
if(typeof(T)==typeof(decimal))
{
var decimalA = a.ConvertTo<decimal>();
var decimalB = b.ConvertTo<decimal>();
return Math.Abs(decimalA - decimalB)< epsilon.ConvertTo<decimal>()* Math.Max(Math.Abs(decimalA), Math.Abs(decimalB));
}
thrownewNotSupportedException("Unsupported types, only the following types are supported: float, double, and decimal.");
}
/// <summary>
/// 判断浮点数 a 是否大于浮点数 b.
/// <remarks>
///     <para>
///     使用方式:
///     <code>
///     <![CDATA[
///     double num1 = 0.123456;
///     double num2 = 0.1234567;
///     bool result = IsGreaterThan<double>(num1, num2);
///     Console.WriteLine(result); // Output: True
///   ]]>
///   </code>
///     </para>
/// </remarks>
/// </summary>
/// <param name="a">浮点数 a</param>
/// <param name="b">浮点数 b</param>
/// <param name="epsilon">精度默认: 0.000001</param>
/// <returns>如果 a 大于 b,返回 true;否则返回 false</returns>
publicstaticboolIsGreaterThan<T>(thisT a,T b,double epsilon = ModuleConstants.Epsilon)whereT:struct,IComparable,IConvertible,IFormattable
{
if(typeof(T)==typeof(float))
{
var floatA = a.ConvertTo<float>();
var floatB = b.ConvertTo<float>();
return floatA > floatB &&!AreAlmostEqual(floatA, floatB, epsilon);
}
if(typeof(T)==typeof(double))
{
var doubleA = a.ConvertTo<double>();
var doubleB = b.ConvertTo<double>();
return doubleA > doubleB &&!AreAlmostEqual(doubleA, doubleB, epsilon);
}
// ReSharper disable once InvertIf
if(typeof(T)==typeof(decimal))
{
var decimalA = a.ConvertTo<decimal>();
var decimalB = b.ConvertTo<decimal>();
return decimalA > decimalB &&!AreAlmostEqual(decimalA, decimalB, epsilon);
}
thrownewNotSupportedException("Unsupported types, only the following types are supported: float, double, and decimal.");
}

上面的代码中我们使用了自定义的 ConverTo 函数,所以这里我们将该函数的扩展一起提供出来.该扩展在 EasilyNET.Core 的库中已存在定义.同样这两个扩展函数也都已经加入. 当然为了简化,我们可以使用.NET 框架提供的 Convert 类进行数据转换而不使用 ConvertTo.

代码语言:javascript
代码运行次数:0
运行
复制

using System.ComponentModel;
using System.Globalization;

// ReSharper disable UnusedMember.Global
// ReSharper disable MemberCanBePrivate.Global

namespace EasilyNET.Core.Misc;

/// <summary>
/// 类型是否可直接转换扩展
/// </summary>
publicstaticclassIConvertibleExtensions
{
/// <summary>
/// 是否是数字类型
/// </summary>
/// <param name="type">要检查的类型</param>
/// <returns>如果是数字类型,则为 <see langword="true"/>,否则为 <see langword="false"/></returns>
publicstaticboolIsNumeric(thisType type)=>
        Type.GetTypeCode(type)switch
{
            TypeCode.Byte    => true,
            TypeCode.SByte   => true,
            TypeCode.UInt16  => true,
            TypeCode.UInt32  => true,
            TypeCode.UInt64  => true,
            TypeCode.Int16   => true,
            TypeCode.Int32   => true,
            TypeCode.Int64   => true,
            TypeCode.Decimal => true,
            TypeCode.Double  => true,
            TypeCode.Single  => true,
            _                => false
};

/// <summary>
/// 类型直转
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <param name="value">要转换的值</param>
/// <returns>转换后的值</returns>
/// <exception cref="InvalidCastException">如果无法转换类型,则抛出异常</exception>
publicstaticT?ConvertTo<T>(thisIConvertible?value)whereT:IConvertible
{
if(value==null||Equals(value, DBNull.Value))returndefault;
var targetType =typeof(T);
var sourceType =value.GetType();
// 如果源类型和目标类型相同,直接返回
if(sourceType == targetType)return(T)value;
// 优化数字类型转换
if(targetType.IsNumeric())
{
return targetType switch
{
                _ when targetType ==typeof(byte)=>(T)(object)Convert.ToByte(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(sbyte)=>(T)(object)Convert.ToSByte(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(ushort)=>(T)(object)Convert.ToUInt16(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(uint)=>(T)(object)Convert.ToUInt32(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(ulong)=>(T)(object)Convert.ToUInt64(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(short)=>(T)(object)Convert.ToInt16(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(int)=>(T)(object)Convert.ToInt32(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(long)=>(T)(object)Convert.ToInt64(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(decimal)=>(T)(object)Convert.ToDecimal(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(double)=>(T)(object)Convert.ToDouble(value, CultureInfo.InvariantCulture),
                _ when targetType ==typeof(float)=>(T)(object)Convert.ToSingle(value, CultureInfo.InvariantCulture),
                _                                    =>thrownewInvalidCastException($"Cannot convert {value} to {targetType}")
};
}
// 优化枚举类型转换
if(targetType.IsEnum)return(T)Enum.Parse(targetType,value.ToString(CultureInfo.InvariantCulture));
// 优化可空类型转换
if(targetType.IsGenericType && targetType.GetGenericTypeDefinition()==typeof(Nullable<>))
{
var underlyingType = Nullable.GetUnderlyingType(targetType)!;
returnvalueisEnum enumValue ?(T)Enum.ToObject(underlyingType, enumValue):(T)value.ToType(underlyingType, CultureInfo.InvariantCulture);
}
// 添加对 Guid 类型的支持
if(targetType ==typeof(Guid))
{
return(T)(object)Guid.Parse(value.ToString(CultureInfo.InvariantCulture));
}
// 使用 TypeDescriptor 进行类型转换
var converter = TypeDescriptor.GetConverter(value);
if(converter.CanConvertTo(targetType))return(T?)converter.ConvertTo(value, targetType);
        converter = TypeDescriptor.GetConverter(targetType);
return converter.CanConvertFrom(sourceType)
?(T?)converter.ConvertFrom(value)
:thrownewInvalidCastException($"Cannot convert {value} to {targetType}");
}

/// <summary>
/// 类型直转
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <param name="value">要转换的值</param>
/// <param name="convertible">转换后的值</param>
/// <returns>是否转换成功</returns>
publicstaticboolTryConvertTo<T>(thisIConvertible?value,outT? convertible)whereT:IConvertible
{
        convertible =default;
try
{
            convertible =(T?)Convert.ChangeType(value,typeof(T), CultureInfo.InvariantCulture);
return true;
}
catch(Exception)
{
return false;
}
}
}

结论

在编写涉及浮点数比较的代码时,避免使用==和!=,而是使用一个小的误差范围来判断两个浮点数是否相等.这种方法可以帮助我们避免由于浮点数精度问题导致的错误判断.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么不能直接比较浮点数
  • 正确的比较方法
  • 进一步优化我们的比较算法
    • 如何选择一个合理的 epsilon 值
    • 让其支持跟多类型的浮点数
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档