@ T.S。在评论中指出,除了教育目的,很少有重新发明轮子的好理由。当谈到写作解析器,正是这些极端案例,这使得它很难得到正确:格式
- 验证,更具体的返回友好 消息给用户
- 优化代码的性能,分配,并发性,...
这就是说,在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;
}
}
}
}
我为了离队多串一气呵成的id
情况下使用的Queue
。
最后,解析器,其中模式匹配允许简洁的代码。
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
使用递归算法。 – jdweng
第二个想法写一个解析器......为什么要用这个?使用JSON或YAML。解析器已经写入并可用 –
这里的语法不仅仅是'id NAME {CONTENT}'。您需要提供如何解析此数据的完整详细信息。 – Enigmativity