写代码的小伙伴都知道,在计算机里,我们使用浮点数来表示小数.然而,由于浮点数在计算机中的表示方式,直接使用
==
和!=
来判断两个浮点数是否相等可能会导致意想不到的结果.
浮点数在计算机中是以二进制形式存储的,这种表示方式会导致精度问题.例如,十进制的小数0.1
在二进制中是一个无限循环小数,计算机只能存储其近似值.因此,两个看似相等的浮点数在计算机中可能并不完全相等.
double a = 0.1 + 0.2;
double b = 0.3;
Console.WriteLine(a == b);// 输出: False
在上面的例子中,a 和 b 看似应该相等,但由于浮点数的精度问题,a 并不完全等于 b.
为了正确比较两个浮点数,我们可以使用一个小的误差范围(epsilon)来判断它们是否“足够接近”.这个误差范围可以根据具体的应用场景来选择.
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 值:
/// <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 的算法.
/// <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
类型.所以我们需要将我们的类型进行扩展一下.
/// <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
.
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;
}
}
}
在编写涉及浮点数比较的代码时,避免使用==和!=,而是使用一个小的误差范围来判断两个浮点数是否相等.这种方法可以帮助我们避免由于浮点数精度问题导致的错误判断.