65.9K
CodeProject 正在变化。 阅读更多。
Home

时区实验室 II:探索 DST 调整规则

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.50/5 (3投票s)

2016年6月17日

CPOL

19分钟阅读

viewsIcon

9979

downloadIcon

98

当我需要计算任何一年的夏令时转换时,我不得不更深入地研究存储在 Windows 注册表中的时区信息。

引言

为了测试一个读取存储在 Windows 计算机上的文件的时间戳的程序中的一些边界情况,我需要随时访问任何一年的夏令时 (DST) 转换日期。

背景

两年前,为了满足类似的需求,我写了一个我称之为“时区实验室”的程序,我在“时区实验室:Windows 时区探索”中对此进行了介绍,网址是:https://codeproject.org.cn/Articles/816165/Time-Zone-Lab-An-Expedition-into-Windows-Time-Zone。今天,为了满足一个密切相关的需求,我得出结论,从 Windows 注册表中的时区表中提取我所需信息的最简单方法是扩展“时区实验室”,然后围绕新例程的输出构建一个 Microsoft Excel 工作表。

由于世界大部分地区都实行夏令时(以下简称“DST”),并且由于地球的轴相对于太阳的倾斜方式,它们在何时开始和停止使用 DST 上永远无法达成一致。Windows 为世界上的每个时区维护一组调整规则。为了进一步增加复杂性,不时地,国家会更改管理 DST 开始和结束时间的规则,就像美国在 2007 年所做的那样。因此,每个时区不是一条规则,而是一组规则,每条规则都有一个开始日期和结束日期,用于确定其适用的年份。例如,我居住的中部标准时区就有两条规则。第一条规则适用于 2006 年底之前的日期,第二条规则适用于此后的所有日期。

在我深入研究之前很久,我就想知道 Windows 是否维护了一个年份表,其中包含每年的 DST 开始和结束日期的行,针对每个时区。然而,这样的表将是巨大的,并且会给微软带来持续的维护负担,而这很容易避免,因为转换日期是基于数学公式的,使得在需要时相对容易地计算出任何一年的转换日期。

使用代码

提取调整规则的任务落到了静态类 TimeZoneTasks 的一个新成员身上。新方法 EmumerateTimeZoneAdjustments 有两种确定要提取调整规则的时区的方法。

  1. 当程序以初始命令行参数 EmumerateTimeZoneAdjustments 调用,后跟一个时区 ID 时,将报告适用于指定时区的调整规则。
  2. 当程序在没有参数的情况下调用时,将报告适用于本地时区的调整规则,该时区由 Windows 控制面板的区域设置区域确定。

如果您不熟悉它们,EnumTimeZones 命令行选项可以提供一个完整的时区 ID 列表,以及每个时区的大量信息。由于此任务会创建一个非常宽的表,您可能希望将输出通过管道传输到文本文件,这样您就不必展开覆盖每个时区基本信息的非常长的行。令我惊讶的是,时区 ID 并不是一些虚构的、否则无意义的代码;在大多数情况下,它是计算机为之配置的国家所知的时区名称。

如果您宁愿避免处理创建宽报告的控制台程序,您可以在 Microsoft Excel 工作簿 DST_Transition_Date_Computations.XLSX 的工作表 Time Zones on ZAPHOD42 中找到原始时区实验室创建的计算机上生成的报告的输出,稍后将详细介绍。对于那些急于知道的人来说,ZAPHOD42 是我给那台计算机起的 NETBIOS 名称,它现在大部分被用于支持一个我偶尔照看的 DOS 应用程序。今年,我的工作已转移到一个运行 64 位操作系统、NETBIOS 名称为 ENIGMA 的全新 Windows 7 机器上。

调整规则表通过 TimeZoneInfo.AdjustmentRule 对象公开给 .NET 程序,特别是其 TransitionTime 属性,该属性定义了从中派生任何适用规则年份的调整日期的参数。

TimeZoneInfo.AdjustmentRule 对象

