Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Nell'esempio di codice seguente viene illustrato come l'albero delle espressioni che rappresenta l'espressione num => num < 5
lambda può essere scomposto nelle relative parti.
// Add the following using directive to your code file:
// using System.Linq.Expressions;
// Create an expression tree.
Expression<Func<int, bool>> exprTree = num => num < 5;
// Decompose the expression tree.
ParameterExpression param = (ParameterExpression)exprTree.Parameters[0];
BinaryExpression operation = (BinaryExpression)exprTree.Body;
ParameterExpression left = (ParameterExpression)operation.Left;
ConstantExpression right = (ConstantExpression)operation.Right;
Console.WriteLine($"Decomposed expression: {param.Name} => {left.Name} {operation.NodeType} {right.Value}");
// This code produces the following output:
// Decomposed expression: num => num LessThan 5
A questo punto, scriviamo del codice per esaminare la struttura di un albero delle espressioni. Ogni nodo in un albero delle espressioni è un oggetto di una classe derivata da Expression
.
Tale progettazione rende la visita a tutti i nodi in un albero delle espressioni un'operazione ricorsiva relativamente semplice. La strategia generale consiste nell'iniziare nel nodo radice e determinare il tipo di nodo.
Se il tipo di nodo ha figli, visitare ricorsivamente i figli. In ogni nodo figlio, ripetere il processo utilizzato nel nodo radice: determinare il tipo e, se il tipo ha nodi figlio, visitare ognuno di essi.
Esaminare un'espressione senza elementi figlio
Per iniziare, visitare ogni nodo in un albero delle espressioni semplice. Ecco il codice che crea un'espressione costante e quindi esamina le relative proprietà:
var constant = Expression.Constant(24, typeof(int));
Console.WriteLine($"This is a/an {constant.NodeType} expression type");
Console.WriteLine($"The type of the constant value is {constant.Type}");
Console.WriteLine($"The value of the constant value is {constant.Value}");
Il codice precedente stampa l'output seguente:
This is a/an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 24
A questo punto, scriveremo il codice che esaminerebbe questa espressione e scriviamo alcune proprietà importanti su di esso.
Espressione di addizione
Si inizierà con l'esempio di aggiunta dall'introduzione a questa sezione.
Expression<Func<int>> sum = () => 1 + 2;
Annotazioni
Non usarevar
per dichiarare questo albero delle espressioni, perché il tipo naturale del delegato è Func<int>
, non Expression<Func<int>>
.
Il nodo radice è un LambdaExpression
. Per ottenere il codice interessante sul lato destro dell'operatore =>
, è necessario trovare uno dei figli di LambdaExpression
. Fai questo con tutte le espressioni in questa sezione. Il nodo padre ci aiuta a trovare il tipo di ritorno di LambdaExpression
.
Per esaminare ogni nodo in questa espressione, è necessario visitare in modo ricorsivo molti nodi. Ecco una semplice implementazione:
Expression<Func<int, int, int>> addition = (a, b) => a + b;
Console.WriteLine($"This expression is a {addition.NodeType} expression type");
Console.WriteLine($"The name of the lambda is {((addition.Name == null) ? "<null>" : addition.Name)}");
Console.WriteLine($"The return type is {addition.ReturnType.ToString()}");
Console.WriteLine($"The expression has {addition.Parameters.Count} arguments. They are:");
foreach (var argumentExpression in addition.Parameters)
{
Console.WriteLine($"\tParameter Type: {argumentExpression.Type.ToString()}, Name: {argumentExpression.Name}");
}
var additionBody = (BinaryExpression)addition.Body;
Console.WriteLine($"The body is a {additionBody.NodeType} expression");
Console.WriteLine($"The left side is a {additionBody.Left.NodeType} expression");
var left = (ParameterExpression)additionBody.Left;
Console.WriteLine($"\tParameter Type: {left.Type.ToString()}, Name: {left.Name}");
Console.WriteLine($"The right side is a {additionBody.Right.NodeType} expression");
var right = (ParameterExpression)additionBody.Right;
Console.WriteLine($"\tParameter Type: {right.Type.ToString()}, Name: {right.Name}");
In questo esempio viene stampato l'output seguente:
This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 arguments. They are:
Parameter Type: System.Int32, Name: a
Parameter Type: System.Int32, Name: b
The body is a/an Add expression
The left side is a Parameter expression
Parameter Type: System.Int32, Name: a
The right side is a Parameter expression
Parameter Type: System.Int32, Name: b
Si noterà molta ripetizione nell'esempio di codice precedente. Ripuliamo e costruiamo un visitatore di nodo di espressione più generico. Questo ci richiederà di scrivere un algoritmo ricorsivo. Qualsiasi nodo potrebbe essere di un tipo che potrebbe avere figli. Qualsiasi nodo che ha figli richiede di visitare questi figli e determinare che tipo di nodo sia. Ecco la versione pulita che usa la ricorsione per visitare le operazioni di addizione:
using System.Linq.Expressions;
namespace Visitors;
// Base Visitor class:
public abstract class Visitor
{
private readonly Expression node;
protected Visitor(Expression node) => this.node = node;
public abstract void Visit(string prefix);
public ExpressionType NodeType => node.NodeType;
public static Visitor CreateFromExpression(Expression node) =>
node.NodeType switch
{
ExpressionType.Constant => new ConstantVisitor((ConstantExpression)node),
ExpressionType.Lambda => new LambdaVisitor((LambdaExpression)node),
ExpressionType.Parameter => new ParameterVisitor((ParameterExpression)node),
ExpressionType.Add => new BinaryVisitor((BinaryExpression)node),
_ => throw new NotImplementedException($"Node not processed yet: {node.NodeType}"),
};
}
// Lambda Visitor
public class LambdaVisitor : Visitor
{
private readonly LambdaExpression node;
public LambdaVisitor(LambdaExpression node) : base(node) => this.node = node;
public override void Visit(string prefix)
{
Console.WriteLine($"{prefix}This expression is a {NodeType} expression type");
Console.WriteLine($"{prefix}The name of the lambda is {((node.Name == null) ? "<null>" : node.Name)}");
Console.WriteLine($"{prefix}The return type is {node.ReturnType}");
Console.WriteLine($"{prefix}The expression has {node.Parameters.Count} argument(s). They are:");
// Visit each parameter:
foreach (var argumentExpression in node.Parameters)
{
var argumentVisitor = CreateFromExpression(argumentExpression);
argumentVisitor.Visit(prefix + "\t");
}
Console.WriteLine($"{prefix}The expression body is:");
// Visit the body:
var bodyVisitor = CreateFromExpression(node.Body);
bodyVisitor.Visit(prefix + "\t");
}
}
// Binary Expression Visitor:
public class BinaryVisitor : Visitor
{
private readonly BinaryExpression node;
public BinaryVisitor(BinaryExpression node) : base(node) => this.node = node;
public override void Visit(string prefix)
{
Console.WriteLine($"{prefix}This binary expression is a {NodeType} expression");
var left = CreateFromExpression(node.Left);
Console.WriteLine($"{prefix}The Left argument is:");
left.Visit(prefix + "\t");
var right = CreateFromExpression(node.Right);
Console.WriteLine($"{prefix}The Right argument is:");
right.Visit(prefix + "\t");
}
}
// Parameter visitor:
public class ParameterVisitor : Visitor
{
private readonly ParameterExpression node;
public ParameterVisitor(ParameterExpression node) : base(node)
{
this.node = node;
}
public override void Visit(string prefix)
{
Console.WriteLine($"{prefix}This is an {NodeType} expression type");
Console.WriteLine($"{prefix}Type: {node.Type}, Name: {node.Name}, ByRef: {node.IsByRef}");
}
}
// Constant visitor:
public class ConstantVisitor : Visitor
{
private readonly ConstantExpression node;
public ConstantVisitor(ConstantExpression node) : base(node) => this.node = node;
public override void Visit(string prefix)
{
Console.WriteLine($"{prefix}This is an {NodeType} expression type");
Console.WriteLine($"{prefix}The type of the constant value is {node.Type}");
Console.WriteLine($"{prefix}The value of the constant value is {node.Value}");
}
}
Questo algoritmo è la base di un algoritmo che visita qualsiasi oggetto arbitrario LambdaExpression
. Il codice che hai creato cerca solo un piccolo campione dei possibili set di nodi dell'albero delle espressioni che può incontrare. Tuttavia, è comunque possibile imparare un po 'da ciò che produce. Il caso predefinito nel Visitor.CreateFromExpression
metodo stampa un messaggio nella console degli errori quando viene rilevato un nuovo tipo di nodo. In questo modo, si sa di aggiungere un nuovo tipo di espressione.
Quando esegui questa funzione di visita sulla precedente espressione di somma, ottieni il seguente output:
This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
Ora che hai sviluppato un'implementazione del visitatore più generale, puoi visitare ed elaborare molti più tipi di espressioni diversi.
Espressione di addizione con più operandi
Si proverà un esempio più complesso, ma si limitano ancora i tipi di nodo solo all'aggiunta:
Expression<Func<int>> sum = () => 1 + 2 + 3 + 4;
Prima di eseguire questi esempi sull'algoritmo visitor, provare un esercizio di pensiero per determinare cosa potrebbe essere l'output. Tenere presente che l'operatore +
è un operatore binario: deve avere due elementi figlio, che rappresentano gli operandi sinistro e destro. Esistono diversi modi per costruire un albero che potrebbe essere corretto:
Expression<Func<int>> sum1 = () => 1 + (2 + (3 + 4));
Expression<Func<int>> sum2 = () => ((1 + 2) + 3) + 4;
Expression<Func<int>> sum3 = () => (1 + 2) + (3 + 4);
Expression<Func<int>> sum4 = () => 1 + ((2 + 3) + 4);
Expression<Func<int>> sum5 = () => (1 + (2 + 3)) + 4;
È possibile vedere la separazione in due possibili risposte per evidenziare il più promettente. Il primo rappresenta le espressioni associative corrette . Il secondo rappresenta espressioni associative a sinistra . Il vantaggio di entrambi i due formati è che il formato viene ridimensionato a qualsiasi numero arbitrario di espressioni di addizione.
Se si esegue questa espressione tramite il visitatore, viene visualizzato questo output, verificando che l'espressione di addizione semplice venga lasciata associativa.
Per eseguire questo esempio e visualizzare l'albero delle espressioni completo, si apporta una modifica all'albero delle espressioni di origine. Quando l'albero delle espressioni contiene tutte le costanti, l'albero risultante contiene semplicemente il valore costante di 10
. Il compilatore esegue tutta l'addizione e riduce l'espressione alla forma più semplice. È sufficiente aggiungere una variabile nell'espressione per visualizzare l'albero originale:
Expression<Func<int, int>> sum = (a) => 1 + a + 3 + 4;
Creare un visitatore per questa somma ed eseguire il visitatore visualizzato in questo output:
This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 3
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 4
È possibile eseguire uno qualsiasi degli altri esempi tramite il codice visitatore e visualizzare l'albero rappresentato. Ecco un esempio dell'espressione precedente sum3
(con un parametro aggiuntivo per impedire al compilatore di calcolare la costante):
Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);
Ecco l'output del visitatore:
This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The Right argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 3
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
Si noti che le parentesi non fanno parte dell'output. Nell'albero delle espressioni non sono presenti nodi che rappresentano le parentesi nell'espressione di input. La struttura dell'albero delle espressioni contiene tutte le informazioni necessarie per comunicare la precedenza.
Estensione di questo esempio
L'esempio riguarda solo gli alberi delle espressioni più rudimentali. Il codice visualizzato in questa sezione gestisce solo numeri interi costanti e l'operatore binario +
. Come esempio finale, aggiorniamo il visitatore per gestire un'espressione più complessa. È possibile renderlo funzionante per l'espressione fattoriale seguente:Let's make it work for the following factorial expression:
Expression<Func<int, int>> factorial = (n) =>
n == 0 ?
1 :
Enumerable.Range(1, n).Aggregate((product, factor) => product * factor);
Questo codice rappresenta una possibile implementazione per la funzione fattoriale matematica. Il modo in cui è stato scritto questo codice evidenzia due limitazioni della compilazione di alberi delle espressioni assegnando espressioni lambda alle espressioni. Prima di tutto, le espressioni lambda delle istruzioni non sono consentite. Ciò significa che non è possibile usare cicli, blocchi, istruzioni if/else e altre strutture di controllo comuni in C#. L'uso delle espressioni è limitato. In secondo luogo, non è possibile chiamare in modo ricorsivo la stessa espressione. Potresti, se fosse già un delegato, ma non puoi chiamarlo nella forma dell'albero delle espressioni. Nella sezione sulla creazione di alberi delle espressioni vengono illustrate le tecniche per superare queste limitazioni.
In questa espressione vengono visualizzati nodi di tutti questi tipi:
- Uguale (espressione binaria)
- Moltiplicazione (espressione binaria)
- Condizionale (espressione
? :
) - Espressione di chiamata al metodo (chiamata
Range()
eAggregate()
)
Un modo per modificare l'algoritmo visitor consiste nel continuare a eseguirlo e scrivere il tipo di nodo ogni volta che si raggiunge la default
clausola. Dopo alcune iterazioni, hai visto ciascuno dei potenziali nodi. Allora, hai tutto quello di cui hai bisogno. Il risultato sarà simile al seguente:
public static Visitor CreateFromExpression(Expression node) =>
node.NodeType switch
{
ExpressionType.Constant => new ConstantVisitor((ConstantExpression)node),
ExpressionType.Lambda => new LambdaVisitor((LambdaExpression)node),
ExpressionType.Parameter => new ParameterVisitor((ParameterExpression)node),
ExpressionType.Add => new BinaryVisitor((BinaryExpression)node),
ExpressionType.Equal => new BinaryVisitor((BinaryExpression)node),
ExpressionType.Multiply => new BinaryVisitor((BinaryExpression) node),
ExpressionType.Conditional => new ConditionalVisitor((ConditionalExpression) node),
ExpressionType.Call => new MethodCallVisitor((MethodCallExpression) node),
_ => throw new NotImplementedException($"Node not processed yet: {node.NodeType}"),
};
E ConditionalVisitor
MethodCallVisitor
elabora questi due nodi:
public class ConditionalVisitor : Visitor
{
private readonly ConditionalExpression node;
public ConditionalVisitor(ConditionalExpression node) : base(node)
{
this.node = node;
}
public override void Visit(string prefix)
{
Console.WriteLine($"{prefix}This expression is a {NodeType} expression");
var testVisitor = Visitor.CreateFromExpression(node.Test);
Console.WriteLine($"{prefix}The Test for this expression is:");
testVisitor.Visit(prefix + "\t");
var trueVisitor = Visitor.CreateFromExpression(node.IfTrue);
Console.WriteLine($"{prefix}The True clause for this expression is:");
trueVisitor.Visit(prefix + "\t");
var falseVisitor = Visitor.CreateFromExpression(node.IfFalse);
Console.WriteLine($"{prefix}The False clause for this expression is:");
falseVisitor.Visit(prefix + "\t");
}
}
public class MethodCallVisitor : Visitor
{
private readonly MethodCallExpression node;
public MethodCallVisitor(MethodCallExpression node) : base(node)
{
this.node = node;
}
public override void Visit(string prefix)
{
Console.WriteLine($"{prefix}This expression is a {NodeType} expression");
if (node.Object == null)
Console.WriteLine($"{prefix}This is a static method call");
else
{
Console.WriteLine($"{prefix}The receiver (this) is:");
var receiverVisitor = Visitor.CreateFromExpression(node.Object);
receiverVisitor.Visit(prefix + "\t");
}
var methodInfo = node.Method;
Console.WriteLine($"{prefix}The method name is {methodInfo.DeclaringType}.{methodInfo.Name}");
// There is more here, like generic arguments, and so on.
Console.WriteLine($"{prefix}The Arguments are:");
foreach (var arg in node.Arguments)
{
var argVisitor = Visitor.CreateFromExpression(arg);
argVisitor.Visit(prefix + "\t");
}
}
}
L'output per l'albero delle espressioni sarà:
This expression is a/an Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef: False
The expression body is:
This expression is a Conditional expression
The Test for this expression is:
This binary expression is a Equal expression
The Left argument is:
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef: False
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 0
The True clause for this expression is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
The False clause for this expression is:
This expression is a Call expression
This is a static method call
The method name is System.Linq.Enumerable.Aggregate
The Arguments are:
This expression is a Call expression
This is a static method call
The method name is System.Linq.Enumerable.Range
The Arguments are:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef: False
This expression is a Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 arguments. They are:
This is an Parameter expression type
Type: System.Int32, Name: product, ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: factor, ByRef: False
The expression body is:
This binary expression is a Multiply expression
The Left argument is:
This is an Parameter expression type
Type: System.Int32, Name: product, ByRef: False
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: factor, ByRef: False
Estendere la libreria di esempi
Gli esempi in questa sezione illustrano le tecniche di base per visitare ed esaminare i nodi in un albero delle espressioni. Semplifica i tipi di nodi che si incontrano per concentrarsi sulle attività principali di visita e accesso ai nodi in un albero delle espressioni.
In primo luogo, i visitatori gestiscono solo costanti che sono numeri interi. I valori costanti possono essere di qualsiasi altro tipo numerico e il linguaggio C# supporta conversioni e promozioni tra tali tipi. Una versione più affidabile di questo codice rispecchia tutte queste funzionalità.
Anche l'ultimo esempio riconosce un subset dei tipi di nodo possibili. È comunque possibile inserire molte espressioni che ne causano il fallimento. Un'implementazione completa è inclusa in .NET Standard con il nome ExpressionVisitor e può gestire tutti i possibili tipi di nodo.
Infine, la libreria usata in questo articolo è stata creata per la dimostrazione e l'apprendimento. Non è ottimizzato. Rende chiare le strutture e per evidenziare le tecniche usate per visitare i nodi e analizzare ciò che c'è.
Anche con queste limitazioni, è consigliabile iniziare a scrivere algoritmi che leggono e conoscono gli alberi delle espressioni.