ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#工控开发中Math与DateTime的常见陷阱与最佳实践

C#工控开发中Math与DateTime的常见陷阱与最佳实践 工控项目里摸爬滚打这几年我发现自己用得最多的不是那些花哨的框架反而是C#里最基础的Math和DateTime。别小看这两个类越是基础的东西越藏着容易翻车的细节。比如Math.Round的银行家舍入、DateTime.Now的性能开销、日期格式化里MM和mm的区别这些坑我几乎都踩了一遍。这篇就当作一次系统梳理把C#静态类和日期时间相关的核心知识点、实操经验、避坑手册一次说清楚。不管你是刚入门C#的新手还是写过几年上位机的老手都能从这里拿走点直接能用的东西。1. 静态类与Math类的基础认知1.1 为什么Math必须是静态类——工具方法的设计逻辑C#里static class静态类这个概念刚接触时容易懵既然都是类为什么不能new一个Math对象出来用答案其实很简单——Math类就是一组数学工具的集合它不需要保存任何状态。你想想调用Math.Sqrt(16)的时候这个函数关心的是“16的平方根是多少”它不需要记住上一次调用的人是谁也不需要维护内部变量。如果每次用之前都得new一下反而要处理对象的生命周期纯属多余。这就是静态类存在的核心逻辑没有实例状态、只有静态成员、不能被继承的类就应该设计成静态类。实际操作中静态类的约束也很明确所有成员必须是static不能有实例构造函数编译器默认给你加一个私有构造函数防止外部实例化。从C# 8.0开始静态类还可以定义static接口成员但使用场景很有限日常写工具模块用不上了解即可。我自己写工具类时也遵循同样的原则如果一个类里全是无状态的方法比如StringHelper、FileHelper、Validator全部声明为静态类。这不仅是习惯也是团队协作时的约定——别人看到static class就知道这是个工具集合里面不会有需要手动管理的资源。1.2 Math类核心成员全景数学常量和常用函数一览Math类位于System命名空间无需额外引用包即可使用。先梳理一下核心成员方便没系统性看过文档的朋友快速建立概念地图。数学常量// 圆周率 π ≈ 3.14159265358979 double pi Math.PI; // 自然常数 e ≈ 2.71828182845905 double e Math.E;这两个常量在中级数学运算中很常见。比如工控里做圆弧插补计算需要把角度转弧度参与坐标计算double radius 12.5; // 60度角对应的弧长 double arcLength radius * (60 * Math.PI / 180);常用函数分类函数说明典型场景Math.Abs(x)取绝对值判断误差是否在允许范围内Math.Sign(x)返回符号-1、0、1判断运动方向Math.Max(a, b)/Math.Min(a, b)取较大/较小值限幅Math.Sqrt(x)平方根几何计算Math.Pow(x, y)幂运算信号处理、算法实现Math.Floor(x)向下取整分页计算Math.Ceiling(x)向上取整数量向上取整Math.Round(x)四舍五入特殊显示值处理Math.Truncate(x)向零截断取整数部分Math.Sin(x)/Math.Cos(x)/Math.Tan(x)三角函数弧度制运动控制、坐标旋转Math.Atan2(y, x)计算向量角度方位角计算Math.Acos(x)/Math.Asin(x)反三角函数反算角度Math.Exp(x)e的x次方指数衰减算法Math.Log(x)/Math.Log(x, base)自然对数/指定底数对数算法调参Math.Log10(x)常用对数底数10数值压缩比如说Math.Max和Math.Min你会发现在上位机里最常用的场景是“数据限幅”。从传感器读取的温度值可能因为干扰变成异常值这时候限幅double rawTemp ReadTempFromSensor(); // 传感器温度范围 0~150℃超出范围直接限幅到边界 double safeTemp Math.Max(0, Math.Min(150, rawTemp));这种写法比if判断简洁得多而且在一行里维护两个边界可读性反而更好。2. Math常用方法实操拆解从四舍五入到性能陷阱2.1 取整三兄弟Floor、Ceiling、Truncate的区别初学最容易搞混的就是这几个取整方法尤其处理负数时特别容易翻车。记住一个原则Math.Floor永远向负无穷取整Math.Ceiling永远向正无穷取整Math.Truncate无条件向零截断。举例来说double a 3.14; double b -3.14; // Floor: 3.14 → 3, -3.14 → -4 Console.WriteLine(${Math.Floor(a)}, {Math.Floor(b)}); // Ceiling: 3.14 → 4, -3.14 → -3 Console.WriteLine(${Math.Ceiling(a)}, {Math.Ceiling(b)}); // Truncate: 3.14 → 3, -3.14 → -3 Console.WriteLine(${Math.Truncate(a)}, {Math.Truncate(b)});为什么Truncate(-3.14)是-3而Floor(-3.14)是-4因为Truncate直接丢弃小数位-3.14的整数部分是-3Floor是找“小于等于-3.14的最大整数”显然是-4。实际场景中分页计算是Ceiling的经典应用。比如你有一批数据每页显示50条总共1234条数据需要多少页int totalCount 1234; int pageSize 50; int totalPages (int)Math.Ceiling((double)totalCount / pageSize); // 输出: 25页这里有个小技巧如果直接用整数除法1234 / 50结果是24会漏掉最后不足50条的那一页所以必须先转成double再做除法然后Ceiling向上取整。2.2 幂运算、绝对值与最值——这些基础操作藏着性能细节Math.Pow看起来很好用但别在循环里滥用。实测下来Math.Pow底层走的是对数指数换算性能比直接乘法差一个量级。比如算x的平方x * x在循环里优势明显// 循环1亿次场景下Math.Pow(x, 2) 比 x * x 慢约 8~10 倍 // 建议直接用乘法 double square x * x;但幂次是变量或者指数很大时比如计算Math.Pow(2, n)这种动态幂次直接用Math.Pow或者移位运算符。2的n次方在整数范围内用移位更快int n 10; int result 1 n; // 等于 1024Math.Abs的另一个使用场景是误差判断。工控里判断实际到达位置是否在允许误差内double targetPos 100.0; double actualPos ReadActualPosition(); double tolerance 0.05; if (Math.Abs(targetPos - actualPos) tolerance) { // 到位置了 }这里为什么要用Math.Abs而不是直接判断targetPos - actualPos的大小因为你不知道实际位置比目标位置多还是少绝对值一包就直接把正负两个方向的偏差统一起来了代码一次搞定。2.3 三角函数使用的关键C#的Sin和Cos接收的是弧度这个问题我在给新手讲运动控制、讲视觉标定时几乎都会遇到。Math.Sin、Math.Cos的入参单位不是角度而是弧度radian。圆的一周是360度对应弧度是2 * Math.PI。所以角度转弧度的公式就是double angle 30.0; double radians angle * Math.PI / 180.0; double sinValue Math.Sin(radians);在编写视觉定位、机械手坐标变换时绕坐标系旋转的公式最常见// 点(x, y)绕原点旋转θ角的坐标变换 double x 10.0, y 20.0; double angleDeg 45.0; double rad angleDeg * Math.PI / 180.0; double newX x * Math.Cos(rad) - y * Math.Sin(rad); double newY x * Math.Sin(rad) y * Math.Cos(rad);反三角求角度也是一样的逻辑返回结果默认是弧度需要转成角度显示// 已知直角三角形的对边和邻边求角度 double opposite 5.0; double adjacent 12.0; double angleRad Math.Atan2(opposite, adjacent); double angleDeg angleRad * 180.0 / Math.PI;Math.Atan2和Math.Atan的区别也要注意。Atan2(y, x)能根据x和y的符号自动判断象限返回角度范围是-π到π更适合方位角计算Atan(y/x)只能返回-π/2到π/2无法区分第二、第三象限全角计算时千万别用错。2.4 值得单独说的坑Math.Round的银行家舍入Math.Round在C#里的默认行为不是小学学的“四舍五入”而是“银行家舍入”四舍六入五成双。这是为了减少统计学上的累积误差而设计的。测试一下就明白了double x 2.5; double y 3.5; Console.WriteLine(Math.Round(x)); // 输出 2 Console.WriteLine(Math.Round(y)); // 输出 42.5舍入到最近的偶数23.5舍入到最近的偶数4。如果业务场景要求严格的四舍五入必须注明舍入模式double value 2.5; double rounded Math.Round(value, 0, MidpointRounding.AwayFromZero); // 输出 3同理保留指定小数位数时如果第三位小数恰好是5默认模式下同样会遇到“五成双”问题double value 82.335; // 默认银行家舍入有时候结果是82.33而不是82.34看后继位数而定 double result1 Math.Round(value, 2); // 强制四舍五入 double result2 Math.Round(value, 2, MidpointRounding.AwayFromZero);在计量、结算场景必须明确选择舍入策略否则会出现“账不平”的诡异问题。我在给一个老系统做维护时就碰到过因为默认AwayFromZero.NET Framework老版本的行为迁移到.NET Core默认行为改变导致打印的报表数据差异的案例。这里也提醒一下不同.NET版本默认舍入模式并不完全一致跨版本迁移时一定要做回归测试。另外补充一个实用技巧如果只是给用户显示不涉及计算用ToString(F2)格式化字符串就够了不需要走Math.Round性能更好也不会引入二进制浮点数误差叠加。3. DateTime核心功能梳理日期表示与格式化3.1 创建DateTime的几种方式与底层机制DateTime在C#中是一个结构体struct不是类class。这意味着它是值类型赋值时默认是复制一份而不是引用传递。日常创建常用方式有这些// 方式一直接指定年月日时分秒 DateTime dt1 new DateTime(2025, 6, 15, 14, 30, 25); // 方式二只指定年月日时分秒默认为0 DateTime dt2 new DateTime(2025, 6, 15); // 方式三通过解构赋值 (DateTime dt, _) (new DateTime(2025, 6, 15), 0); // 方式四转换为UTC时间 DateTime utcNow DateTime.UtcNow; // 方式五当前本地时间 DateTime localNow DateTime.Now; // 方式六只取当前日期时间部分为00:00:00 DateTime today DateTime.Today;关于DateTime.Now和DateTime.UtcNow的取舍值得多说几句。工控系统如果都是单机运行DateTime.Now直观方便但一旦有多台设备需要记录同一事件的时间建议统一用DateTime.UtcNow存储展示时再转本地时间。否则不同机器时区设置不一致记录的日志时间就无法对齐了。还有一个容易忽略的日期溢出问题。DateTime能表示的范围是公元0001年到9999年直接AddDays超出范围会抛ArgumentOutOfRangeException。比如从9999年12月31日AddDays(1)就会崩。实际业务中很少碰到但用DateTime.MaxValue做边界判断时心里要有个数。DateTime内部存储的实际是一个Ticks刻度值表示从公元0001年1月1日午夜12:00起经过的100纳秒间隔数。这个特性让日期比较变得非常高效——DateTime对象可以直接用、、比较本质就是比较Ticks。3.2 日期格式化MM和mm的秘密以及自定义格式串日期格式化是高频操作但格式串里的符号大小写非常容易踩坑。先看经典的坑MM是月份mm是分钟。yyyy-MM-dd HH:mm:ss和yyyy-mm-dd HH:mm:ss是两个完全不同的东西DateTime now DateTime.Now; // 正确格式2025-06-15 14:30:25 string correct now.ToString(yyyy-MM-dd HH:mm:ss); // 错误示例月份被格式化成分钟2025-06-15 14:30:25 里的 mm 是错误写法 // 如果写成 yyyy-mm-dd 输出的是 2025-30-15分钟当月份完整记住这几个常用模式格式含义输出示例yyyy-MM-dd年月日数字2025-06-15yyyy年MM月dd日中文年月日2025年06月15日HH:mm:ss时分秒24小时制14:30:25hh:mm:ss tt时分秒12小时制02:30:25 PMyyyy-MM-dd HH:mm:ss.fff带毫秒2025-06-15 14:30:25.123yyyy-MM-dd HH:mm:ss.fffffff带7位精度2025-06-15 14:30:25.1234567ddd星期几的缩写周日dddd星期几全称星期日yyyy-MM-dd HH:mm:ss zzz带时区偏移2025-06-15 14:30:25 08:00另外格式化日期还有几个标准格式串比如ToString(d)输出短日期、ToString(D)输出长日期、ToString(T)输出长时间。这些依赖系统当前文化CultureInfo国内开发默认中文环境没问题但如果程序部署在英文系统上输出可能在月份和星期上变成英文。跨区域部署时建议要么显式指定CultureInfo比如CultureInfo.InvariantCulture要么用自定义格式串完全控制输出。再说一个我在日志模块里常用的技巧文件名里带毫秒防止日志文件重名string logFileName $log_{DateTime.Now:yyyyMMdd_HHmmss_fff}.txt; // 输出: log_20250615_143025_123.txt:格式串写在插值字符串里面可以直接对DateTime应用格式比先ToString再拼字符串简洁很多。3.3 日期运算AddDays、日期差值的正确姿势日期加减的核心是Add系列方法DateTime now DateTime.Now; DateTime tomorrow now.AddDays(1); DateTime nextMonth now.AddMonths(1); DateTime nextYear now.AddYears(1); DateTime nextHour now.AddHours(1);这里要特别注意AddMonths的行为——它不是简单加30天而是日历月份的下一个月同一天。比如1月31日AddMonths(1)2月没有31日它会自动处理为2月的最后一天2025年2月28日。这个行为常常让人困惑但实际中是很合理的DateTime dt new DateTime(2025, 1, 31); DateTime next dt.AddMonths(1); // 输出: 2025-02-28自动调整到当月最后一天两个日期相减得到的是TimeSpan对象可以直接拿到天数、小时数、分钟数DateTime start new DateTime(2025, 6, 15, 8, 0, 0); DateTime end new DateTime(2025, 6, 16, 9, 30, 0); TimeSpan diff end - start; int totalDays diff.Days; // 1 int totalHours diff.Hours; // 1天的余数部分 double totalHoursAll diff.TotalHours; // 25.5总小时数Days和TotalHours的区别是新手最容易搞混的。Days只返回整天数Hours是余数部分的小时数而TotalHours是完整的浮点数小时数。比如上面例子里diff.Days是1diff.Hours是1因为25.5小时中1天之外的剩余1.5小时取整TotalHours是25.5。搞清楚了写超时判断时就不容易出问题。DateTime也支持直接相加运算符。TimeSpan可以加到DateTime上TimeSpan waitTime new TimeSpan(0, 30, 0); // 30分钟 DateTime wakeTime DateTime.Now waitTime;3.4 时间戳转换——上位机对接最常见的操作上位机对接PLC可编程逻辑控制器、设备SDK时时间戳转换几乎绕不开。尤其是对接老设备时很多模块返回的是Unix时间戳。Unix时间戳的含义是从1970年1月1日UTC 0时0分0秒起经过的秒数。转换公式如下// 时间戳 → DateTime long timestamp 1749875425; // 假设是一个Unix秒数 DateTime dt DateTimeOffset.FromUnixTimeSeconds(timestamp) .ToLocalTime(); Console.WriteLine(dt.ToString(yyyy-MM-dd HH:mm:ss)); // DateTime → 时间戳 DateTime localTime new DateTime(2025, 6, 15, 14, 30, 25); long unixTimestamp new DateTimeOffset(localTime) .ToUnixTimeSeconds(); Console.WriteLine(unixTimestamp);注意毫秒时间戳要用FromUnixTimeMilliseconds别和秒时间戳混了。有些设备文档写的“时间戳”实际上是以毫秒为单位的13位数一不小心就差了1000倍。我见过排查了半天的串口数据问题最后发现是两个模块之间时间戳单位不一致。再补充一个实用场景毫秒级时间戳在很多工控协议里用的是double类型表示这时候可以直接用DateTimeOffset.FromUnixTimeMilliseconds((long)timestampDouble)转换。精度损失在这里无所谓显示到秒级别就够用了。4. 综合实操贴近真实场景的日期与数学工具应用4.1 上位机场景任务耗时统计与日志记录写上位机软件时经常要统计某个动作的耗时比如一次完整的扫码、称重、读写信号。最直接的方式就是用Stopwatch配合DateTime做日志记录using System.Diagnostics; public class ActionRecorder { private readonly Stopwatch _sw new Stopwatch(); private DateTime _startTime; public void StartAction() { _sw.Restart(); _startTime DateTime.Now; Log($动作开始: {_startTime:yyyy-MM-dd HH:mm:ss.fff}); } public void EndAction(string actionName) { _sw.Stop(); DateTime endTime DateTime.Now; TimeSpan elapsed _sw.Elapsed; Log(${actionName}结束: {endTime:yyyy-MM-dd HH:mm:ss.fff}, 耗时: {elapsed.TotalMilliseconds:F1} ms); } private void Log(string message) { Console.WriteLine($[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}); } }这里我把Stopwatch和DateTime结合使用。Stopwatch负责高精度计时DateTime负责记录时间点。别直接用两次DateTime.Now相减来统计耗时——DateTime.Now精度在Windows上大约1到15毫秒取决于系统定时器分辨率短时延计时误差不可接受。Stopwatch底层走的是高精度计数器微秒级别的区分度。另外如果程序运行时间长了循环里用DateTime.Now打日志其实也有性能消耗一次DateTime.Now调用大概耗时几十到几百纳秒看起来不多但高频调用比如每秒几十次的循环也会累积。这种情况可以考虑用环境时间缓存方案或者只在事件触发时取时间。4.2 排产场景计算“本月第一个工作日”业务系统里经常要算“某个日期所在周的周一”“某月第一个工作日”这种相对日期。用DateTime和Math组合可以优雅地解决// 获取某个日期所在周的周一以周一为一周开始 DateTime GetMonday(DateTime date) { int diff ((int)date.DayOfWeek 6) % 7; return date.Date.AddDays(-diff); } // 获取某月第一个工作日跳过周六周日 DateTime GetFirstWorkDay(int year, int month) { DateTime firstDay new DateTime(year, month, 1); while (firstDay.DayOfWeek DayOfWeek.Saturday || firstDay.DayOfWeek DayOfWeek.Sunday) { firstDay firstDay.AddDays(1); } return firstDay; }((int)date.DayOfWeek 6) % 7这个表达式的意思是DayOfWeek枚举里周日是0、周一是1……周六是6加6再取模7就能把周日映射成0、周一到周六映射成1~6得到“距离本周周一过了几天”然后往前减即可。这里用到的思想是日期计算里先归一到基准点比如周一再偏移是最稳妥的写法。直接硬编码日期会导致节假日、月底年边这些边界条件出bug。再进一步如果要跳过法定节假日就必须引入节假日表了。这时用HashSetDateTime存节假日列表判断逻辑变成HashSetDateTime holidays LoadHolidays(); // 从配置或数据库加载 DateTime GetNextWorkDay(DateTime date) { date date.Date.AddDays(1); while (date.DayOfWeek DayOfWeek.Saturday || date.DayOfWeek DayOfWeek.Sunday || holidays.Contains(date)) { date date.AddDays(1); } return date; }这种小工具函数一旦写好可以在很多地方复用。注意HashSet.Contains的查找复杂度是O(1)比List.Contains快得多节假日多了也不卡。4.3 温控曲线场景Math函数的组合应用再分享一个我在温控项目里用过的例子涉及线性和非线性换算。温度传感器输出的是非线性信号时需要根据校准表把原始值映射到实际温度。最常用的线性插值计算用Math函数组合就能完成// 校准点表原始值, 实际温度 double[] rawPoints { 100, 200, 300, 400 }; double[] tempPoints { 25.0, 52.3, 79.8, 107.1 }; double LinearInterpolate(double rawValue) { if (rawValue rawPoints[0]) return tempPoints[0]; if (rawValue rawPoints[^1]) return tempPoints[^1]; for (int i 0; i rawPoints.Length - 1; i) { if (rawValue rawPoints[i] rawValue rawPoints[i 1]) { double ratio (rawValue - rawPoints[i]) / (rawPoints[i 1] - rawPoints[i]); return tempPoints[i] ratio * (tempPoints[i 1] - tempPoints[i]); } } return 0; }这个例子本质上就是Math.Max、Math.Min限幅、除法比例、乘法加权的组合。实际工程问题大多数都是这样功能不复杂但边界条件和数据校验要做到位。区间判断用和而不是和确保落在校准点上的情况能命中分支。5. 常见问题与避坑手册5.1 高频问题排查速查表这里汇总一下我见过的Math和DateTime高频问题可以直接照表排查。现象原因解决方案Math.Round(2.5)结果不是3默认是银行家舍入四舍六入五成双用Math.Round(2.5, 0, MidpointRounding.AwayFromZero)DateTime.ToString(yyyy-MM-dd)输出的月份不对项目里混用MM和mmmm是分钟月份必须用大写MM两个DateTime.Now相减耗时不准DateTime.Now精度有限不适合高精度计时改用StopwatchDateTime.Now.ToString(yyyy-MM-dd)在英文系统显示不一样受CurrentCulture影响指定CultureInfo.InvariantCulture或自定义格式串1月31日AddMonths(1)变成2月28/29日AddMonths按日历月份调整2月无31日接受该行为或先判断原日期的Day再调整DateTime相减得到负数天数结束时间早于开始时间先做大小判断或者用Math.Abs(diff.TotalDays)从设备读出的时间戳除以1000/10000才正常时间戳单位不一致秒、毫秒、微秒混合先确认协议文档的单位再按对应方法解析Math.Sin(30)得到负数入参是弧度不是角度先乘以Math.PI/180转弧度Math.Pow在循环里很慢底层实现复杂速度不如乘法固定幂次用乘法或移位代替DateTime.MinValue报日期溢出超出DateTime可表示范围0001~9999做边界检查或用TryParse解析外国格式日期失败字符串里的斜杠/短横线与Culture不匹配用DateTime.TryParseExact指定格式时间戳存入数据库变成负数日期在1970年之前或时区问题确认时区设置统一用UTC存储5.2 我踩过的几个印象深刻的坑第一个坑是DateTime作为字典键的问题。DateTime是结构体默认的GetHashCode是按Ticks生成的因此同一个时刻不同实例的哈希值相同。但如果你用一个DateTime变量作为字典键然后修改了这个变量再去查字典它会找不到——因为结构体是值复制查的时候复制的是一份“快照”的哈希而字典里存的是旧快照。别问我怎么知道的那次排查了很久最后用DateTime.Ticks作为键才解决问题。第二个坑是DateTime.ToString()在不同框架版本下默认输出的格式会变。在.NET Framework里先运行得好好的日期格式迁移到.NET Core之后同一个ToString()方法输出的格式可能变成不同的样式。这不是代码逻辑错了而是不同版本对不同Culture的支持有所调整。所以一定要用自定义格式串或者显式指定Culture。第三个坑是Math.Round结合float转double的问题。float的精度只有7位有效数字转成double之后会带上“尾巴”。比如float类型的82.335f转成double后实际值可能是82.33499908447265625这时Math.Round(value, 2, MidpointRounding.AwayFromZero)得到的结果可能是82.33而不是预期的82.34。解决方案是货币/金额计算用decimal而不是double/float。这也是为什么银行和财务系统偏爱decimal的原因——二进制浮点数天然存在表示误差decimal是十进制运算能精确表示小数。5.3 性能相关的几点补充在写高频调用的工具模块时有几个性能相关的结论值得记住。DateTime.Now内部触发系统调用和时区转换开销比DateTime.UtcNow高。如果循环里只需要记录“相对时间”可以考虑缓存DateTime utcStart DateTime.UtcNow; // ... 高频循环中不再调用DateTime.Now而是基于UtcNow计算差值DateTime提供Kind属性Local、Utc、Unspecified在跨时区比较时要特别小心。Kind不同但Ticks相同的两个DateTime比较是相等的但DateTime.SpecifyKind和ToUniversalTime()转换会影响实际换算。建议在写入数据库或与其他系统交互时统一指定DateTimeKind.Utc。字符串拼接日期的方式性能较差尤其在日志系统里频繁执行// 不推荐每次都ToString再拼接 string log Time: dt.ToString() Value: value.ToString(); // 推荐插值字符串更简洁性能也更好 string log $Time:{dt:yyyy-MM-dd HH:mm:ss} Value:{value:F3};自定义格式串内部的解析是有成本的如果同一格式在循环里大量使用可以考虑缓存CultureInfo实例或预格式化。5.4 跨时区与夏令时问题最后说一个容易被国内开发者忽略的问题——夏令时。国内不实行夏令时但如果你部署的程序要处理海外数据或者对接的服务器在美国、欧洲就需要考虑夏令时转换。C#里用TimeZoneInfo处理时区// 将UTC时间转换为东部标准时间含夏令时自动调整 DateTime utc DateTime.UtcNow; TimeZoneInfo estZone TimeZoneInfo.FindSystemTimeZoneById(Eastern Standard Time); DateTime estTime TimeZoneInfo.ConvertTimeFromUtc(utc, estZone);如果业务系统只记录时间而不关心时区建议统一用UTC格式存储yyyy-MM-ddTHH:mm:ssZISO 8601带Z后缀展示时再转本地时间。这样至少逻辑上是清晰的不会出现不同机器各显示各的时间这种混乱。DateTimeOffset类型在处理跨时区时间时比DateTime更可靠因为它保存了UTC偏移量。如果对接的接口频繁跨时区建议优先考虑DateTimeOffset而不是DateTime避免比较和排序时因为时区偏移不一致而出错。6. 结尾一些个人的使用习惯说了这么多最后分享一点我的个人习惯。写C#代码这几年对于Math和DateTime这类基础类型我总结下来就三条原则第一凡是涉及金额、精确小数计算的一律用decimal而不是double第二凡是涉及跨机器、跨时区的时间记录一律用DateTimeOffset或UTC存储展示时再转本地第三凡是循环里高频调用的计算先想想有没有更快的替代写法Math.Pow能换成乘法就换DateTime.Now能少调就少调。还有一个小技巧想分享给大家遇到拿不准的日期格式化行为不要用眼睛猜直接在控制台里敲一行代码跑一下马上就能看到真实输出。这个习惯帮我避开了很多次因为记忆模糊而产生的低级错误。毕竟这些基础模块的细节就算写了十年C#也难免有记串的时候。
返回列表