سلسلة net. للمحترفين الشجرة التعبيرية Expression Tree في dot Net


رضوى العربي

تُستخدَم الشجرة التعبيرية (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





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن