Предыстория.
Разобрав исходный текст, на выходе образовалось дерево:
Само по себе дерево не имеет ни какого смысла, оно “Деревянное”, смыслом и какой либо ценностью обладает результат анализа (обхода) этого дерева. Для тех кто не готов напрягаться и писать самописные сани по спуску с дерева (например, меня) в antlr4 добавлена возможность получить анализатор почти бесплатно.
1. Visitor
Классика — поведенческий шаблон проектирования. При обходе узлов определяется метод обрабатывающий текущий тип узла, после чего метод вызывается и вот конкретно здесь начинается разработка, а именно анализ пришедшего поддерева.
2. Listener
Новшество, появившееся в четвертой версии. Поведение этого класса уже далеко не классическое (Observer или Publish/Subscribe). В классическом исполнении наблюдается менеджер который оповещает подписчиков о наступлении событий. Поведения рассматриваемого слушателя больше похоже на работу инспектора. Инспектор перед проверкой узла делает заметку “Я проверяю Х узел”, далее идет обход потомков узла, после обхода, которых можно сделать “Заключение о результатах обхода узла Х”.
Практика
Для лучшего понимания происходящего обратимся к автору распознавателя pragprog.com/book/tpantlr2/the-definitive-antlr-4-reference. В книге “The Definitive ANTLR 4 Reference” В разделе 8.2 Translating JSON to XML автор производит трансляцию, используя Listener.
Примеры в книге основаны на JAVA, я с JAVA не знаком, но переводятся на C# без болезненно (вот что значит клонирование best practice).
Для приготовления слушателя нам понадобиться VS, C# proj и JASON.g4 с примерно таким наполнением
grammar JASON;
json: object
| array
;
object
: '{' pair ( ',' pair )* '}' # AnObject
| '{' '}' # EmptyObject
;
pair: STRING':'value;
array
: '['value(','value)*']' # ArrayOfValues
| '['']' # EmptyArray
;
value
: STRING # String
| NUMBER # Atom
| object # ObjectValue
| array # ArrayValue
| 'true' # Atom
| 'false' # Atom
| 'null' # Atom
;
STRING
: '"' ( ESC | ~["\\] )* '"';
fragment ESC: '\\'(["\\/bfnrt]|UNICODE);
fragment UNICODE:'u'HEX HEX HEX HEX;
fragment HEX:[0-9a-fA-F];
NUMBER
: '-'? INT'.'INT EXP? //1.35,1.35E-9,0.3,-4.5
| '-'? INT EXP //1e10-3e4
| '-'? INT //-3,45
;
fragment INT: '0'|[1-9][0-9]*;//noleadingzeros
fragment EXP: [Ee][+\-]? INT;//\-since-means"range"inside[...]
WS : [\t\n\r]+->skip;
Это грамматика позволяющая распознать JASON. В свойствах файла необходимо выставить Generate Listener и Generate Visitor (это еще пригодиться). Результатом работы Слушателя в оригинальном примере из книги является текст xml, меня это не устраивает я буду получать XElement (все равно xml текст нужно будет во что то переводить, хотя плюс текста в том что он не зажимает в рамки использования конкретных классов).
Алгоритм прост: тк antlr4 использует нисходящий разбор (в нашем случае от корня к узлам), то xml будет формироваться точно так же, создание элемента предка к которому будут добавляться потомки.
Пример Слушателя:
class XmlListener : JASONBaseListener
{
#region fields
ParseTreeProperty<XElement> xml = new ParseTreeProperty<XElement>();
#endregion
#region result
public XElement Root { get; private set; }
#endregion
#region xml api
XElement GetXml( IParseTree ctx )
{
return xml.Get( ctx );
}
/// <summary>
/// поиск родительского xml элемента
/// </summary>
XElement GetParentXml( IParseTree ctx )
{
var parent = ctx.Parent;
XElement result = GetXml( parent );
if ( result == null )
result = GetParentXml( parent );
return result;
}
void SetXml( IParseTree ctx, XElement e )
{
xml.Put( ctx, e );
}
#endregion
#region listener
public override void ExitString( JASONParser.StringContext context )
{
var value = GetStringValue( context.STRING() );
AddValue( context, value );
}
public override void ExitAtom( JASONParser.AtomContext context )
{
var value = context.GetText();
AddValue( context, value );
}
public override void EnterPair( JASONParser.PairContext context )
{
var name = GetStringValue( context.STRING() );
XElement element = new XElement( name );
XElement ParentElement = GetParentXml( context );
ParentElement.Add( element );
SetXml( context, element );
}
public override void EnterJson( JASONParser.JsonContext context )
{
Root = new XElement( "JSON" );
SetXml( context, Root);
}
#endregion
#region private
private string GetStringValue( ITerminalNode terminal )
{
return terminal.GetText().Trim( '"' );
}
private void AddValue( ANTLR_CSV.JASONParser.ValueContext context, string value )
{
var parent = GetParentXml( context );
if ( context.Parent.RuleIndex == JASONParser.RULE_array )
{
XElement element = new XElement( "elemnt" );
element.Value = value;
parent.Add( element );
SetXml( context, element );
}
else
parent.Value = value;
}
#endregion
}
EnterJson соответствует входу в узел описанный в грамматике так:
json: object
| array
;
ExitString соответствует выходу из узла описанного в грамматике так:
STRING # String
В отличии от оригинального примера я не использую всех прелестей Enter и Exit. За то есть ParseTreeProperty признанный хранить пары [поддерево, значение], наверное лучше это заменить на обычный словарь (хуже точно не будет).
Пример Посетителя:
class XmlVisitor : JASONBaseVisitor<XElement>
{
#region fields
private XElement _result;
ParseTreeProperty<XElement> xml = new ParseTreeProperty<XElement>();
#endregion
#region xml api
XElement GetXml( IParseTree ctx )
{
return xml.Get( ctx );
}
XElement GetParentXml( IParseTree ctx )
{
var parent = ctx.Parent;
XElement result = GetXml( parent );
if ( result == null )
result = GetParentXml( parent );
return result;
}
void SetXml( IParseTree ctx, XElement e )
{
xml.Put( ctx, e );
}
#endregion
#region visitor
/// <summary>
/// значение по умолчанию - создаваемое дерево xml
/// </summary>
protected override XElement DefaultResult { get { return _result; } }
public override XElement VisitJson( JASONParser.JsonContext context )
{
_result = new XElement( "JSON" );
SetXml( context, _result );
return VisitChildren( context );
}
public override XElement VisitString( JASONParser.StringContext context )
{
var value = GetStringValue( context.STRING() );
AddValue( context, value );
return DefaultResult;
}
public override XElement VisitAtom( JASONParser.AtomContext context )
{
var value = context.GetText();
AddValue( context, value );
return DefaultResult;
}
public override XElement VisitPair( JASONParser.PairContext context )
{
var name = GetStringValue( context.STRING() );
XElement element = new XElement( name );
XElement ParentElement = GetParentXml( context );
ParentElement.Add( element );
SetXml( context, element );
return VisitChildren( context );
}
#endregion
#region private
private string GetStringValue( ITerminalNode terminal )
{
return terminal.GetText().Trim( '"' );
}
private void AddValue( ANTLR_CSV.JASONParser.ValueContext context, string value )
{
var parent = GetParentXml( context );
if ( context.Parent.RuleIndex == JASONParser.RULE_array )
{
XElement element = new XElement( "elemnt" );
element.Value = value;
parent.Add( element );
SetXml( context, element );
}
else
parent.Value = value;
}
#endregion
}
Как говориться “Найдите 10 отличий”, первое отличие VisitJson, управление посещением без вызова VisitChildren( context ) посещение потомков прекращается, а значит и обход. Каждый из методов посещения должен возвращать значение, то есть всегда есть результат посещения, а это удобно:
var result = visitor.Visit( tree );
Когда при работе со слушателем:
walker.Walk( listener, tree );
var result = listener.Root;
В оригинальном примере без Слушателя было бы довольно туго, для данного решения разницы особо нет, но я отдаю свой голос в пользу решения на Посетителе.
Ну и что бы можно было опробовать своими руками:
private static IParseTree CreateTree()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine( "{" );
sb.AppendLine( "\"description\":\"Animaginary server config file\"," );
sb.AppendLine( "\"count\":500," );
sb.AppendLine( "\"logs\":{\"level\":\"verbose\",\"dir\":\"/var/log\"}," );
sb.AppendLine( "\"host\":\"antlr.org\"," );
sb.AppendLine( "\"admin\":[\"parrt\",\"tombu\"]," );
sb.AppendLine( "\"aliases\":[]" );
sb.AppendLine( "}" );
AntlrInputStream input = new AntlrInputStream( sb.ToString() );
JASONLexer lexer = new JASONLexer( input );
CommonTokenStream tokens = new CommonTokenStream( lexer );
JASONParser parser = new JASONParser( tokens );
IParseTree tree = parser.json();
return tree;
}
Пример JSON текста взят практически без изменений из оригинального примера:
private static void ListenerXml()
{
IParseTree tree = CreateTree();
ParseTreeWalker walker = new ParseTreeWalker();
XmlListener listener = new XmlListener();
walker.Walk( listener, tree );
var result = listener.Root;
}
private static void VisitorXml()
{
IParseTree tree = CreateTree();
XmlVisitor visitor = new XmlVisitor();
var result = visitor.Visit( tree );
}
Ну и результат выполнения:
<JSON>
<description>Animaginary server config file</description>
<count>500</count>
<logs>
<level>verbose</level>
<dir>/var/log</dir>
</logs>
<host>antlr.org</host>
<admin>
<elemnt>parrt</elemnt>
<elemnt>tombu</elemnt>
</admin>
<aliases />
</JSON>
Как ни странно, но оба метода выдали одно и то же.
Далее
P.S. Слушатель vs Посетитель – 0: 1
Комментарии (7)
datacompboy
11.06.2015 22:54А теперь для посетителя создаём глубокое дерево…
InWake Автор
14.06.2015 12:53для слушателя то же самое дерево, только слушатель обойдет все дерево (обходом нельзя управлять), а посещением можно управлять, например для оптимизированных bool выражений (target != null && target.Value > 100 — если target == null, то правое условие даже проверять не нужно, что удобно. Выполнение же правой части выдаст ошибку)
datacompboy
14.06.2015 13:13Во-1х, для слушателя совсем не обязательно держать всё дерево в памяти.
Во-2х, слушатель может отписываться когда он закончил, или возвращать подсказки (hint'ы) для обхода (которые, впрочем, автомат может и игнорировать — например, если на одном дереве сейчас параллельно запущено 5 слушателей), впрочем, не вычислять если он уже всё знает он может всегда в любом случае.
В-3их, построить дерево можно линейно, а вот посетитель расходует стек. А стек это такой мерзкий предмет — то он есть, то его нет.жInWake Автор
14.06.2015 14:03с 1м согласен.
Со 2м не согласен. Отписка — ее нет (ну может я плохо искал). Хоте ее можно реализовать самостоятельно (допиливая существующий функционал или написав новый, чего не особо хочется)
с 3м согласен, злоупотреблять стэком это не хорошо.datacompboy
14.06.2015 14:09Если её нет именно у antlr — то это отличный способ внести лепту в опенсорс :)
а вот оптимизация вычислений таки доступна вне зависимости от возможности отписки.
Cobolorum
Вы совершенно не разобрались для чего visitor и listener предназначены, в каких случаях надо применять одно, а в каких другое. И как их правильно комбинировать между собой. У каждого из этих механизмов есть довольно конкретное предназначение и области применения.
InWake Автор
Здесь рассматривается конкретная задача, и ее решение двумя способами. Так же проводиться сравнение двух подходов для решения (да да, я повторяюсь) конкретной задачи. Так же я написал, что если оставить исходную постановку задачи (трансляция JSON в текст XML), по слушатель подойдет лучше.
Сначала подумал «бред», потом все ж пришел к выводу, что это вполне возможно (одно поддерево скушать, а другое посещать). Есть какой ни будь пример (заинтересовало)?А можно как то формализовать области применения одного и другого?