TimeZoneInfo.AdjustmentRule 对象公开以下五个属性,这些属性完整地描述了一个 DST 调整规则。

  1. DateStart 是一个 System.DateTime 结构,用于标识规则适用的最早日期。虽然该属性是完整的 DateTime 结构,但只使用日期部分;表示时间的成员设置为零。
  2. DateEnd 是另一个 System.DateTime 结构,用于标识规则适用的最后日期。上述关于 DateStart 成员的观察也适用于此成员。
  3. DaylightDelta 是一个 System.TimeSpan 结构,用于给出添加到标准时间以得出 DST 时间的时间量。Days 成员始终为零,我猜 SecondsMilliseconds 成员也为零。某些地区可能使用 Minutes 成员来按小时的几分之一进行调整,尽管我没有扫描此类情况。无论如何,Ticks 成员是等于所有其他部分的总和(转换为 Ticks)的 Ticks(100 纳秒增量)的数量。

  4. DaylightTransitionStart 是一个 TransitionTime 结构,它定义了确定 DST 何时开始所需的参数。

  5. DaylightTransitionEnd 是一个 TransitionTime 结构,它定义了确定 DST 何时结束所需的参数。

TransitionTime 结构

就像 TimeZoneInfo.AdjustmentRule 对象公开五个属性一样,TransitionTime 结构也公开五个属性。我猜它是一个结构而不是一个完整的类,是因为成员直接从 Windows 注册表中的二进制 TZI 值读取。与其他调整规则成员不同,不需要进行转换。

  1. IsFixedDateRule 是一个布尔值,如果为 **true**,则表示转换发生在固定日期,例如十一月的第一天。否则,转换发生在从其他属性的值计算出的浮动日期。

  2. Month 是转换发生的月份的序数。此属性适用于**所有**转换日期。有效值为 1 到 12,您几乎肯定熟悉的月份编号,它们恰好对应于底层 Windows API 例程使用的 SYSTEMTIME 结构中 wMonth 成员中的值。此成员是 System.Int32

  3. Day 是转换发生的月份的序数日。其值仅适用于发生在固定日期上的转换,即当 IsFixedDateRule 为 **true** 时。当 IsFixedDateRule 为 **false** 时,其值为 **1**,该值被忽略。此成员是 System.Int32

  4. DayOfWeek 将转换发生的星期几表示为 System.DayOfWeek 枚举的成员。这些名称是符号性的,不会本地化,这意味着您可能会看到 Sunday,即使您知道它是 Sontag(德语中的星期日),但这与其他枚举类型没有区别,并且他们在文档页面中提供了获取本地化名称的说明,https://msdn.microsoft.com/en-us/library/system.timezoneinfo.transitiontime.dayofweek(v=vs.110).aspx。您应该知道,数值从 0(星期日)到 6(星期六),这恰好对应于底层 Windows API 例程使用的 SYSTEMTIME 结构中 wDayOfWeek 成员中的值。当 IsFixedDateRule 成员为 **false** 时,此成员起作用,我猜大部分时间都是如此。

  5. Week 是发生时间更改的月份中的周数,值为 1 到 5,其中 5 表示指定月份的最后一个工作日,并且仅适用于浮动日期,即 IsFixedDateRule 成员值为 **false** 的日期。此成员是 System.Int32

虽然我可以让这个方法生成一个多年份的转换日期表或计算指定年份的转换日期,但我选择将这项工作放在已经包含上述时区图的 Microsoft Excel 工作簿中。下一节将讨论工作表,并解释其公式。

Microsoft Excel 工作簿 DST_Transition_Date_Computations.XLSX

