Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Az alábbi példakód bemutatja, hogy a lambda kifejezést num => num < 5
jelképező kifejezésfa hogyan bontható fel annak részeire.
// 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
Most írjunk egy kódot egy kifejezésfa szerkezetének vizsgálatához. A kifejezésfa minden csomópontja egy osztály objektuma, amely a következőből Expression
származik:
Ez a kialakítás egy kifejezésfa összes csomópontjának felkeresése viszonylag egyszerű rekurzív művelet. Az általános stratégia az, hogy a gyökércsomóponton kezdődjön, és meghatározza, hogy milyen csomópontról van szó.
Ha a csomóponttípus gyermekekkel rendelkezik, rekurzívan keresse fel a gyermekeket. Minden gyermekcsomópontnál ismételje meg a gyökércsomóponton használt folyamatot: határozza meg a típust, és ha a típusnak vannak gyermekei, látogasson el mindegyik gyermekhez.
Gyermekmentes kifejezés vizsgálata
Kezdjük az egyes csomópontok egyszerű kifejezésfában való megtekintésével. Az alábbi kód állandó kifejezést hoz létre, majd megvizsgálja annak tulajdonságait:
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}");
Az előző kód a következő kimenetet nyomtatja ki:
This is a/an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 24
Most írjuk meg azt a kódot, amely megvizsgálná ezt a kifejezést, és kiírnánk róla néhány fontos tulajdonságot.
Hozzáadási kifejezés
Kezdjük a szakasz bevezetőjében szereplő hozzáadási mintával.
Expression<Func<int>> sum = () => 1 + 2;
Megjegyzés
Ne használja var
a kifejezésfa deklarálására, mert a delegált természetes típusa Func<int>
és nem Expression<Func<int>>
.
A gyökércsomópont egy LambdaExpression
. Annak érdekében, hogy megszerezze az érdekes kódot az =>
operátor jobb oldalán, meg kell találnia az LambdaExpression
egyik gyermekét. Ezt a szakasz összes kifejezésével teheti meg. A szülőcsomópont segít abban, hogy megtaláljuk a LambdaExpression
visszatérési típusát.
A kifejezés minden csomópontjának vizsgálatához rekurzívan meg kell látogatnia számos csomópontot. Íme egy egyszerű első implementáció:
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}");
Ez a minta a következő kimenetet nyomtatja ki:
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
Az előző kódmintában sok ismétlődést tapasztal. Tisztítsuk meg, és hozzunk létre egy általánosabb célú kifejezéscsomópont-látogatót. Ez megköveteli, hogy rekurzív algoritmust írjunk. Bármely csomópont olyan típusú lehet, amely gyermekeket tartalmazhat. Minden olyan csomópont, amely gyermekekkel rendelkezik, meg kell látogatnia ezeket a gyerekeket, és meg kell határoznia, hogy mi az a csomópont. Íme a kitisztított verzió, amely a rekurziót használja a hozzáadási műveletek felkereséséhez:
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}");
}
}
Ez az algoritmus egy másik algoritmus alapja, amely meglátogat bármilyen tetszőleges LambdaExpression
. Az általad létrehozott kód csak a lehetséges kifejezésfa csomópontok egy kis mintáját keresi, amelyekkel találkozhat. Azonban továbbra is sokat lehet tanulni abból, amit produkál. (A metódus alapértelmezett esete Visitor.CreateFromExpression
egy új csomóponttípus észlelésekor üzenetet küld a hibakonzolnak. Így új kifejezéstípust vehet fel.)
Amikor a látogatót az előző összeadási kifejezésen futtatja, a következő kimenetet kapja:
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
Most, hogy egy általánosabb látogatói implementációt készített, sokkal több különböző kifejezéstípust tekinthet meg és dolgozhat fel.
Hozzáadási kifejezés több operandussal
Próbáljunk ki egy bonyolultabb példát, de továbbra is csak a csomóponttípusok hozzáadását korlátozzuk:
Expression<Func<int>> sum = () => 1 + 2 + 3 + 4;
Mielőtt futtatná ezeket a példákat a látogatói algoritmuson, próbáljon ki egy gondolati gyakorlatot, hogy kiderítse, mi lehet a kimenet. Ne feledje, hogy az +
operátor bináris operátor: két gyermeknek kell lennie, amelyek a bal és a jobb operandusokat jelölik. A helyes fa felépítésének számos lehetséges módja lehet:
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;
A lehetőségek két külön válaszra való bontása kiemeli a legígéretesebbet. Az első a jobb asszociatív kifejezéseket jelöli. A második a bal asszociatív kifejezéseket jelöli. Mindkét formátum előnye, hogy a formátum tetszőleges számú összeadási kifejezésre skálázható.
Ha ezt a kifejezést a látogatón keresztül futtatja, ezt a kimenetet látja, és ellenőrzi, hogy az egyszerű összeadási kifejezés asszociatív marad-e.
A minta futtatásához és a teljes kifejezésfa megtekintéséhez egy módosítást kell végeznie a forráskifejezés fáján. Ha a kifejezésfa az összes állandót tartalmazza, az eredményül kapott fa egyszerűen a függvény állandó értékét 10
tartalmazza. A fordító elvégzi az összes kiegészítést, és a kifejezést a legegyszerűbb formára csökkenti. Elég egyetlen változót hozzáadni a kifejezéshez az eredeti fa megtekintéséhez:
Expression<Func<int, int>> sum = (a) => 1 + a + 3 + 4;
Hozzon létre egy látogatót ehhez az összeghez, és futtassa a látogatót, aki ezt a kimenetet látja:
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
Futtathatja a többi mintát a látogatókódon keresztül, és megtekintheti, hogy milyen fát jelöl. Íme egy példa az előző kifejezésre sum3
(egy további paraméterrel, amely megakadályozza, hogy a fordító az állandót számítsa):
Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);
A látogató kimenete a következő:
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
Figyelje meg, hogy a zárójelek nem részei a kimenetnek. A kifejezésfában nincsenek csomópontok, amelyek a bemeneti kifejezés zárójeleit jelölik. A kifejezésfa struktúrája tartalmazza az elsőbbség közléséhez szükséges összes információt.
A minta kiterjesztése
A minta csak a legkezdetlegesebb kifejezésfákkal foglalkozik. Az ebben a szakaszban látott kód csak az állandó egész számokat és a bináris +
operátort kezeli. Végső mintaként frissítsük a látogatót egy bonyolultabb kifejezés kezelésére. Tegyük fel, hogy a következő faktoriális kifejezéshez használható:
Expression<Func<int, int>> factorial = (n) =>
n == 0 ?
1 :
Enumerable.Range(1, n).Aggregate((product, factor) => product * factor);
Ez a kód egy lehetséges implementációt jelöl a matematikai faktoriális függvényhez. A kód írásának módja a kifejezésfák létrehozásának két korlátozását emeli ki, ha lambdakifejezéseket rendel a kifejezésekhez. Először is, a lambdakifejezések nem engedélyezettek. Ez azt jelenti, hogy nem használhat hurkokat, blokkokat, ha / egyéb utasításokat, és más, a C#-ban gyakran használt vezérlőstruktúrákat. Kizárólag kifejezéseket használhat. Másodszor, ugyanazt a kifejezést nem lehet rekurzívan meghívni. Már megtehetné, ha ez egy meghatalmazott lenne, de annak a kifejezésfa formájában nem hívhatja meg. A kifejezésfák készítéséről szóló szakaszban megtanulhatja, hogyan háríthatja el ezeket a korlátozásokat.
Ebben a kifejezésben az alábbi típusú csomópontokkal találkozik:
- Egyenlőség (bináris kifejezés)
- Szorzás (bináris kifejezés)
- Feltételes (a
? :
kifejezés) - Metódushívási kifejezés (hívás
Range()
ésAggregate()
)
A látogatói algoritmus módosításának egyik módja az, ha folyamatosan végrehajtja, és minden alkalommal felírja a csomópont típusát, amikor eléri a default
záradékot. Néhány iteráció után az egyes lehetséges csomópontok láthatók. Akkor mindene megvan, ami kell. Az eredmény a következőhöz hasonló:
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}"),
};
A ConditionalVisitor
és MethodCallVisitor
a két csomópontot dolgozza fel:
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");
}
}
}
A kifejezésfa kimenete pedig a következő:
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
A mintatár kiterjesztése
Az ebben a szakaszban szereplő minták a kifejezésfában található csomópontok meglátogatására és vizsgálatára vonatkozó alapvető technikákat mutatják be. Egyszerűsítette azokat a csomóponttípusokat, amelyekkel a kifejezésfában lévő csomópontok felkeresésével és elérésével kapcsolatos alapvető feladatokra összpontosíthat.
Először is a látogatók csak egész számokat tartalmazó állandókat kezelnek. Az állandó értékek bármilyen más numerikus típust is jelenthetnek, és a C#-nyelv támogatja az ilyen típusok közötti konverziókat és előléptetéseket. A kód robusztusabb verziója tükrözné ezeket a képességeket.
Még az utolsó példa is felismeri a lehetséges csomóponttípusok egy részét. Továbbra is számos olyan kifejezést adhat hozzá, amelyek miatt a művelet meghiúsul. A .NET Standard teljes körű implementációt tartalmaz a név ExpressionVisitor alatt, és képes kezelni az összes lehetséges csomóponttípust.
Végül a cikkben használt könyvtár bemutató és tanulási célokra készült. Nincs optimalizálva. Egyértelművé teszi a struktúrákat, és kiemeli a csomópontok meglátogatásához és az ott található elemek elemzéséhez használt technikákat.
A kifejezések fáinak olvasását és megértését végző algoritmusok írásához még ezekkel a korlátozásokkal is jó úton kell haladnia.