我有一个应用程序,它以strings的形式接受“命令”,这些命令被解析,然后根据字符串中的第一个单词传递给一个特定的类。
每个string命令都由由空格分隔的两个或三个单词组成,还可以有一个'=‘,后面跟着用逗号分隔的更多单词/数字。
下面是一些例子:
add layer layerName=path,style -带有=的命令将遵循这种格式。=前有三个“部件”,后面有任意数量的“部件”,用逗号隔开。
assign database C:\temp folder\temp.mdb -没有=的命令将遵循这种格式。最多三个“部分”。
现在,我有一个List,它由所有可能的“第一”单词组成,它们是动作动词。
我将string命令解析为一个string[],并打开第一个元素以确定要调用的正确函数。
下面是包含可能的动作动词的List (为了更好的可读性而缩短):
//list of all types of commands. Add to this list if more commands are created.
private static readonly List<string> Commands = new List<string>
{"add", "assign", "cancel", "reload"}; //the actual list has ~50 strings
private static readonly string[] EmptyString = { string.Empty };下面是我的解析函数:
/// <summary>
/// Given a command string, will parse it
/// </summary>
public static void StartParse(string command)
{
if (string.IsNullOrWhiteSpace(command)) //make sure the command isn't empty
{
Log("Command is empty.");
return;
}
string textToParse = command;
if (textToParse.Contains('='))
{
string[] parts = textToParse.Split(new[] { '=' }, 2); //split the command into two parts
string[] secondPart;
//special case, only one command will have this format:
//ex: add layer layerName=filepath| ( OBJECTID = 10 ) OR ( OBJECTID = 20),styleName
if (parts[1].Contains('|') && parts[1].Contains('('))
{
string secondParts = "";
for (int i = 1; i < parts.Length; i++)
{
secondParts += parts[i];
}
secondPart = secondParts.Replace(@"""", "").Split(',');
}
else
{
secondPart = parts.Last().Replace(@"""", "").Split(',');
}
string[] firstPart = parts.First().Replace(@"""", "").Split(' '); //get rid of any quotes and split by spaces
//if a filepath is in the first part of a command and contains spaces, it could be split into more than three parts
//Note: if the above is true, the filepath will always start at the THIRD parameter
//ex: use database c:\users\test folder\temppath.mdb=...
if (firstPart.Length > 3)
{
StringBuilder sb = new StringBuilder();
for (int i = 2; i < firstPart.Length; i++) //file path will always start at the third parameter
{
sb.Append(firstPart[i] + " "); //make the different parts of the filepath one string
}
firstPart = new[] { firstPart[0], firstPart[1], sb.ToString() };
}
string[] allParts = MergeArrays(firstPart, secondPart); //make one array out of the two parts of the command
allParts[0] = allParts[0].ToLower(); //make everything lower
//If first element is not in the command list
if (!Commands.Contains(allParts.First()))
{
Log(allParts.First() + " is not a proper command.");
}
CallCommand(allParts);
}
else //if there is no '=' in the command
{
//creates a string array out of the command string, split by spaces
string[] parts = TextParser.ParseText(textToParse, ' ', '"').ToArray();
parts[0] = parts[0].ToLower();
//If first element is not in the command list
if (!Commands.Contains(parts.First()))
{
Log(parts.First() + " is not a proper command.");
}
CallCommand(parts);
}
}下面是使用switch语句确定要调用哪个函数的函数:
/// <summary>
/// Calls the correct command function or sends off a message based on the command verb.
/// The command verb is always the first argument.
/// </summary>
/// <param name="command">An array of strings representing all the arguments of a commmand.</param>
public static void CallCommand(string[] command)
{
string commandVerb = command.First().ToLower();
//shortened for better readability, actual switch has ~50 cases
switch (commandVerb)
{
case "add":
AddCommand.AddObj(command);
break;
case "assign":
AssignCommand.ParseCommand(command);
break;
case "cancel":
CancelCommand.ParseCommand(command);
break;
case "reload":
ReloadCommand.ParseCommand(command);
break;
default:
Log("This command doesn't exist.");
break;
}
}
/// <summary>
/// Merges two arrays into one
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="first"></param>
/// <param name="second"></param>
/// <returns>Returns the merged array</returns>
public static T[] MergeArrays<T>(T[] first, T[] second)
{
T[] result = new T[first.Length + second.Length];
Array.Copy(first, result, first.Length);
Array.Copy(second, 0, result, first.Length, second.Length);
return result;
}下面是我使用的帮助解析器类:
public static class TextParser
{
/// <summary>
/// Parses text given a delimiter and a text qualifier
/// </summary>
public static IEnumerable<string> ParseText(string line, char delimiter, char textQualifier)
{
if (line == null)
yield break;
bool inString = false;
StringBuilder token = new StringBuilder();
for (int i = 0; i < line.Length; i++)
{
var currentChar = line[i];
var prevChar = '\0';
prevChar = i > 0 ? line[i - 1] : '\0';
var nextChar = '\0';
nextChar = i + 1 < line.Length ? line[i + 1] : '\0';
if (currentChar == textQualifier && (prevChar == '\0' || prevChar == delimiter) && !inString)
{
inString = true;
continue;
}
if (currentChar == textQualifier && (nextChar == '\0' || nextChar == delimiter) && inString)
{
inString = false;
continue;
}
if (currentChar == delimiter && !inString)
{
yield return token.ToString();
token = token.Remove(0, token.Length);
continue;
}
token = token.Append(currentChar);
}
yield return token.ToString();
}
}下面是其中一个命令类的示例(它们都是这样设置的):
public static class AssignCommand
{
/// <summary>
/// Starts parsing the command and assigns a tag to a feature object if one is found with the given name
/// </summary>
public static void ParseCommand(string[] command)
{
if (command.Length < 4)
{
Log("Assign command failed: must have at least four arguments total", null)
return;
}
var objectName = command[2];
var tag = command[3];
var feature = GlobalObjects.Map.FindFeatureByName(objectName); //Map is a third party object. GlobalObjects is a global class in the app containing the Map object. I didn't make it, it's been there forever.
if (feature != null)
{
feature.Tag = tag;
}
}
}优先考虑的是尽可能提高速度,因为会有成千上万的这些命令进入。
需要注意的一点是,对于每个动作动词,都有一个类来匹配它来处理命令,这就是在switch语句中看到的。我不包括这些,因为它们很长,但是每个函数都有一个ParseCommand函数,它只是将字符串数组解析成一个对象。我正在考虑为它们创建一个基类,并使用某种泛型将switch语句简化为一行。但不确定这是否真的会提高性能。
另一个想法是使用Dictionary而不是开关语句,Key是动作动词,Value是委托。
发布于 2019-09-25 16:14:40
看起来,您给我们的是一个系统,它接受一个字符串并将其解析为一个语义命令。
关于你展示的代码,我建议你自己建立更少的东西。整个解析系统可能只是一个带有捕获组的(已编译的)正则表达式。我确实建议用字典代替开关语句。所有这些可能会使这个系统变得更小、更容易阅读,并可能使它更具表现力。
尽管如此,这个路由系统(希望如此)并不是性能瓶颈,您需要问的是如何提高性能。首先您需要查看获取命令字符串列表的系统,还需要查看运行这些命令的系统(S)。
您的最终目标可能是建立一个流读取器,为每个循环提供一个异步的、与外部资源连接的共享池。但是我们无法从这里看到大部分的系统,如果只有100 k项需要处理,那就太过分了。
发布于 2019-09-26 04:39:36
StartParse表示也会有它的补充EndParse。但没有。所以这个名字很不幸。StartParse没有返回值。这使得它对消费者来说有点像黑匣子。我希望是返回您解析的命令。“我正在考虑为它们创建一个基类”,如果命令共享足够的状态,->将创建一个基类,但是如果它们共享一些状态/操作,则更倾向于创建一个接口ICommand。StartParse是无效的,所以对参数检查的无声捕获不会引起消费者的注意。更喜欢抛出ArgumentException或ArgumentNullException。.ToLower(); // make everything lower的注释是完全多余的。StartParse调用CallCommand。虽然我认为它应该返回一个命令,但是在解析器中调用方法名CallCommand感觉非常奇怪。在我看来,CallCommand意味着您正在执行一个命令,而不是将一些输入数据解析为一个命令。StartParse的方法体是冗长的。您应该考虑编写更紧凑的代码。使用regex在另一个答案中提供的替代解决方案是一个很好的选择,因为输入语言相当简单。List<string> Commands中的开关大小写不使用该列表来验证,则使用允许谓词CallCommand的上下文没有任何好处。你对commandVerb交换机进行了硬编码。同样,在错误输入时,您会在使用者不知道该命令未被识别的情况下,悄然忽略该错误输入。TextParser也可以使用一些重构。如果您有像currentChar == textQualifier && (prevChar == '\0'这样的循环代码,这应该是一个信号,可以重写代码,只编写一行代码一次。AssignCommand内部使用对全局共享静态数据GlobalObjects.Map的引用。这使得测试和重用类变得困难。考虑使用IoC。与静态解析方法不同,您可以为ICommandParser接口提供一个或多个特定的解析器实现。然后,可以将它们注册到IoC容器中。https://codereview.stackexchange.com/questions/229643
复制相似问题