最相关,也最有趣的工作表是 CST Adjustment Rules,它分为三个主要部分,以及三个起支持作用的小部分。

  1. 整个工作表围绕顶部的区域展开,由 25 列组成,分为四组彩色编码。该表存储了我居住的 Central Standard Time 时区存储的所有两个调整规则。
    1. 第一部分,带有蓝色标题,给出了 DST 调整规则的索引(数组下标)、序号和生效日期。生效日期对应于从中填充它的 TimeZoneInfo.AdjustmentRuleDateStartDateEnd 属性。
    2. 粉红色区域中的列显示了保存时间调整增量的 TimeSpan 结构的相关成员,对应于其值来源的 DaylightDelta 成员。
    3. 带有绿色标题和边框的列对应于 DaylightTransitionStart 结构,一个 TimeZoneInfo.TransitionTime 结构,如果您忘记了;它显示了所有内容,并补充了来自整个表格右侧的两个小表格中的星期和月份名称查找。这些表格构成了上述三个支持角色中的两个。
    4. 带有米色标题和棕色边框的列对应于另一个 TransitionTime 结构,DaylightTransitionEnd 
  2. 紧挨着上面描述的蓝色列下方是另一个独立的区域,其中除最后一行外的所有单元格都是蓝色的。由于它依赖于第一部分,所以它是在之后创建的,并用于计算进入最后一个主要部分的公式。
    1. 由于该区域专门用于计算单年的转换日期,因此唯一的输入变量是单元格 B20,它对应于一个名为 DST_Year 的单单元格命名范围。
    2. 右侧的单元格使用上面蓝色单元格中的值来计算和显示转换日期。
    3. 蓝色单元格根据适用于指定年份的转换规则,从上方多色表中的适用行填充。
    4. 单元格 A20(年份左侧的单元格)中的公式通过范围测试单元格 A20 中的年份与调整规则表中的两行,来确定要应用的正确调整规则。由于我只对一个时区感兴趣,它只有两条规则,我没费心去弄清楚如何对无限数量的行进行范围测试。如果需要,留给第一个需要的读者作为一个练习。
    5. 上面两个转换日期上方有十个蓝色单元格是单单元格命名范围,我从中构造了它们的公式。当我想或需要向观众(可能是一两年后的我自己)清晰地展示公式的参数时,我使用这种技术。
  3. 最后一个主要部分是 1999 年至 2400 年的转换日期表,2400 年恰好是下一个闰年世纪。
    1. 带有紫色标题和边框的列包含年份和转换日期。
    2. 其余列显示从调整规则表中查找的值,从中派生出调整日期。
    3. 除了存储 IsFixedDateRule 值的列之外,浅灰色部分中的列名与它们的值来源的调整规则表中的列名完全对应。为了避免进一步加宽列,我将这些标签缩短为 Fixed,这应该能传达它们的意思。
    4. 在包含列组标签的行和包含单个列标签的行之间,有一行未标记的行包含数字;这些是数据从中提取的列号,来自调整规则表。查找公式使用这些单元格中的值,以便可以将左上角的公式复制到表中其余部分。否则,单元格公式是普通的 VLOOKUP 公式,它使用锚定到存储调整规则的 Index(G 列)的列和存储要从中提取值的 Adjustment_Rules_Lookup 范围的行的行(第 23 行)的地址。
    5. 目标区域中转换日期单元格中的公式是通过将两个使用命名范围构造的公式复制到文本编辑器中,然后使用其查找和替换功能将每个命名范围替换为右侧浅灰色数据单元格区域中顶行的相应单元格来开发的。一旦测试通过,这两个公式就被复制了 402 次以完成表格。

有三个关键公式值得解释,第一个是 G 列中的公式,它通过执行两个包含范围测试来确定它右侧所有内容的基础规则。乍一看,它看起来像个怪物,而且确实很长。

=IF(AND(D25>=YEAR(CST_Adjustment_Rule_1_Start_Date),D25<=YEAR(CST_Adjustment_Rule_1_End_Date)),0,IF(AND(D25>=YEAR(CST_Adjustment_Rule_2_Start_Date),D25<=YEAR(CST_Adjustment_Rule_2_End_Date)),1,"Undefined"))

当此公式被格式化为更像嵌套方法调用时,会更容易理解,如下所示。

=IF ( AND ( D25 >= YEAR ( CST_Adjustment_Rule_1_Start_Date ) ,
            D25 <= YEAR ( CST_Adjustment_Rule_1_End_Date ) ) ,
      0 ,
      IF ( AND ( D25 >= YEAR ( CST_Adjustment_Rule_2_Start_Date ) ,
                 D25 <=YEAR ( CST_Adjustment_Rule_2_End_Date ) ) ,
                 1 ,
                 "Undefined"
         )
    )

如果要理解计算转换年份的公式,最好先弄清楚单元格 C20 和 D20 中的公式,它们是用命名范围编写的。

单元格 C20 中的公式计算 DST 转换开始日期(夏令时开始的日期),如下所示。

=IF(DST_Start_IsFixedDate,DATE(DST_Year,DST_Start_Month,DST_Start_DayOfMonth),DATE(DST_Year,DST_Start_Month,(DST_Start_Week*7+1)-WEEKDAY(DATE(DST_Year,DST_Start_Month,8-DAY(DST_Start_Weekday)))))

我将像解析调整规则查找一样解析这个公式。

=IF ( DST_Start_IsFixedDate ,
      DATE ( DST_Year ,
             DST_Start_Month ,
             DST_Start_DayOfMonth ) ,
      DATE ( DST_Year ,
             DST_Start_Month ,
             ( DST_Start_Week * 7 + 1 )
             - WEEKDAY ( DATE ( DST_Year ,
                                DST_Start_Month ,
                                8 - DST_Start_Weekday 
                              )
                       )
             )
      )

