文件哈希校验是唯一可靠手段
仅靠“重跑一遍代码”无法保证输出一致性——编译器优化、浮点运算顺序、时区/本地化设置、随机种子、未初始化内存读取都可能让同一份代码在不同环境或时间点产出不同字节。真正能验证确定性的,只有对输入和输出文件做哈希比对。
System.Security.Cryptography.SHA256.Create()是首选:抗碰撞强、.NET 内置、无外部依赖 必须用
FileStream以
FileAccess.Read+
FileShare.Read打开,避免被其他进程锁住或写入干扰 别用
File.ReadAllText()或
File.ReadAllBytes()—— 它们会把 BOM、换行符(
\r\nvs
\n)、编码隐式转换全包进来,导致哈希不稳 示例关键片段:
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan); using var sha = SHA256.Create(); var hash = sha.ComputeHash(fs); // 直接流式计算,不加载全量到内存
文本处理必须锁定编码和换行符
哪怕只是读一行改一个字段再写回,
StreamWriter默认用系统当前编码(可能是 GBK),而
StreamReader自动探测 BOM —— 这会导致 Windows 和 Linux 下输出文件字节完全不同。 读写文本一律显式指定
Encoding.UTF8,且禁用 BOM:
new StreamWriter(file, append: false, Encoding.UTF8, bufferSize: 4096) { AutoFlush = true }
换行符统一用 "\n"(Linux 风格),别用
Environment.NewLine—— 后者在 Windows 是
"\r\n",破坏二进制一致性 若需兼容已有 Windows 工具,可在最终输出阶段做一次
.Replace("\n", "\r\n"),但哈希校验必须基于该替换后的字节流
JSON/XML 序列化务必关闭格式化(Formatting.None)和自动缩进,否则空格、换行位置受对象属性顺序影响
浮点数序列化必须禁用科学计数法与精度漂移
C# 的
double.ToString()默认用
G17格式,看似精确,但在某些值上会因舍入策略差异产生不同字符串(如
1e-5可能输出为
"0.00001"或
"1E-05"),直接破坏字节一致性。 所有浮点输出强制用固定小数位数格式,例如
value.ToString("F15", CultureInfo.InvariantCulture)
必须传 CultureInfo.InvariantCulture—— 否则小数点可能变成逗号(如德语系统) 避免
Math.Round()后再转字符串:它不保证 IEEE 754 舍入模式一致;优先用
decimal处理金融类确定性计算 若从
double构建 JSON,用
JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString并配合自定义
JsonConverter<double></double>控制输出格式
时间戳和随机数必须脱离系统环境
任何调用
DateTime.Now、
Guid.NewGuid()、
Random.Shared.Next()的地方,都是确定性的天敌——它们的输出取决于执行时刻、硬件熵源、进程生命周期。 时间相关字段全部用输入文件中提取的固定时间(如文件
LastWriteTimeUtc),或由配置传入的
DateTimeOffset参数,绝不用运行时动态获取 UUID/GUID 若必须生成,用基于输入内容的确定性哈希(如
SHA256(inputBytes).Take(16)构造
byte[16]再转
Guid) 随机数需求(如采样、扰动)改用
new Random(seed),seed 必须来自输入哈希或配置,不能用默认无参构造 注意:.NET 6+ 的
Random默认种子来自
Environment.TickCount,不是真随机但也不稳定;必须显式传入确定性 seed
最常被忽略的是文件元数据——
File.SetLastWriteTime()会修改 NTFS 时间戳,但不影响文件内容哈希;可安全调用。真正要盯死的,永远是字节流本身:打开方式、编码、换行、浮点格式、时间/随机源。只要这五处锁死,同一输入必然产出同一输出。
