C# 数值计算与精度
大约 12 分钟约 3745 字
C# 数值计算与精度
简介
C# 数值类型包括整数、浮点数、Decimal 和 BigInteger。理解各类型的精度范围、溢出行为和计算陷阱,有助于避免金融计算和科学计算中的精度丢失。在金融系统中,一个浮点精度错误可能导致账目不平;在科学计算中,累积误差可能让结果完全失真;在高并发系统中,整数溢出可能引发安全漏洞。本文将从类型体系、精度陷阱、溢出机制、性能优化和最佳实践等维度全面解析 C# 数值计算。
特点
类型体系总览
整数类型
| 类型 | 位数 | 范围 | 默认值 | 后缀 |
|---|---|---|---|---|
| sbyte | 8 | -128 ~ 127 | 0 | - |
| byte | 8 | 0 ~ 255 | 0 | - |
| short | 16 | -32,768 ~ 32,767 | 0 | - |
| ushort | 16 | 0 ~ 65,535 | 0 | - |
| int | 32 | -2,147,483,648 ~ 2,147,483,647 | 0 | 无 |
| uint | 32 | 0 ~ 4,294,967,295 | 0 | U/u |
| long | 64 | -9.2e18 ~ 9.2e18 | 0L | L/l |
| ulong | 64 | 0 ~ 1.8e19 | 0 | UL/ul |
| nint | 平台 | 平台相关 | 0 | - |
| nuint | 平台 | 平台相关 | 0 | - |
浮点类型
| 类型 | 位数 | 有效数字 | 范围 | 后缀 |
|---|---|---|---|---|
| float | 32 | ~7 位 | 1.5e-45 ~ 3.4e38 | F/f |
| double | 64 | ~15 位 | 5.0e-324 ~ 1.7e308 | D/d |
| decimal | 128 | ~28-29 位 | 1.0e-28 ~ 7.9e28 | M/m |
实现
浮点精度陷阱与解决
// ❌ 浮点精度问题
double a = 0.1 + 0.2;
Console.WriteLine(a == 0.3); // False! 0.30000000000000004
Console.WriteLine($"{a:R}"); // 0.30000000000000004
// 原因:0.1 在二进制中是无限循环小数,无法精确表示
// 0.1 (decimal) ≈ 0.0001100110011... (binary)
// ✅ 解决方案1:使用 decimal(28-29 位精度)
decimal price = 0.1m + 0.2m;
Console.WriteLine(price == 0.3m); // True
// ✅ 解决方案2:使用容差比较
public static bool AlmostEqual(double a, double b, double epsilon = 1e-10)
=> Math.Abs(a - b) < epsilon;
// ✅ 解决方案3:使用整数运算(金融场景推荐)
// 用"分"代替"元",避免浮点运算
int totalCents = 100 + 200; // 1元 + 2元 = 3元
decimal totalYuan = totalCents / 100m;
Console.WriteLine(totalYuan); // 3.00更多浮点陷阱
// 陷阱1:大数加小数丢失精度
double big = 1_000_000_000.0;
double small = 0.000_000_001;
double result = big + small;
Console.WriteLine(result == big); // True! small 被完全丢失
// 陷阱2:累积误差
double sum = 0.0;
for (int i = 0; i < 1_000_000; i++)
{
sum += 0.000_001;
}
Console.WriteLine(sum); // 0.9999999999986123,不是 1.0
// 解决:Kahan 求和算法,减少累积误差
double kahanSum = 0.0;
double compensation = 0.0;
for (int i = 0; i < 1_000_000; i++)
{
double y = 0.000_001 - compensation;
double t = kahanSum + y;
compensation = (t - kahanSum) - y;
kahanSum = t;
}
Console.WriteLine(kahanSum); // 1.0,精度显著提高金融计算最佳实践
// ✅ 金融计算推荐使用 decimal
decimal subtotal = 99.99m;
decimal tax = subtotal * 0.08m; // 7.9992
decimal total = Math.Round(subtotal + tax, 2); // 107.99
// MidpointRounding 控制四舍五入方式
decimal value = 2.5m;
Console.WriteLine(Math.Round(value, 0, MidpointRounding.AwayFromZero)); // 3
Console.WriteLine(Math.Round(value, 0, MidpointRounding.ToEven)); // 2(银行家舍入)
// ✅ 金额计算工具类
public static class MoneyCalculator
{
public static decimal Add(params decimal[] amounts)
{
decimal sum = 0m;
foreach (var amount in amounts)
sum += Math.Round(amount, 2);
return Math.Round(sum, 2);
}
public static (decimal Tax, decimal Total) CalculateTotal(decimal subtotal, decimal taxRate)
{
var tax = Math.Round(subtotal * taxRate, 2, MidpointRounding.AwayFromZero);
var total = subtotal + tax;
return (tax, total);
}
}溢出检查
// unchecked(默认)— 溢出无异常,静默截断
int max = int.MaxValue;
int overflow = max + 1; // -2147483648(无异常!)
// checked — 溢出抛出 OverflowException
try
{
checked { int result = max + 1; }
}
catch (OverflowException ex)
{
Console.WriteLine($"溢出捕获: {ex.Message}");
}
// 项目级开启 checked
// 在 .csproj 中开启: CheckForOverflowUnderflow = true
// 对特定代码块关闭 checked
checked
{
int a = int.MaxValue;
unchecked
{
int b = a + 1; // 不抛异常
}
}类型转换与精度丢失
// 隐式转换:小范围到大范围,安全
int i = 100;
long l = i; // 安全
double d = i; // 安全
float f = i; // 安全(但可能丢失精度)
// 显式转换(强制转换):大范围到小范围,可能丢失
long big = 300_000_000_0;
int narrowed = (int)big; // 溢出,结果不确定
// 浮点到整数:截断小数部分
double pi = 3.14159;
int intPi = (int)pi; // 3(截断,不是四舍五入)
int roundedPi = (int)Math.Round(pi); // 3(四舍五入)
// Convert 方法提供更安全的转换
int converted = Convert.ToInt32(3.9); // 4(四舍五入)
int overflowCheck = Convert.ToInt32(big); // 抛出 OverflowException
// 数值字符串解析
bool success = int.TryParse("12345", out int parsed); // 推荐,不抛异常
int parsed2 = int.Parse("12345"); // 格式不对会抛异常
int parsed3 = int.Parse("12345", NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);BigInteger 大数运算
using System.Numerics;
// 阶乘
BigInteger Factorial(int n)
{
BigInteger result = 1;
for (int i = 2; i <= n; i++) result *= i;
return result;
}
Console.WriteLine($"100! = {Factorial(100)}");
// 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
// 大数运算
var a = BigInteger.Parse("123456789012345678901234567890");
var b = BigInteger.Parse("987654321098765432109876543210");
Console.WriteLine($"加法: {a + b}");
Console.WriteLine($"乘法: {a * b}");
Console.WriteLine($"幂运算: {BigInteger.Pow(2, 100)}");
// 大数取模(密码学常用)
BigInteger ModPow(BigInteger value, BigInteger exponent, BigInteger modulus)
=> BigInteger.ModPow(value, exponent, modulus);
// 最大公约数
BigInteger gcd = BigInteger.GreatestCommonDivisor(a, b);
Console.WriteLine($"GCD: {gcd}");数值 API
using System.Numerics;
// Vector<T> — SIMD 向量运算(硬件加速)
var v1 = new Vector<float>(new float[] { 1, 2, 3, 4, 5, 6, 7, 8 });
var v2 = new Vector<float>(new float[] { 8, 7, 6, 5, 4, 3, 2, 1 });
var sum = v1 + v2; // SIMD 加速的向量加法
var dot = Vector.Dot(v1, v2); // 点积
// 向量化计算示例:两个数组的逐元素相加
float[] arr1 = Enumerable.Range(0, 10000).Select(i => (float)i).ToArray();
float[] arr2 = Enumerable.Range(0, 10000).Select(i => (float)(i * 2)).ToArray();
float[] result = new float[10000];
// SIMD 向量化处理(比逐元素循环快 4-8 倍)
int vectorSize = Vector<float>.Count;
int i = 0;
for (; i <= arr1.Length - vectorSize; i += vectorSize)
{
var va = new Vector<float>(arr1, i);
var vb = new Vector<float>(arr2, i);
(va + vb).CopyTo(result, i);
}
// 处理剩余元素
for (; i < arr1.Length; i++)
result[i] = arr1[i] + arr2[i];
// BitOperations — 高性能位操作
uint value = 0b_0000_0000_0000_0000_0000_0000_0001_0000;
int bitPos = BitOperations.TrailingZeroCount(value); // 4
int log2 = BitOperations.Log2(value); // 4
uint rotated = BitOperations.RotateRight(value, 2);
int popCount = BitOperations.PopCount(value); // 1(置位位数)
// MathF — float 版本数学函数(避免 double 隐式转换)
float sinF = MathF.Sin(MathF.PI / 4);
float sqrtF = MathF.Sqrt(2.0f);
float absF = MathF.Abs(-3.14f);
float maxF = MathF.Max(1.0f, 2.0f);精确比较工具
public static class NumericComparer
{
// 浮点数相对容差比较(适用于科学计算)
public static bool ApproximatelyEqual(double a, double b, double relativeEpsilon = 1e-8)
{
if (a == b) return true; // 处理 0 == 0
double absA = Math.Abs(a);
double absB = Math.Abs(b);
double diff = Math.Abs(a - b);
if (a == 0 || b == 0 || diff < double.MinValue)
return diff < relativeEpsilon;
return diff / Math.Min(absA + absB, double.MaxValue) < relativeEpsilon;
}
// 整数安全运算(不溢出)
public static int SafeAdd(int a, int b)
{
checked { return a + b; }
}
public static int SafeMultiply(int a, int b)
{
checked { return a * b; }
}
}
// 使用
Console.WriteLine(NumericComparer.ApproximatelyEqual(0.1 + 0.2, 0.3)); // True数值格式化
// 数值格式化 — 注意不同文化环境的差异
double value = 1234567.89;
// 标准格式
Console.WriteLine(value.ToString("N2")); // 1,234,567.89
Console.WriteLine(value.ToString("F2")); // 1234567.89
Console.WriteLine(value.ToString("E4")); // 1.2346E+006
Console.WriteLine(value.ToString("P2")); // 123,456,789.00 %
Console.WriteLine(value.ToString("C2", new CultureInfo("zh-CN"))); // ¥1,234,567.89
Console.WriteLine(value.ToString("C2", new CultureInfo("en-US"))); // $1,234,567.89
// 注意:Parse 时也要指定 Culture
// 错误:double.Parse("1.234,56") 在某些文化环境下会失败
// 正确:double.Parse("1234.56", CultureInfo.InvariantCulture)优点
缺点
性能注意事项
// 性能对比:不同数值类型的运算速度
using System.Diagnostics;
var sw = Stopwatch.StartNew();
double dSum = 0;
for (int i = 0; i < 10_000_000; i++) dSum += 0.1;
sw.Stop();
Console.WriteLine($"double: {sw.ElapsedMilliseconds} ms");
sw.Restart();
decimal mSum = 0m;
for (int i = 0; i < 10_000_000; i++) mSum += 0.1m;
sw.Stop();
Console.WriteLine($"decimal: {sw.ElapsedMilliseconds} ms");
// 典型结果:double ~30ms, decimal ~300ms(decimal 慢约 10 倍)性能建议:
- 科学计算和游戏开发:使用
double或float,追求速度 - 金融计算:使用
decimal,追求精度 - 整数运算:优先使用
int,溢出风险场景使用long或checked - SIMD 加速:大量数值运算使用
Vector\<T\>,可提升 4-8 倍性能
数值类型与序列化
// JSON 序列化中的数值类型陷阱
using System.Text.Json;
// 问题 1:double 精度在 JSON 序列化后可能变化
var data = new { Price = 0.1 + 0.2 };
string json = JsonSerializer.Serialize(data);
Console.WriteLine(json); // {"Price":0.30000000000000004}
// 解决:使用 decimal 或自定义转换器
var safeData = new { Price = 0.1m + 0.2m };
string safeJson = JsonSerializer.Serialize(safeData);
Console.WriteLine(safeJson); // {"Price":0.3}
// 问题 2:long 超出 JavaScript 安全整数范围(2^53)
var bigId = new { Id = long.MaxValue }; // 9223372036854775807
string idJson = JsonSerializer.Serialize(bigId);
// JavaScript: Number(9223372036854775807) → 9223372036854776000(精度丢失)
// 解决:序列化为字符串
var safeId = new { Id = long.MaxValue.ToString() };
// 问题 3:自定义数值精度
public class DecimalPrecisionConverter : JsonConverter<decimal>
{
private readonly int _precision;
public DecimalPrecisionConverter(int precision = 2) => _precision = precision;
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> reader.GetDecimal();
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
=> writer.WriteNumberValue(Math.Round(value, _precision));
}
// 注册转换器
var options = new JsonSerializerOptions();
options.Converters.Add(new DecimalPrecisionConverter(4));数值类型与数据库映射
// Entity Framework Core 数值类型映射注意事项
// 1. decimal 精度配置
// 默认 decimal(18,2),金融场景通常需要更高精度
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
entity.Property(e => e.Amount)
.HasColumnType("decimal(18,4)") // 18位总长,4位小数
.HasPrecision(18, 4);
entity.Property(e => e.ExchangeRate)
.HasColumnType("decimal(18,8)") // 汇率需要更高精度
.HasPrecision(18, 8);
});
}
// 2. 不同数据库的数值类型映射
// +-----------------+-------------------+-------------------+
// | C# 类型 | SQL Server | PostgreSQL |
// +-----------------+-------------------+-------------------+
// | int | INT | integer |
// | long | BIGINT | bigint |
// | decimal | DECIMAL(18,2) | numeric(18,2) |
// | float | REAL | real |
// | double | FLOAT | double precision |
// | byte[] | VARBINARY(MAX) | bytea |
// | Guid | UNIQUEIDENTIFIER | uuid |
// +-----------------+-------------------+-------------------+
// 3. 金额计算常见错误
// 错误:在 C# 中用 double 计算,再存入数据库的 decimal 列
double doubleAmount = 99.99 * 0.08; // 7.999199999999999
decimal dbAmount = (decimal)doubleAmount; // 7.9991999999999989...
// 正确:全程使用 decimal
decimal decimalAmount = 99.99m * 0.08m; // 7.9992
decimal rounded = Math.Round(decimalAmount, 2); // 8.00数值类型单元测试
[TestFixture]
public class NumericTests
{
[Test]
public void Float_Addition_PrecisionIssue()
{
double result = 0.1 + 0.2;
Assert.AreNotEqual(0.3, result); // 浮点精度问题
Assert.IsTrue(Math.Abs(result - 0.3) < 1e-10); // 容差比较
}
[Test]
public void Decimal_Addition_ExactPrecision()
{
decimal result = 0.1m + 0.2m;
Assert.AreEqual(0.3m, result); // decimal 精确计算
}
[Test]
public void Checked_Overflow_ThrowsException()
{
Assert.Throws<OverflowException>(() =>
{
checked { int _ = int.MaxValue + 1; }
});
}
[Test]
public void KahanSum_ReducesAccumulationError()
{
double naive = 0.0;
double kahan = 0.0;
double compensation = 0.0;
for (int i = 0; i < 1_000_000; i++)
{
naive += 0.000_001;
double y = 0.000_001 - compensation;
double t = kahan + y;
compensation = (t - kahan) - y;
kahan = t;
}
// Kahan 求和更接近 1.0
Assert.IsTrue(Math.Abs(kahan - 1.0) < Math.Abs(naive - 1.0));
}
[Test]
public void MoneyCalculation_RoundsCorrectly()
{
var (tax, total) = MoneyCalculator.CalculateTotal(99.99m, 0.08m);
Assert.AreEqual(8.00m, tax);
Assert.AreEqual(107.99m, total);
}
[Test]
public void BigInteger_Factorial_LargeNumber()
{
var result = Factorial(20);
Assert.AreEqual(BigInteger.Parse("2432902008176640000"), result);
}
[TestCase(2.5, MidpointRounding.AwayFromZero, 3)]
[TestCase(2.5, MidpointRounding.ToEven, 2)]
[TestCase(3.5, MidpointRounding.ToEven, 4)]
public void Rounding_Mode_Works(
decimal value, MidpointRounding mode, decimal expected)
{
Assert.AreEqual(expected, Math.Round(value, 0, mode));
}
}总结
金融计算使用 decimal,科学计算使用 double,整数计算注意溢出(使用 checked)。BigInteger 处理超大整数。Vector<T> 利用 SIMD 加速数值计算。浮点比较使用容差而非 ==。建议在金融和统计项目中统一使用 decimal,并开启 CheckForOverflowUnderflow。类型转换时优先使用 TryParse,避免异常开销。
关键知识点
- IEEE 754 浮点数在二进制中无法精确表示 0.1、0.2 等十进制小数
- decimal 使用十进制算术,精度 28-29 位,但性能远低于 double
- 溢出在 unchecked 模式下静默发生,checked 模式下抛出异常
- 类型转换可能导致精度丢失和溢出,应使用 TryParse 安全处理
数值类型的特殊值
// 浮点数特殊值
double positiveInfinity = double.PositiveInfinity;
double negativeInfinity = double.NegativeInfinity;
double nan = double.NaN;
// 特殊值判断
Console.WriteLine(double.IsInfinity(1.0 / 0.0)); // True
Console.WriteLine(double.IsNaN(0.0 / 0.0)); // True
Console.WriteLine(double.IsPositiveInfinity(1.0 / 0.0)); // True
// NaN 的比较特性(所有比较都返回 false,包括 NaN == NaN)
Console.WriteLine(double.NaN == double.NaN); // False!
Console.WriteLine(double.NaN < 0); // False!
Console.WriteLine(double.NaN > 0); // False!
Console.WriteLine(double.IsNaN(double.NaN)); // True(唯一正确的判断方式)
// 常量
Console.WriteLine(double.MaxValue); // 1.7976931348623157E+308
Console.WriteLine(double.MinValue); // -1.7976931348623157E+308
Console.WriteLine(double.Epsilon); // 5E-324(最小正数)
Console.WriteLine(decimal.MaxValue); // 79228162514264337593543950335
Console.WriteLine(decimal.MinValue); // -79228162514264337593543950335二进制与位运算
// 位运算符(常用于标志位、权限掩码和性能优化)
// & 按位与 | 按位或 ^ 按位异或 ~ 按位取反
// << 左移 >> 右移 >>> 无符号右移(C# 11+)
// 标志位操作(权限系统)
[Flags]
public enum Permissions
{
None = 0b_0000,
Read = 0b_0001,
Write = 0b_0010,
Execute = 0b_0100,
Admin = 0b_1000,
All = Read | Write | Execute | Admin
}
var userPerms = Permissions.Read | Permissions.Write;
Console.WriteLine(userPerms.HasFlag(Permissions.Read)); // True
Console.WriteLine((userPerms & Permissions.Execute) != 0); // False
userPerms |= Permissions.Execute; // 添加权限
userPerms &= ~Permissions.Write; // 移除权限
userPerms ^= Permissions.Admin; // 切换权限
// 二进制字面量与分隔符
int binary = 0b_1010_1010;
int hex = 0xFF_00_FF;
int dec = 1_000_000;
// 快速判断 2 的幂
bool IsPowerOfTwo(int n) => n > 0 && (n & (n - 1)) == 0;
// 快速乘除 2 的幂
int doubled = 5 << 1; // 10(乘以 2)
int halved = 10 >> 1; // 5(除以 2)
int multiplied8 = 3 << 3; // 24(乘以 8)
// 交换两个整数(不用临时变量)
void Swap(ref int a, ref int b)
{
a ^= b;
b ^= a;
a ^= b;
}- IEEE 754 浮点数在二进制中无法精确表示 0.1、0.2 等十进制小数
- decimal 使用十进制算术,精度 28-29 位,但性能远低于 double
- 溢出在 unchecked 模式下静默发生,checked 模式下抛出异常
- 类型转换可能导致精度丢失和溢出,应使用 TryParse 安全处理
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 在金融系统中使用 double 进行金额计算——会导致精度丢失
- 使用 == 比较浮点数——应该使用容差比较
- 忽略溢出风险——生产环境应开启 checked 或在关键位置手动检查
- 字符串解析不指定 CultureInfo——不同地区的数字分隔符不同
进阶路线
- 学习 System.Numerics 中的 Matrix4x4、Quaternion 等数学类型
- 了解 IEEE 754 标准的 NaN、Infinity 和特殊值的处理规则
- 掌握 BenchmarkDotNet 进行数值计算性能基准测试
- 探索
Span<T>和Memory<T>在数值计算中的应用
适用场景
- 金融系统、电商结算、财务报表 — 必须使用 decimal
- 科学计算、物理模拟、游戏引擎 — 使用 double/float
- 密码学、大数计算 — 使用 BigInteger
- 图像处理、信号处理 — 使用
Vector<T>进行 SIMD 加速
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了"高级"而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把 C# 数值计算放到当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 项目中哪些地方使用了浮点数做金额计算?是否存在精度风险?
- 相比默认实现或替代方案,采用 decimal 最大的收益和代价分别是什么?
