【问题标题】:Rewrite IsHexString method with RegEx用 RegEx 重写 IsHexString 方法
【发布时间】:2012-02-29 20:04:42
【问题描述】:

我有一个方法检查 字符串是否 有效的十六进制字符串:

public bool IsHex(string value)
{
  if (string.IsNullOrEmpty(value) || value.Length % 2 != 0)
    return false;

  return 
    value.Substring(0, 2) == "0x" &&
    value.Substring(2)
      .All(c => (c >= '0' && c <= '9') ||
                (c >= 'a' && c <= 'f') ||
                (c >= 'A' && c <= 'F'));
}

规则是:
表达式必须由偶数个十六进制数字(0-9、AF、af)组成。
字符0x必须是前两个表达式中的字符。

我确信它可以用正则表达式重写,以更简洁、更有效的方式。
你能帮我解决这个问题吗?

【问题讨论】:

  • 我不知道效率,但它肯定是一个班轮。不要假设 Regex 会比您的实现更有效。
  • 你的方法不检查它是否包含一个十六进制字符串,它检查它是否是一个十六进制字符串和一个十六进制字符串只有。细微的差别。另外,它只允许长度为 2 的倍数的十六进制值。
  • @Abel,你是对的。在原始帖子中编辑。
  • 您当前的代码允许 0x 作为有效的十六进制字符串。这是故意的吗?
  • @Oded:David 的当前方法总是遍历 all 个字符。正则表达式只会在必要时这样做。如果第一个字符不是0,它将立即停止。此外,正则表达式引擎在可能的情况下使用直接查找表(意思是:当前字符不在查找中,然后失败,这比多个 if 更快)并且它向前跳跃尽可能大。很难击败一个好的正则表达式。有关正则表达式引擎如何深入工作,请参阅 Jeffery Friedl 的 Mastering Regular Expressions。

标签: c# .net regex hex


【解决方案1】:

所以,基本上你想检查数字是否以0x 开头并以0-9 和/或A-F 的(非空)序列继续。这可以很容易地指定为正则表达式:

return RegEx.IsMatch(value, "^0x[0-9A-Fa-f]+$")

我不知道你为什么要检查value.Length % 2 != 0...“0x1”不是一个有效的十六进制数字吗?此外,我的函数在"0x" 上返回false,而您的函数将返回true。如果您想更改它,请将正则表达式中的 +(= 一个或多个)替换为 *(= 零个或多个)。

编辑:既然您已经证明了您的“偶数”要求,我建议您使用 Abel 的 RegEx。如果你这样做,我建议你调用你的方法IsMsSqlHex 或类似的东西来证明它不遵循“通常的”十六进制规则。

【讨论】:

  • 它应该遵循 mssql 转换为二进制文档中的规则。 (添加到原帖)
  • 您可能想要添加^ 和$,否则它的行为与上面的代码不同。也许你甚至应该做^0x([0-9A-Fa-f]{2})+$
  • @Abel:谢谢,我添加了 ^ 和 $。我暂时不会添加 {2},因为这个要求对我来说真的没有意义。
  • 我想指出,这个答案的性能特征很差,因为您没有缓存解析表达式所涉及的工作,或者从表达式创建的编译程序集。最好为您在编译时知道的正则表达式创建一个static readonly Regex。
  • @JonathanDickinson:“非常差”可能言过其实。根据输入,我尝试了您的建议并设法获得了 5-15% 的整体收益。不是可以忽略不计,但也不是很多。
【解决方案2】:

更新问题后,适合您的新正则表达式应该是:

^0x(?:[0-9A-Fa-f]{2})+$

