更有效的方法来确定字符串是否以一组令牌中的令牌开头?

我现在正在做一些我现在正在做的代码:

public CommandType GetCommandTypeFromCommandString(String command) { if(command.StartsWith(CommandConstants.Acknowledge)) return CommandType.Acknowledge; else if (command.StartsWith(CommandConstants.Status)) return CommandType.Status; else if (command.StartsWith(CommandConstants.Echo)) return CommandType.Echo; else if (command.StartsWith(CommandConstants.Warning)) return CommandType.Warning; // and so on return CommandType.None; } 

我想知道在C#中是否有更有效的方法。 这段代码需要每秒执行很多次,而且我对完成所有这些字符串比较所花费的时间不太满意。 有什么建议? 🙂

一种优化方法是使用StringComparison枚举来指定您只需要进行序数比较。 像这样:

 if(command.StartsWith(CommandConstants.Acknowledge, StringComparison.Ordinal)) return CommandType.Acknowledge; 

如果您没有指定字符串比较方法,那么当前文化将用于比较,这会使事情变慢。

我做了一些(真的很天真)基准测试:

 var a = "foo bar foo"; var b = "foo"; int numTimes = 1000000; Benchmark.Time(() => a.StartsWith(b, StringComparison.Ordinal), "ordinal", numTimes); Benchmark.Time(() => a.StartsWith(b), "culture sensitive", numTimes); 

其中产生了以下结果:

序数在35.6033毫秒内运行了1000000次
文化敏感度在175.5859毫秒内运行了1000000次

您还应该对比较进行排序,以便首先比较最可能的标记(快乐路径)。

Theese优化是一种简单的方法,可以使您当前的实现更好,但如果性能真的很关键(我的意思是非常关键),那么您应该考虑实现某种状态机。

在概念上与Vojislav的FSM答案类似,你可以尝试将常数放入trie中 。 然后,您可以使用trie的单次遍历替换您的比较序列。

这里描述了一个C#trie实现。

这是相当多的工作,但你可以根据每个令牌构建一个FSM 。 FSM将逐个接受命令字符串中的字符; 对于每个令牌,它将具有一个最终状态,并且当命令不以任何令牌开始时,将具有另外的最终状态。

我认为你可以用正则表达式和词典做得更好:

 static Regex reCommands = new Regex("^(cmd1|cmd2|cmd3|cmd4)", RegexOptions.Compiled); static Dictionary Commands = new Dictionary(); private static InitDictionary() { Commands.Add("cmd1", cmdType1); Commands.Add("cmd2", cmdType2); Commands.Add("cmd3", cmdType3); Commands.Add("cmd4", cmdType4); } public CommandType GetCommandTypeFromCommandString(String command) { Match m = reCommands.Match(command); if (m.Success) { return Commands[m.Groups[1].Value]; } return CommandType.None; // no command } 

我认为你应该考虑更多的可读性而不是担心效率。 这些操作非常快。 我是第二个塞尔,这部分代码不太可能成为瓶颈。 我会做这样的事情:

 public CommandType GetCommandTypeFromCommandString(String command) { for(String currentCommand : allCommands) { if(command.StartsWith(currentCommand)) return currentCommand; } return CommandType.None; } 

编辑:作为事后的想法,如果您知道哪些命令最常使用,您可以订购您的arrays,以便这些命令在开始…如果您保留它们,也可以使用if语句执行此操作。

编辑:鉴于对StopWatch的一个警告的误解,我的原始答案与StringComparison.Ordinal结合使用时表现不如StartsWith。 即使您使用所有正确的选项编译正则表达式,它也会慢一点,其性能可与使用StartsWith而不使用任何StringComparison设置相媲美。 但是,正则表达式路径确实可以让您更灵活地匹配模式,而StartsWith却没有,所以我为后代留下了我的原始答案……

原答案:

我不得不承认,我不确定你到底在寻找什么 – 但是,在我看来,这种代码通常会解析一些描述的日志文件,以寻找有用的信息。 为了节省您长时间进行所有字符串比较,您可以在枚举中生成值的正则表达式,然后使用匹配的项解析正确的枚举项:

 public enum CommandType { Acknowledge, Status, Echo, Warning } static public CommandType? GetCommandTypeFromString(String command) { var CommandTypes = String.Join("|", Enum.GetNames(typeof(CommandType))); var PassedConstant = Regex.Match(command, String.Format("(?i:^({0}))", CommandTypes)).Value; if (PassedConstant != String.Empty) return (CommandType)Enum.Parse(typeof(CommandType), PassedConstant, true); return null; } static void Main(string[] args) { Console.WriteLine(GetCommandTypeFromString("Acknowledge that I am great!").ToString()); } 

这将从我的字符串的开头拉出CommandType.Acknowledge,但只有当它存在于字符串的开头时…它还会拉出其他正确的CommandTypes。

对接受的答案进行类似的基准测试,我的性能提升了大约40%。 我在10421ms内运行了接受的代码超过一百万次迭代,但我的运行时间只有6459ms。

当然,虽然您使用的if语句可能看起来不如您所希望的那样高效,但它仍然比使用正则表达式更容易阅读…

创建一个

 IDictionary 

并用值填充它。

你不需要比较所有东西……只需在表格中查找即可。

您还需要更好地定义命令语法。 在命令和行的其余部分之间需要一个空格,例如……

“多少次”多少次? 我严重怀疑这部分代码是瓶颈。 你可以用一个基于每个命令的第一个字母的switch语句来优化它,假设它们都是不同的。

但话说回来,这真的很有用吗? 我不会打赌它太多了。

我做了类似的扩展方法:

 public static bool StartsWith(this string s, params string[] candidate) { string match = candidate.FirstOrDefault(t => s.StartsWith(t)); return match != default(string); } 

现在这只是一个谓词,它返回数组中的字符串是否以给定字符串开头,但您可以稍微修改它:

 public static int PrefixIndex(this string s, params string[] candidate) { int index = -1; string match = candidate.FirstOrDefault(t => { index++; return s.StartsWith(t); }); return match == default(string) ? -1 : index; } 

在使用中它将是:

 int index = command.PrefixIndex(tokenStrings); if (index >= 0) { // convert to your enum } 

在评论中,我看到你想在1/40秒内进行30次字符串比较。 我的朋友,你应该可以在1 MHz的机器上做到这一点。 在1/40秒内进行数千次字符串比较应该没有汗水。

特里肯定会成为最快的方法。 如果前缀长度相同,那么您可以通过散列前缀来获得更快的实现来获取数组的索引,但如果您不知道要散列多少字节,则会出现故障。

注意:我没有演示如何使用exception处理来控制程序流。 如果Enum的字符串名称不存在,Enum.Parse将抛出exception。 Catch子句只返回默认的CommandType为None,就像提问者的示例代码一样。

如果对象只是为了返回实际的Enum对象给定字符串名称,则不能使用:

 try { return (CommandType)Enum.Parse(typeof(CommandType), strCmdName, true); } catch (Exception) { return CommandType.None; }