Listeners
Listeners walk a tree and are notified as nodes are reached. They are the lightweight alternative to processing: a listener has access to a context object it can accumulate results in, which makes it a good fit for building something new out of a tree — a string representation, IL, an evaluated value. Reach for processing instead when the job is to mutate the tree.
Creating a Listener
There are two base classes to inherit from:
Listener<TContext, TNode>listens to every node in the tree.Listener<TContext, TBaseNode, TNode>listens only to nodes of a specific type. Other nodes are ignored, but their descendents are still walked, so the whole tree is visited either way.
Three methods can be overridden to get at the nodes:
| Method | Called |
|---|---|
BeforeListenToNode |
Immediately before a node and its descendents are visited. |
ListenToNode |
When the node itself is visited. |
AfterListenToNode |
Immediately after a node and its descendents have been visited. |
Between them, the before and after methods bracket a whole subtree, which is what you want for anything that nests — opening and closing brackets, pushing and popping a scope, indenting output.
ShouldListenToChildren can be overridden to skip a node's descendents entirely.
Start the walk by calling Listen with the context and the root node:
var context = new FormattingContext();
listener.Listen(context, expression);
return context.Output.ToString();
The context is passed in to Listen rather than created by the listener. That means listeners hold no state of their own between runs and a single instance can be used to walk many trees, including concurrently.
Exceptions are not handled. If a listener throws, the exception escapes from Listen and no further nodes are visited.
Composite Listeners
Walking a tree usually means doing something different for each kind of node. Rather than one listener with a switch over node types, build a CompositeListener<TContext, TBaseNode> from listeners that each handle one type, using the fluent interface from Build:
private static readonly CompositeListener<FormattingContext, Expression> Listener =
CompositeListener<FormattingContext, Expression>
.Build()
.With(new ConstantListener())
.With(new ArrayListener())
.ToListener();
Exactly one listener will ever be used for a node — the one registered for the most specific type that node matches. Registering a listener for a base type therefore gives fallback behaviour for anything more specific that has no listener of its own, and the With overload taking a two parameter listener registers a catch-all for the base node type itself. If no listener matches at all the node is skipped, though its descendents are still visited.
Only one listener can be registered per type, and ToListener throws if no listeners were registered at all. Listeners can share implementation through their own base classes in the usual way.
Example
The Listeners example uses composite listeners to produce a string representation of a tree. The Maths example uses them twice over, to evaluate an expression tree and to compile it.