我使用(?: 进行非捕获分组以提高效率。 {2} 表示您想要两个前面的表达式(即两个十六进制字符),+ 表示您想要一个或多个十六进制字符。请注意,这不允许将 0x 作为有效值。

效率

“Oded”提到了效率。我不知道你的要求,所以我认为这比其他任何事情都更能锻炼大脑。只要最小的匹配正则表达式,正则表达式就会飞跃。例如,在 10,000 个大小为 50-5000 个字符的可变输入字符串上尝试我自己的正则表达式,全部正确,它在 1.1 秒内运行。

当我尝试以下正则表达式时:

^0x(?:[0-9A-Fa-f]{32})+(?:[0-9A-Fa-f]{2})+$

在 0.67 秒内,它的运行速度提高了约 40%。但小心点。了解您的输入就是了解如何编写高效的正则表达式。例如,如果正则表达式失败,它将进行大量回溯。如果我的一半输入字符串的长度不正确,对于相同的输入,运行时间会激增到大约 34 秒或 3000% (!)。

如果大多数输入字符串都很大,这将变得更加棘手。如果 99% 的输入都是有效长度,那么所有字符都是 > 4130 个字符,只有少数不是,写

^0x(?:[0-9A-Fa-f]{4096})+^0x(?:[0-9A-Fa-f]{32})+(?:[0-9A-Fa-f]{2})+$

效率更高,更能节省时间。但是,如果许多人的length % 2 = 0 不正确,则由于回溯,这会适得其反。

最后,如果你的大多数字符串都满足偶数规则,并且只有部分或多个字符串包含错误字符,那么速度就会提高:包含错误字符的输入越多,性能就越好。也就是说,因为当它发现一个无效字符时,它可以立即爆发。

结论:如果您的输入混合了小、大、错误字符、错误计数,您最快的方法是使用检查字符串长度(在 .NET 中为瞬时)和使用高效正则表达式的组合。

【讨论】:

    【解决方案3】:

    谩骂:如果您完全关心速度,请忘记正则表达式。正则表达式是 NFA,因此在大多数情况下,它比 DFA 或手写解析器慢。

    忽略您在这里要求使用正则表达式可能会更有效(即使您的实现可能很好 - 它确实分配了字符串):

    static bool IsHex(string value)
    {
        if (string.IsNullOrEmpty(value) || value.Length < 3)
            return false;
    
        const byte State_Zero = 0;
        const byte State_X = 1;
        const byte State_Value = 2;
    
        var state = State_Zero;
    
        for (var i = 0; i < value.Length; i++)
        {
            switch (value[i])
            {
                case '0': 
                    {
                        // Can be used in either Value or Zero.
                        switch (state)
                        {
                            case State_Zero: state = State_X; break;
                            case State_X: return false;
                            case State_Value: break;
                        }
                    }
                    break;
                case 'X': case 'x': 
                    {
                        // Only valid in X.
                        switch (state)
                        {
                            case State_Zero: return false;
                            case State_X: state = State_Value; break;
                            case State_Value: return false;
                        }
                    }
                    break;
                case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
                case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
                case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
                    {
                        // Only valid in Value.
                        switch (state)
                        {
                            case State_Zero: return false;
                            case State_X: return false;
                            case State_Value: break;
                        }
                    }
                    break;
                default: return false;
            }
        }
    
        return state == State_Value;
    }
    

    如果我能正确了解您想要实现的目标,那么此功能可能会更好地满足您的需求:

    static bool ParseNumber(string value, out int result)
    {
        if (string.IsNullOrEmpty(value))
        {
            result = 0;
            return false;
        }
    
        if (value.StartsWith("0x"))
            return int.TryParse(value.Substring(2), NumberStyles.AllowHexSpecifier, null, out result);
        else
            return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
    }
    

    只是为了好玩I went and profiled:

    我的笔记本电脑上的结果(发布/无调试器):

    • 没有编译/缓存的正则表达式 正则表达式耗时 8137 毫秒(2 倍缓存,20 倍手写)
    • 带有已编译/缓存的 Regex 的 Regex 耗时 3463 毫秒(8 倍手写)
    • 手写耗时 397 毫秒 (1x)

    【讨论】:

    • 非常有趣的阅读,+1。奇怪的是,您设法通过编译的正则表达式获得了如此多的性能提升。我对 10,000 个字符串的测试集(每个字符串 50-5000 个和 2% 错误)的测量结果显示您的手写 200 毫秒,我优化的正则表达式编译静态 780 毫秒,编译堆栈 630 毫秒,未编译 640 毫秒。显然,编译并不总是有帮助(MS 声明它无论如何都会被缓存)。
    • 通常我会完全同意你的观点,但它根本没有改善:在这种情况下,Debug 或 Release 的配置似乎无关紧要。虽然我确实注意到编译的正则表达式有错误,但忘记了RegexOptions.Compiled。这从 640 提高到 560 毫秒(调试或发布)。尽管如此,你的手工方法是迄今为止最快的;)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-01-04
    • 1970-01-01
    • 2020-05-30
    • 1970-01-01
    • 2012-09-19
    • 1970-01-01
    • 2010-10-16
    相关资源
    最近更新 更多