最外层的 IF 函数评估一个布尔值 DST_Start_IsFixedDate

  • 由于值为 True 意味着转换发生在每年的固定日期,因此计算很简单。
  • 浮动日期的计算要复杂得多,分解一下会更容易理解。
    • 计算日期的年份和月份部分对于固定日期和浮动日期都是相同的。可以将 IF 函数移到单个 Date 函数的第三个参数中来简化公式。但是,由于编写的公式已经过测试,满足我当前的需要,并且效率没有明显降低,因此我将简化留给珍视计算效率的另一位同事。
    • 浮动日期公式的 Day 部分可以整齐地分成两半,我将分别进行图解。
    • 第一部分是 DST_Start_Week * 7 + 1,它给出了转换发生的月份的日期,请记住 Windows 和 Excel 都以星期日开始计算周,并且 DST_Start_Week 的值范围是从 1(月份的第一周)到 5(代表月份的最后一周,通常是部分周)。
    • 从这个值中,减去转换发生的星期几(一个介于 1(星期日)到 7(星期六)之间的整数值)。
    • 由于 Weekday 函数需要一个 Date 作为输入,一个嵌套的 Date 函数提供了它。
    • 最后一个嵌套 Date 函数的 Year 和月份与上面相同,而 Day8 减去 DST_Start_Weekday。仔细想想;因为星期日是 1,所以 Day 解析为 **7**(8 - 1)。因此,对于中部标准时区 2016 年的公式是 Date (2016,3,15)-Weekday(Date(2016,3,7)),简化后为 Date (2016,3,15)-2,即 2016 年 3 月 13 日星期日。

结束转换日期的计算是类似的,用 _End_ 范围名称替换 _Start_ 范围名称。

表格中的公式遵循相同的设计,用相对列地址替换了范围名称,但需要注意的是,最后一个子表达式,例如单元格 E25 中的 N25+1,必须用括号括起来以强制正确的求值顺序。

为了完整起见,这个工作表中的第三个小角色是左上角的图例。

我不指望有人会使用我作为思维练习创建的 402 年表。我期望实际使用蓝色区域中的年份字段,这需要的工作量更少;输入一年,得到转换日期。

为了防止意外导致其无法使用,该工作表受到保护。密码 TheCodeProject 并不是什么大秘密;我在工作簿的高级属性的注释部分放了一份副本。转换日期计算区域中的年份单元格是开放的,顶表中的调整规则单元格也是如此,这样您就可以输入您感兴趣的时区的值。

我不能在不引起注意的情况下离开工作簿,它还有其他一些有趣的功能。

  • _Index of Named Ranges 是我在 Excel 模板库中维护的一个标准工作表,我可以快速将其插入到任何中等复杂的项目中使用许多范围的项目中,例如这个项目。您可以通过将插入点放在单元格 A2 并选择“公式”选项卡上的“使用公式”下拉菜单中的最后一个选项“粘贴名称”来填充它。此命令填充 A 列和 B 列中的单元格,而其余列则由公式填充。如果将其他范围粘贴到边框单元格下方,请执行以下操作。
    • 将 A 列和 B 列最后一个边框行的格式复制并向下拖动到需要的位置,或者可能再多一点,以便为将来的增长留出空间。在此任务中使用“选择性粘贴”。
    • 复制 C 列到 H 列最后一个边框行的所有内容并向下拖动到您在 A 列和 B 列中复制格式的相同位置。由于您想要所有内容,这是一个常规的复制操作。
  • Index 是整个工作簿的目录,它使所有工作表都可以轻松访问,而无需使用 Microsoft 在窗口底部提供的笨拙的 VCR 按钮。此工作表由一个加载项中的 VBA 宏生成,该宏可以根据需要频繁运行以更新索引。运行宏时,索引会被完全重建,然后按工作表名称排序。如果您在它前面插入另一个工作表,不用担心;下次运行宏时,一个新的工作表将被插入到标签顺序的最前面。加载项 WorkbookIndexGenerator.XLAM,以及 DST_Transition_Date_Computations.XLSX,都包含在本文附带的存档的 NOTES 目录中。加载项中的 VBA 项目已使用我在签名时有效的代码签名证书进行签名。

我通常手动为前两个标签进行颜色编码,如所示;宏不执行此操作,并且我从未费心完全自动化范围名称索引的维护。曾经,我有一个执行所有操作的宏,但我发现它太麻烦了,很早就放弃了。

关注点

将焦点移回 C# 代码,虽然大部分代码都平淡无奇(特别是新的部分),但新代码中有几个值得注意的特性,以及现有代码中有几个值得您关注的特性。

