تُستخدَم الشجرة التعبيرية (Expression Tree) عند الحاجة لإنشاء تعبيرات (expressions) خلال زمن التشغيل (runtime)، مما يجعلها مناسبة للأغراض التالية:
-
مع الواجهات
IEnumerable
وIQueryable
لفَحْص خَبَر (predicate) معين. -
مع
Entity Framework
أوLINQ to SQL
لتنشئة عبارةWhere
لفَحْص خَبَر معين.
يُمكِن إنشاء شجرة تعبيرية (Expression Tree) بطريقتين أساسيتين:
الأولى: اِستخدَام واجهة برمجة التطبيقات (API) الخاصة بالنوع Expression
(طريقة يدوية). الثانية: إِسْناد دالة مُجرَّدة (lambda expression) إلى مُتغيّر من النوع Expression
(طريقة مُولدَّة آليًا).
إنشاء النوع BinaryExpression
باستخدام واجهة برمجة التطبيقات
على سبيل المثال، إذا كان لديك كُلًا من الخَبَر _ => _.Field
وسِلسِلة نصية تَحمِل القيمة "VALUE"، يُمكِنك إنشاء التعبير _ => _.Field == "VALUE"
أثناء زمن التشغيل (runtime) لفَحْص قيمة الخَبَر.
في الشيفرة التالية، عُرِّف التابع BuildEqualPredicate
والذي يَستخدِم واجهة برمجة التطبيقات الخاصة بالصنف Expression
، فيَستدعِي التابع Expression.Equal
ليُنشِئ شجرة تعبيرية من النوع BinaryExpression
تَفحَص ما إذا كانت قيمة المُتغيّر Field
مُساوية للسِلسِلة النصية "VALUE":
public static Expression<Func<T, bool>> BuildEqualPredicate<T>( Expression<Func<T, string>> memberAccessor, string term) { var toString = Expression.Convert(Expression.Constant(term), typeof(string)); Expression expression = Expression.Equal(memberAccessor.Body, toString); var predicate = Expression.Lambda<Func<T, bool>>( expression, memberAccessor.Parameters); return predicate; }
يُمكن تَمرير الخَبَر المُنشَئ (predicate) كمُعامِل للتابع المُوسِع Where
، كالتالي:
var predicate = PredicateExtensions.BuildEqualPredicate<Entity>( _ => _.Field, "VALUE"); var results = context.Entity.Where(predicate).ToList();
تنشئة النوع LambdaExpression
بإسناد دالة مجردة إلى متغير من النوع Expression
عادةً ما تُسْنَد الدوال المُجرَّدة (lambda expressions) إلى مُتغيرات من النوع Delegate
تَعمَل كمُفوِّض قابل للاستدعاء. في المُقابل، يُمكنك إِسنادها إلى مُتَغيّر من النوع Expression
، وفي هذه الحالة، يُولِّد مُصرِّف C# شجرة تعبيرية (Expression Tree) مُكافئة، فمثلًا:
Expression<Func<int, int>> expression = a => a + 1;
يُولَّد عن المثال بالأعلى شجرة تعبيرية (Expression Tree) من النوع LambdaExpression
مُكافِئة للشيفرة التالية:
ParameterExpression parameterA = Expression.Parameter(typeof(int), "a"); var expression = (Expression<Func<int, int>>)Expression.Lambda( Expression.Add( parameterA, Expression.Constant(1)), parameterA);
تُمثِل الشجرة التعبيرية من النوع LambdaExpression
دالة مُجرَّدة تتكون من مَتْن الدالة body وقائمة المُتغيّرات. مثلًا، في المثال بالأعلى، تَستقبِل الدالة المُمثَّلة مُعامِلًا وحيدًا يُدعى a بينما يتكون المَتن من عبارة وحيدة من النوع BinaryExpression
بخاصية NodeType
من النوع Add
. يُمثِل هذا التعبير بدوره عملية جَمع تتكون من تعبيرين فرعيين (sub-expressions) يُشار إليهما بالتعبيرين الأيمن والأيسر. التعبير الأيسر هو من النوع ParameterExpression
يُمثِل المُعامِل a المُمرَّر، أما التعبير الأيمن فهو من النوع ConstantExpression
بقيمة تساوي الواحد.
أبسط ما يُمكنك القيام به هو طباعة قيمة التَعبير (expression)، والذي بدوره يَطبع شيفرة C# المكافئة كالتالي:
Console.WriteLine(expression); //prints a => (a + 1)
يُستخدَم التابع Compile
لتَصرِّيف الشجرة التعبيرية (expression tree) إلى مُتغيّر من النوع Delegate
، قابل للاستدعاء ببيئة التَّنفيذ المُشتركة (CLR)، كالتالي:
Func<int, int> lambda = expression.Compile(); Console.WriteLine(lambda(2)); //prints 3
إنشاء النوع MemberExpression
باستخدام واجهة برمجة التطبيقات
عادةً ما تُترجَم التعبيرات (expressions) إلى لغات آخرى مثل SQL، لكن يُمكِن اِستخدَامها أيضًا لاستدعاء أعضاء الأصناف (members) سواء كانت هذه الأعضاء خاصة (private) أو (internal) أو (protected) وسواء ما كانت الأنواع عَلَّنية (public) أم لا، كطريقة بديلة للانعكاس (Reflection).
بفرض أن لديك الصنف التالي:
public TestClass { public static string StaticPublicField = "StaticPublicFieldValue"; }
يمكن استرجاع قيمة الخاصية StaticPublicField
الساكنة (static) كالتالي:
var fieldExpr = Expression.Field(null, typeof(TestClass), "StaticPublicField"); var labmda = Expression.Lambda<Func<string>>(fieldExpr);
يُمكن تَصرِيف الشجرة التعبيرية لمُفوِّض يُمكِن استدعائه للولوج لقيمة الخاصية:
Func<string> retriever = lambda.Compile(); var fieldValue = retriever();
إنشاء النوع InvocationExpression
باستخدام واجهة برمجة التطبيقات
يُستخدَم التابع الساكن Expression.Invoke
لإنشاء شجرة تعبيرية من النوع InvocationExpression
. يُمَكِّنك هذا النوع من استدعاء دوال مُجرَّدة أُخرى (lambda expressions) مُضمنة بالشجرة التعبيرية ذاتها (Expression tree).
المشكلة:
نريد الوصول إلى العناصر التي تحتوي خاصية Description
الخاصة بهم على السِلسِلة النصية "car". نحتاج إلى التأكد من أن تلك الخاصية ليست فارغة null
قبل البحث فيها عن السِلسِلة النصية، لكن لا نريد أن نُفْرِط في استدعائها لأن الكلفة قد تكون عالية.
using System; using System.Linq; using System.Linq.Expressions; public class Program { public static void Main() { var elements = new[] { new Element { Description = "car" }, new Element { Description = "cargo" }, new Element { Description = "wheel" }, new Element { Description = null }, new Element { Description = "Madagascar" }, }; var elementIsInterestingExpression = CreateSearchPredicate( searchTerm: "car", whereToSearch: (Element e) => e.Description); Console.WriteLine(elementIsInterestingExpression.ToString()); var elementIsInteresting = elementIsInterestingExpression.Compile(); var interestingElements = elements.Where(elementIsInteresting); foreach (var e in interestingElements) { Console.WriteLine(e.Description); } var countExpensiveComputations = 0; Action incCount = () => countExpensiveComputations++; elements .Where( CreateSearchPredicate( "car", (Element e) => ExpensivelyComputed( e, incCount ) ).Compile() ) .Count(); Console.WriteLine("Property extractor is called {0} times.", countExpensiveComputations); } private class Element { public string Description { get; set; } } private static string ExpensivelyComputed(Element source, Action count) { count(); return source.Description; } private static Expression<Func<T, bool>> CreateSearchPredicate<T> (string searchTerm, Expression<Func<T, string>> whereToSearch) { var extracted = Expression.Parameter(typeof(string), "extracted"); Expression<Func<string, bool>> coalesceNullCheckWithSearch = Expression.Lambda<Func<string, bool>>( Expression.AndAlso( Expression.Not( Expression.Call(typeof(string), "IsNullOrEmpty", null, extracted) ), Expression.Call(extracted, "Contains", null, Expression.Constant(searchTerm)) ), extracted); var elementParameter = Expression.Parameter(typeof(T), "element"); return Expression.Lambda<Func<T, bool>>( Expression.Invoke( coalesceNullCheckWithSearch, Expression.Invoke(whereToSearch, elementParameter) ), elementParameter ); } }
الخَرْج:
element => Invoke( extracted => (Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car")), Invoke(e => e.Description, element)) car cargo Madagascar Predicate is called 5 times.
تم تَضمِين الولوج للخاصية Description
بداخل التابع Invoke
كالتالي:
Invoke(e => e.Description, element)
وهذا هو المكان الوحيد الذي يتم التعامل فيه مع الخاصية Description
مباشرة، فقد تم استخراج متُغير آخر من النوع String
وتمريره للتابع التالي.
(Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car"))
من المهم أن تكون على دراية بكيفية عمل العَامِل AndAlso
. إذا آلت قيمة المُعامِل الأيسر للقيمة المنطقية false
، فإن التابع AndAlso
يُعيد نفس ذات القيمة false
دون أن يحسِب قيمة المُعامِل الأيمن. يُعدّ استخدام العَامِل And
أحد أكثر الأخطاء شيوعًا. فبالإضافة إلى حِسابه لقيمة كلا المُعامِلين دومًا بغض النظر عن قيمتهما، فإنه أيضًا قد يُبلِّغ عن اعتراض من النوع NullReferenceException
إذا اُستخدِم بشكل مشابه للمثال بالأعلى.
ترجمة -وبتصرف- للفصل Expression Trees من كتاب .NET Framework Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.