@T.S.在 cmets 中指出,除了教育目的之外,很少有理由重新发明轮子。在编写解析器时,正是那些极端情况让我们很难做到正确:
- 格式验证,更具体地说是返回友好
给用户的消息
- 优化代码的性能、分配、并发性……
也就是说,有了 C# 7 对模式匹配的支持,基本的管道应该很容易实现。
第一站,Node 类,添加初始化。
public class Node
{
public Node(string name)
{
Name = name;
InnerNodes = new List<Node>();
}
public string Name { get; }
public string Contents { get; set; }
public List<Node> InnerNodes { get; }
}
接下来,为我们语法的不同部分创建包装器。
internal abstract class Token
{
}
internal class OpenNodeToken : Token
{
public OpenNodeToken(string name) { Name = name; }
public string Name { get; }
}
internal class CloseNodeToken : Token
{
}
internal class ContentToken : Token
{
public ContentToken(string text) { Text = text; }
public string Text { get; }
}
还有一个帮助类,用于将我们的输入字符串转换为标记序列。
internal static class Tokenizer
{
public static IEnumerable<Token> Scan (string expression)
{
var words = new Queue<string>
(expression.Split(new[] { ' ', '\n', '\r', '\t' }, StringSplitOptions.RemoveEmptyEntries));
while (words.Any())
{
string word = words.Dequeue();
switch (word)
{
case "id":
yield return new OpenNodeToken(words.Dequeue());
words.Dequeue();
break;
case "}":
yield return new CloseNodeToken();
break;
default:
yield return new ContentToken(word);
break;
}
}
}
}
我使用了Queue 以便一次性将多个字符串出列以处理id 的情况。
最后是解析器,其中模式匹配允许简洁的代码。
public static class Parser
{
private static Node currNode;
private static Stack<Node> prevNodes;
private static IEnumerable<Token> tokens;
static Parser()
{
prevNodes = new Stack<Node>();
}
public static Node Deserialize(string input)
{
tokens = Tokenizer.Scan(input);
if (!(tokens.FirstOrDefault() is OpenNodeToken rootToken))
throw new FormatException("Missing root node");
currNode = new Node(rootToken.Name);
foreach(Token token in tokens.Skip(1))
{
switch (token)
{
case ContentToken c:
string s = string.IsNullOrEmpty(currNode.Contents) ? c.Text : " " + c.Text;
currNode.Contents += s;
break;
case OpenNodeToken n:
prevNodes.Push(currNode);
currNode = new Node(n.Name);
break;
case CloseNodeToken c:
if (prevNodes.Any())
{
Node childNode = currNode;
currNode = prevNodes.Pop();
currNode.InnerNodes.Add(childNode);
}
break;
default: throw new NotImplementedException(token.GetType().Name);
}
}
return currNode;
}
}
在这里,我们使用Stack 将父节点推送到解析子节点之前。一旦满足子元素的右括号,我们弹出堆栈的父元素并将子元素添加到它的集合中。
如前所述,一个体面的解析器也应该涵盖极端情况,我将实现这些的乐趣留给了读者。为了测试解析器,我在下面设置了控制台项目。
namespace MySimpleParser
{
class Program
{
public static void Main(string[] args)
{
string s = GetInput();
try
{
Node root = Parser.Deserialize(s);
PrintBranch(root, 1);
}
catch (Exception ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
Console.ReadLine();
}
internal static string GetInput()
{
return @"id i {
any data; any [con=tent]
id j {
any inner data
}
id k {
bla
id m {
any
}
thing
}
}";
}
internal static void PrintNode(Node n, int depth)
{
string indent = new string('-', 3 * depth);
Console.WriteLine($"{indent} Name: {n.Name}");
Console.WriteLine($"{indent} Contents: {n.Contents}");
Console.WriteLine($"{indent} Child Nodes: {n.InnerNodes.Count}");
}
internal static void PrintBranch(Node root, int depth)
{
PrintNode(root, depth);
foreach (Node child in root.InnerNodes) PrintBranch(child, depth + 1);
}
}
}
输出
--- Name: i
--- Contents: any data; any [con=tent]
--- Child Nodes: 2
------ Name: j
------ Contents: any inner data
------ Child Nodes: 0
------ Name: k
------ Contents: bla thing
------ Child Nodes: 1
--------- Name: m
--------- Contents: any
--------- Child Nodes: 0