重新排列 string.Format 生成的输出

ShowTransitionTimeDetails 方法最初是一个普通的字符串格式化器,被提取到一个自己的单语句方法中,因为需要两次使用相同的语句来报告每个包含两个 TransitionTime 结构的调整规则的完整内容。

        private static string ShowTransitionTimeDetails ( TimeZoneInfo.TransitionTime ptzTransitionTime )
        {
            return string.Format (
                "IsFixedDateRule = {0}, Month = {3}, Week = {5}, Day = {1} ({2}), TimeOfDay = {4}" ,
                new object [ ]
                {
                    ptzTransitionTime.IsFixedDateRule ,                        // Format Item 0 = IsFixedDateRule
                    ptzTransitionTime.Day ,                                    // Format Item 1 = Day
                    ptzTransitionTime.DayOfWeek ,                            // Format Item 2 = DayOfWeek
                    ptzTransitionTime.Month ,                                // Format Item 3 = Month
                    ptzTransitionTime.TimeOfDay ,                            // Format Item 4 = TimeOfDay
                    ptzTransitionTime.Week                                    // Format Item 5 = Week
                } );
        }    // ShowTransitionTimeDetails

其值得注意的特性是,当前格式控制字符串是修改后的版本,其字段显示顺序与原始版本不同。但是,由于格式项是根据它们在参数数组中的位置(下标)匹配的,因此数组不受影响。这比 C 运行时库中的 printf 函数的限制有了受欢迎的改进,它确实让我想知道是否有人考虑过将 string.Format 移植到 C。

健壮的命令行参数解析

我创建的几乎所有 Windows 程序,即使是许多具有图形用户界面并在 Windows 子系统中运行的程序,都接受命令行参数。虽然这并不罕见,但我投入到我使用的健壮命令行解析引擎中的工作可能并不罕见。下图显示了实例化时区实验室的命令行引擎的方法调用。

            //  ----------------------------------------------------------------
            //    After I created the first version of this assembly, I realized
            //    that this array is used once only, and may as well be built when
            //    it is needed, and discarded when the CmdLneArgsBasic constructor
            //    returns.
            //  ----------------------------------------------------------------

            CmdLneArgsBasic cmdArgs = new CmdLneArgsBasic (
                new char [ ] { SW_OUTPUT } ,                                    // This program supports one switch argument, which is specified by way of a disposable single-element array.
                CmdLneArgsBasic.ArgMatching.CaseInsensitive );                    // Use the Case Insensitive parsing rules.
            cmdArgs.AllowEmptyStringAsDefault = CmdLneArgsBasic.BLANK_AS_DEFAULT_ALLOWED;    // This property cannot be set by the constructor.

由于此程序集具有狭窄的焦点和很少的选项,因此它对命令解析引擎的要求很少。构造函数定义了一个开关 SW_OUTPUT,并指定参数匹配应不区分大小写。其余参数是位置参数。由于 CmdLneArgsBasic 对象默认支持多达九个这样的参数,因此处理它所需的最多两个参数不需要其他操作。CmdLneArgsBasic 类是我最近在 GitHub 上开源的 WizardWrx.DLLServices2.dll 导出的类之一。存储库 URL 是 https://github.com/txwizard/DLLServices2。该库在三句 BSD 许可下可用,我很乐意有人协助以多种方式扩展它。由于它依赖于三个 32 位本机函数,将库限制在 32 位地址空间,我认为该库最有价值的功能设置在项目的第一个已打开的问题中(由我提出),您可以在 https://github.com/txwizard/DLLServices2/issues/1 查看。

抑制横幅

