Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
In het volgende codevoorbeeld ziet u hoe de expressiestructuur die de lambda-expressie num => num < 5 vertegenwoordigt, in de delen ervan kan worden opgesplitst.
// 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
Nu gaan we code schrijven om de structuur van een expressiestructuur te onderzoeken. Elk knooppunt in een expressiestructuur is een object van een klasse die is afgeleid van Expression.
Dit ontwerp maakt het bezoeken van alle knooppunten in een expressiestructuur een relatief eenvoudige recursieve bewerking. De algemene strategie is om te beginnen bij het hoofdknooppunt en te bepalen welk type knooppunt het is.
Als het knooppunttype onderliggende elementen bevat, bezoek dan recursief de onderliggende elementen. Herhaal bij elk kindknooppunt het proces dat wordt gebruikt bij het hoofdknooppunt: stel het type vast en als het type kindknooppunten heeft, bezoek elk van de kinderen.
Een expressie zonder kinderen onderzoeken
Laten we beginnen met het bezoeken van elk knooppunt in een eenvoudige expressiestructuur. Hier volgt de code waarmee een constante expressie wordt gemaakt en vervolgens de eigenschappen ervan worden onderzocht:
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}");
Met de voorgaande code wordt de volgende uitvoer afgedrukt:
This is a/an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 24
Nu gaan we de code schrijven die deze expressie zou onderzoeken en enkele belangrijke eigenschappen hierover uitschrijven.
Expressie voor optellen
Laten we beginnen met het toevoegingsvoorbeeld uit de inleiding tot deze sectie.
Expression<Func<int>> sum = () => 1 + 2;
Notitie
Gebruik nietvar om deze expressiestructuur te declareren, omdat het natuurlijke type gemachtigde is Func<int>, niet Expression<Func<int>>.
Het hoofdknooppunt is een LambdaExpression. Als u de interessante code aan de rechterkant van de =>-operator wilt verkrijgen, moet u een van de onderliggende elementen van de LambdaExpression vinden. U doet dit met alle expressies in deze sectie. Het bovenliggende knooppunt helpt ons bij het vinden van het retourtype van de LambdaExpression.
Als u elk knooppunt in deze expressie wilt onderzoeken, moet u recursief veel knooppunten bezoeken. Hier volgt een eenvoudige eerste implementatie:
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 dit voorbeeld wordt de volgende uitvoer afgedrukt:
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
U ziet veel herhaling in het voorgaande codevoorbeeld. Laten we dat opschonen en een meer algemene expressieknooppuntbezoeker maken. Hiervoor moeten we een recursief algoritme schrijven. Elk knooppunt zou van een type kunnen zijn dat mogelijk kinderen heeft. Elk knooppunt dat kinderen heeft, vereist dat wij deze kinderen bezoeken en vaststellen wat dat knooppunt is. Hier volgt de opgeschoonde versie die gebruikmaakt van recursie om de toevoegingsbewerkingen te bezoeken:
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}");
}
}
Dit algoritme is de basis van een algoritme dat willekeurige LambdaExpression kan bezoeken. De code die u hebt gemaakt, zoekt alleen naar een klein voorbeeld van de mogelijke sets met expressiestructuurknooppunten die kunnen optreden. U kunt echter nog steeds heel wat leren van wat het produceert. (De standaardcase in de Visitor.CreateFromExpression methode drukt een bericht af op de foutconsole wanneer er een nieuw knooppunttype wordt aangetroffen. Op die manier weet u een nieuw expressietype toe te voegen.)
Wanneer u deze bezoeker uitvoert op de voorgaande toevoegingsexpressie, krijgt u de volgende uitvoer:
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
Nu u een algemenere bezoekers-implementatie hebt gebouwd, kunt u veel meer verschillende soorten expressies bezoeken en verwerken.
Expressie toevoegen met meer operanden
Laten we een ingewikkelder voorbeeld proberen, maar nog steeds de knooppunttypen beperken tot alleen toevoegen:
Expression<Func<int>> sum = () => 1 + 2 + 3 + 4;
Voordat u deze voorbeelden uitvoert op het bezoekersalgoritmen, kunt u een gedachteoefening proberen om te bepalen wat de uitvoer kan zijn. Houd er rekening mee dat de + operator een binaire operator is: deze moet twee onderliggende elementen hebben, die de linker- en rechteroperanden vertegenwoordigen. Er zijn verschillende manieren om een boom te maken die correct kan zijn:
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;
U kunt de scheiding in twee mogelijke antwoorden zien om de meest veelbelovende te markeren. De eerste vertegenwoordigt de juiste associatieve expressies. De tweede vertegenwoordigt linkse associatieve expressies. Het voordeel van beide formaten is dat het formaat kan worden opgeschaald naar een willekeurig aantal optellingen.
Als u deze expressie wel uitvoert via de bezoeker, ziet u deze uitvoer en controleert u of de eenvoudige toevoegingsexpressie associatief blijft.
Als u dit voorbeeld wilt uitvoeren en de volledige expressiestructuur wilt zien, wijzigt u één wijziging in de bronexpressiestructuur. Wanneer de expressiestructuur alle constanten bevat, bevat de resulterende structuur gewoon de constante waarde van 10. De compiler voert alle toevoegingen uit en vermindert de expressie tot de eenvoudigste vorm. Het toevoegen van één variabele in de expressie is voldoende om de oorspronkelijke structuur te zien:
Expression<Func<int, int>> sum = (a) => 1 + a + 3 + 4;
Maak een bezoeker voor deze som en voer de bezoeker uit en zie de uitvoer:
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
U kunt een van de andere voorbeelden uitvoeren via de bezoekerscode en zien welke boom het vertegenwoordigt. Hier volgt een voorbeeld van de voorgaande sum3 expressie (met een extra parameter om te voorkomen dat de compiler de constante kan berekenen):
Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);
Dit is het resultaat van de bezoeker:
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
U ziet dat de haakjes geen deel uitmaken van de uitvoer. Er zijn geen knooppunten in de expressiestructuur die de haakjes in de invoerexpressie vertegenwoordigen. De structuur van de expressiestructuur bevat alle informatie die nodig is om de prioriteit te communiceren.
Dit voorbeeld uitbreiden
Het voorbeeld behandelt alleen de meest elementaire expressiebomen. De code die u in deze sectie hebt gezien, verwerkt alleen constante gehele getallen en de binaire + operator. Als laatste voorbeeld gaan we de bezoeker bijwerken om een complexere expressie af te handelen. Laten we deze laten werken voor de volgende factoriële expressie:
Expression<Func<int, int>> factorial = (n) =>
n == 0 ?
1 :
Enumerable.Range(1, n).Aggregate((product, factor) => product * factor);
Deze code vertegenwoordigt één mogelijke implementatie voor de wiskundige faculteitsfunctie . De manier waarop u deze code hebt geschreven, markeert twee beperkingen van het bouwen van expressiestructuren door lambda-expressies toe te wijzen aan Expressies. Ten eerste zijn statement-lambdas niet toegestaan. Dat betekent dat u geen lussen, blokken, if/else-instructies en andere besturingsstructuren kunt gebruiken die gebruikelijk zijn in C#. U bent beperkt tot het gebruik van expressies. Ten tweede kunt u dezelfde expressie niet recursief aanroepen. U zou dit kunnen doen als het al een delegaat was, maar u kunt het niet aanroepen in de vorm van een expressie-boom. In de sectie over het bouwen van expressiestructuren leert u technieken om deze beperkingen te overwinnen.
In deze expressie ziet u knooppunten van al deze typen:
- Gelijk (binaire uitdrukking)
- Vermenigvuldigen (binaire expressie)
- Voorwaardelijk (de
? :expressie) - Methode-aanroepuitdrukking (aanroepen
Range()enAggregate())
Een manier om het bezoekersalgoritme te wijzigen, is door het voortdurend uit te voeren en het knooppunttype te schrijven telkens wanneer u uw default clausule bereikt. Na enkele iteraties hebt u alle mogelijke knooppunten gezien. Op dat moment hebt u alles wat u nodig hebt. Het resultaat ziet er ongeveer als volgt uit:
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}"),
};
ConditionalVisitor en MethodCallVisitor verwerken die twee knooppunten.
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");
}
}
}
En de uitvoer voor de expressieboom is:
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
De voorbeeldbibliotheek uitbreiden
In de voorbeelden in deze sectie ziet u de belangrijkste technieken voor het bezoeken en onderzoeken van knooppunten in een expressiestructuur. Het vereenvoudigt de typen knooppunten die u tegenkomt om zich te concentreren op de belangrijkste taken van het bezoeken en openen van knooppunten in een expressiestructuur.
Ten eerste verwerken de bezoekers alleen constanten die gehele getallen zijn. Constante waarden kunnen elk ander numeriek type zijn en de C#-taal ondersteunt conversies en promoties tussen deze typen. Een krachtigere versie van deze code zou al deze mogelijkheden weerspiegelen.
Zelfs in het laatste voorbeeld wordt een subset van de mogelijke knooppunttypen herkend. U kunt nog steeds veel uitdrukkingen invoeren waardoor het faalt. Een volledige implementatie is opgenomen in .NET Standard onder de naam ExpressionVisitor en kan alle mogelijke knooppunttypen verwerken.
Ten slotte is de bibliotheek die in dit artikel wordt gebruikt, gebouwd voor demonstratie en leren. Het is niet geoptimaliseerd. Het maakt de structuren duidelijk en benadrukt de technieken die worden gebruikt om de knooppunten te bezoeken en te analyseren wat aanwezig is.
Zelfs met deze beperkingen moet u goed op weg zijn naar het schrijven van algoritmen die expressiestructuren lezen en begrijpen.