虽然此程序集实际上不需要它,因为它的开发是从一个已经包含它的模板开始的,但它具有一个功能,可以抑制部分或全部控制台输出。由于做出此决定的例程可能需要创建一条警告消息,直到横幅之后才需要显示,因此它通过 out 参数(类型为 string)返回消息。下面所示的代码块处理此命令行选项,该选项恰好是唯一支持的开关。根据结果,如果输出选项允许,则显示横幅。接下来,如果 SetOutputFormat 在其 out 参数中留下了消息,则显示该消息。最后,以输出字符串定义开始的代码块关闭,允许字符串超出范围并被垃圾回收。由于 OutputFormat 必须在此块的生命周期内存在,因此它在执行进入该块之前定义和初始化。

            OutputFormat enmOutputFormat = OutputFormat.None;                    // enmOutputFormat needs method scope, but initialization happens inside a scope block.
            {    // Confine the scope of strDeferredMessage without extracting this little block into a separate method.
                string strDeferredMessage = null;

                //  ----------------------------------------------------------------
                //    Short of returning arrays of objects, there is no way for a
                //    method to return two or more values. Since the value that is of
                //    primary interest is the OutputFormat, while strDeferredMessage
                //    goes unused most of the time, since it is reserved for reporting
                //    exceptional circumstances that can wait until after the banner
                //    is displayed, it plays second fiddle. For a method, this means
                //    that it becomes an Out parameter. Since strings are immutable,
                //    and the object is to let SetOutputFormat create a message to be
                //    displayed later, the parameter must be an Out parameter, rather
                //    than a conventional object reference.
                //  ----------------------------------------------------------------

                enmOutputFormat = SetOutputFormat (
                    cmdArgs ,
                    ref strDeferredMessage );

                if ( enmOutputFormat != OutputFormat.None )
                {   // Unless output is suppressed, display the standard BOJ message.
                    s_theApp.DisplayBOJMessage ( );
                }   // if ( enmOutputFormat != OutputFormat.None )

                if ( !string.IsNullOrEmpty ( strDeferredMessage ) )
                {   // SetOutputFormat saves its error message, if any, in SetOutputFormat.
                    Console.WriteLine ( strDeferredMessage );
                }   // if ( !string.IsNullOrEmpty ( s_strDeferredMessage ) )
            }    // String strDeferredMessage goes out of scope, allowing any memory that it appropriated to be reclaimed.

SetOutputFormat 方法将其字符串输入解析为一种美观、紧凑的枚举类型,这种类型在处理和传递时效率更高。

        private static OutputFormat SetOutputFormat (
            CmdLneArgsBasic pcmdArgs ,
            ref string pstrDeferredMessage )
        {
            //  ----------------------------------------------------------------
            //  An invalid input value elicits a message similar to the following.
            //
            //      Requested value 'Foolish' was not found.
            //
            //  The simplest way to report an invalid value is by extracting it
            //  from the Message property of the ArgumentException thrown by the
            //  Enum.Parse method.
            //
            //  I happen to have a library routine, ExtractBoundedSubstrings,
            //  which became part of a sealed class, WizardWrx.StringTricks,
            //  exported by class library WizardWrx.SharedUtl2.dll version 2.62,
            //  which came into being exactly two years ago, 2011/11/23.
            //  ----------------------------------------------------------------

            const bool IGNORE_CASE = true;
            const int NONE = 0;

            OutputFormat renmOutputFormat = OutputFormat.Verbose;

            //  ----------------------------------------------------------------
            //  Enum.Parse needs a try/catch block, because an invalid SW_OUTPUT
            //  value raises an exception that can be gracefully handled without
            //  killing the program.
            //  ----------------------------------------------------------------

            try
            {
                if ( pcmdArgs.ValidSwitchesInCmdLine > NONE )
                {
                    renmOutputFormat = ( OutputFormat ) Enum.Parse (
                        typeof ( OutputFormat ) ,
                        pcmdArgs.GetSwitchByName (
                            SW_OUTPUT ,
                            OutputFormat.Verbose.ToString ( ) ) ,
                        IGNORE_CASE );
                }   // if ( pcmdArgs.ValidSwitchesInCmdLine > NONE )
            }
            catch ( ArgumentException exArg )
            {   // Display of the message is deferred until the BOJ message is printed.
                s_theApp.BaseStateManager.AppExceptionLogger.ReportException ( exArg );

                pstrDeferredMessage = string.Format (
                    Properties.Resources.ERRMSG_INVALID_OUTPUT_FORMAT ,
                    WizardWrx.StringTricks.ExtractBoundedSubstrings (
                        exArg.Message ,
                        SpecialCharacters.SINGLE_QUOTE ) ,
                    renmOutputFormat ,
                    Environment.NewLine );
            }

            return renmOutputFormat;
        }   // private static OutputFormat SetOutputFormat

我更喜欢在 switch 块中使用枚举而不是字符串,原因有两个。

  1. 枚举的处理效率极高。我见过一些实现了 switch 语句作为跳转表的调试代码。这已经是最优的了!
  2. 要在 switch 块中使用字符串,必须是硬编码到语句中的文字量或常量。资源字符串不被视为常量,也不能使用。

可能还有一两个我忽略的亮点。有人告诉我,阅读我的代码就像剥洋葱一样,但没有眼泪。尽情享受吧!

历史

2016 年 6 月 16 日星期四是本文的第一个版本。

© . All rights reserved.