أشجار التعبير في C # باستخدام مثال لإيجاد مشتق (مطابقة شجرة التعبير مقابل مطابقة النمط)

يوم جيد. لطالما كانت أشجار التعبير ، خاصة عند دمجها مع نمط الزائر ، موضوعًا محيرًا إلى حد ما. لذلك ، كلما زادت المعلومات المتنوعة حول هذا الموضوع ، كلما زادت الأمثلة ، كلما كان الأمر أسهل على أولئك المهتمين بالعثور على شيء واضح ومفيد لهم.



تم بناء المقال كالمعتاد - يبدأ بالإطار المفاهيمي والتعاريف وينتهي بأمثلة وطرق الاستخدام. جدول المحتويات أدناه.

أساسيات أشجار التعبير
تركيب أشجار التعبير
أنواع التعبير
مطابقة الأنماط
الساذجة الزائر
الكلاسيكي الزائر

حسنًا ، الهدف ليس فرض حل معين أو القول أن أحدهما أفضل من الآخر. أقترح استخلاص النتائج بأنفسنا ، مع مراعاة جميع الفروق الدقيقة في قضيتك. سأعبر عن رأيي في المثال الخاص بي.


أشجار التعبير


أساسيات


تحتاج أولاً للتعامل مع أشجار التعبير. تعني نوع التعبير أو أي من ورثته (سيتم مناقشتها لاحقًا). في السيناريو المعتاد ، يتم تقديم التعبير / الخوارزميات في شكل تعليمات برمجية / تعليمات قابلة للتنفيذ والتي قد لا يكون لدى المستخدم الكثير للقيام بها (تنفيذ بشكل رئيسي). يسمح لك نوع التعبير بتمثيل تعبير / خوارزمية (عادةً lambdas ، ولكن ليس ضروريًا) كبيانات منظمة في بنية شجرة يمكن للمستخدم الوصول إليها. تعطينا الطريقة الشبيهة بالشجرة لتنظيم المعلومات حول الخوارزمية واسم الفئة "أشجار التعبير".

من أجل الوضوح ، سنقوم بتحليل مثال بسيط. افترض أن لدينا lambda

(x) => Console.WriteLine (x + 5)

يمكن تمثيل هذا على أنه الشجرة التالية


جذر الشجرة هو الجزء العلوي من MethodCall ، معلمات الطريقة هي أيضًا تعبيرات ، وبالتالي يمكن أن يكون لها أي عدد من الأطفال.

في حالتنا ، هناك سليل واحد فقط - ذروة " ArithmeticOperation ". يحتوي على معلومات حول نوع العملية التي يتم إجراؤها والمعاملات اليسرى واليمنى هي أيضًا تعبيرات. سيكون لهذه القمة دائمًا نسلان.

يتم تمثيل المعاملات بواسطة ثابت ( ثابت ) ومعلمة ( معلمة ). هذه التعابير ليس لها أحفاد.

هذه أمثلة مبسطة للغاية ، ولكنها تعكس الجوهر بالكامل.

السمة الرئيسية لأشجار التعبير هي أنه يمكن تحليلها وقراءة جميع المعلومات الضرورية حول ما يجب أن تفعله الخوارزمية. من وجهة نظر ما ، هذا عكس السمات. السمات هي وسيلة للوصف التعريفي للسلوك (مشروط جدًا ، لكن الهدف النهائي هو نفسه تقريبًا). بينما تستخدم أشجار التعبير وظيفة / خوارزمية لوصف البيانات.

يتم استخدامها ، على سبيل المثال ، في موفري إطار الكيان. التطبيق واضح - تحليل شجرة التعبيرات ، لفهم ما يجب تنفيذه هناك ولجعل SQL من هذا الوصف . من الأمثلة الأقل شهرة مكتبة موك للتدخين . تستخدم أشجار التعبير أيضًا في DLR.(وقت تشغيل اللغة الديناميكي). يستخدمها مطورو المترجم لضمان التوافق بين الطبيعة الديناميكية و dotnet ، بدلاً من إنشاء MSIL .

ومن الجدير بالذكر أن أشجار التعبير غير قابلة للتغيير.

بناء الجملة


الشيء التالي الذي يستحق المناقشة هو بناء الجملة. هناك طريقتان رئيسيتان:

  • إنشاء أشجار التعبير من خلال الأساليب الثابتة لفئة التعبير
  • باستخدام تعبيرات لامدا المترجمة في Expression

الأساليب الثابتة لفئة التعبير


استخدام أشجار التعبير من خلال الأساليب الثابتة لفئة التعبير أقل استخدامًا (خاصة من وجهة نظر المستخدم). هذا أمر مرهق ، ولكنه بسيط للغاية ، لدينا
الكثير من الطوب الأساسي تحت تصرفنا ، والذي يمكنك من خلاله بناء
أشياء معقدة للغاية . يحدث الخلق من خلال طرق ثابتة منذ ذلك الحين لدى مُنشئ التعبير تعبير داخلي . وهذا لا يعني أنك بحاجة إلى الكشف عن التفكير.

كمثال ، سوف أقوم بإنشاء تعبير من المثال أعلاه:

(x) => Console.WriteLine (x + 5)

ParameterExpression parameter = Expression.Parameter(typeof(double));
ConstantExpression constant = Expression.Constant(5d, typeof(double));
BinaryExpression add = Expression.Add(parameter, constant);
MethodInfo writeLine = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(double) });
MethodCallExpression methodCall = Expression.Call(null, writeLine, add);
Expression<Action<double>> expressionlambda = Expression.Lambda<Action<double>>(methodCall, parameter);
Action<double> delegateLambda = expressionlambda.Compile();
delegateLambda(123321);

قد لا تكون هذه طريقة مريحة للغاية ، ولكنها تعكس تمامًا البنية الداخلية لأشجار التعبير. بالإضافة إلى ذلك ، توفر هذه الطريقة المزيد من الميزات والميزات التي يمكن استخدامها في أشجار التعبير: بدءًا من الحلقات ، والظروف ، وتجربة الالتقاط ، والانتقال ، والتخصيص ، وتنتهي مع كتل الأعطال ، ومعلومات تصحيح الأخطاء لنقاط التوقف ، والديناميكية ، وما إلى ذلك.

تعبير لامدا


استخدام lambdas كتعبيرات طريقة أكثر تكرارًا. إنه يعمل بكل بساطة - المترجم الذكي في مرحلة التجميع ينظر إلى ما يتم استخدامه لامدا. ويجمعها إما إلى مندوب أو إلى تعبير. في مثال متقن بالفعل ، يبدو كما يلي

Expression<Action<double>> write =  => Console.WriteLine( + 5);

من الجدير توضيح مثل هذا الشيء - التعبير هو وصف شامل. ويكفي
الحصول على النتيجة. يمكن
تحويل أشجار التعبير مثل LambdaExpression أو أحفادها إلى IL قابلة للتنفيذ. لا يمكن تحويل الأنواع المتبقية مباشرة إلى تعليمات برمجية قابلة للتنفيذ (ولكن هذا لا معنى له).
بالمناسبة ، إذا كان شخص ما ينتقد التجميع السريع للتعبير ، يمكنك إلقاء نظرة على مشروع الطرف الثالث هذا .

العكس ليس صحيحا في الحالة العامة. لا يمكن للمندوب أن يلتقطها فقط ويقدم نفسه كتعبير (ولكن هذا لا يزال ممكناً).

لا يمكن تحويل كل اللمداس إلى أشجار تعبيرية. وتشمل هذه:

  • تحتوي على عامل التعيين
  • المساهمة الديناميكية
  • غير متزامن
  • مع الجسم (الأقواس)

double variable;
dynamic dynamic;
Expression<Action> assignment = () => variable = 5; //Compiler error: An expression tree may not contain an assignment operator
Expression<Func<double>> dynamically = () => dynamic; //Compiler error: An expression tree may not contain a dynamic operation
Expression<Func<Task>> asynchon = async () => await Task.CompletedTask; //Compiler error: Async lambda cannot be converted to expresiion trees
Expression<Action> body = () => { }; //Compiler error: A lambda expression with a statement body cannot be converted to an expression tree


أنواع التعبيرات


أقترح إلقاء نظرة سريعة على الأنواع المتاحة لتمثيل الفرص التي لدينا. جميعها موجودة في مساحة الاسم System.Linq.Expressions .

أقترح عليك أولاً التعرف على العديد من الميزات المثيرة للاهتمام وغير العادية. الأنواع الأبسط من التعبيرات التي جمعتها في جهاز لوحي مع وصف موجز.

ديناميكي


باستخدام DynamicExpression ، من الممكن استخدام الديناميكي وجميع ميزاته في أشجار التعبير. هناك واجهة برمجة تطبيقات مربكة إلى حد ما ، جلست في هذا المثال لفترة أطول من جميع الأمثلة الأخرى مجتمعة. يتم توفير كل الارتباك من قبل مجموعة من الأعلام المختلفة. وبعضها مشابه لتلك التي تبحث عنها ، لكنها ليست بالضرورة. وعند العمل بأشجار التعبير الديناميكية ، من الصعب الحصول على خطأ في التحدث. مثال:

var parameter1 = Expression.Parameter(typeof(object), "name1");
var parameter2 = Expression.Parameter(typeof(object), "name2"); 
var dynamicParam1 = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
var dynamicParam2 = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
CallSiteBinder csb = Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation(CSharpBinderFlags.None, ExpressionType.Add, typeof(Program), new[] { dynamicParam1, dynamicParam2 });
var dyno = Expression.Dynamic(csb, typeof(object), parameter1, parameter2);
Expression<Func<dynamic, dynamic, dynamic>> expr = Expression.Lambda<Func<dynamic, dynamic, dynamic>>(dyno, new[] { parameter1, parameter2 });
Func<dynamic, dynamic, dynamic> action = expr.Compile();
var res = action("1", "2");
Console.WriteLine(res); //12
res = action(1, 2);
Console.WriteLine(res); //3

أشرت صراحة إلى أين يأتي الموثق من أجل تجنب الخلط مع الموثق من System.Reflection. من الأشياء المثيرة للاهتمام ، يمكننا القيام بمعلمات المرجع والخروج ، والمعلمات المسماة ، والعمليات الأحادية ، ومن حيث المبدأ ، كل ما يمكن القيام به من خلال ديناميكية ، ولكن هذا يتطلب بعض المهارة.

كتل التقاط استثناء


الشيء الثاني الذي سأنتبه إليه هو وظيفة try / catch / أخيرا / fault ، أو بالأحرى حقيقة
أن لدينا إمكانية الوصول إلى block fault. لا يتوفر في C # ، ولكنه موجود في MSIL. هذا نوع من
التناظرية أخيرًا سيتم تنفيذه في حالة وجود أي استثناء. في المثال أدناه ، سيتم طرح استثناء ، وبعد ذلك سيتم عرض "Hi" وسينتظر البرنامج الإدخال. فقط بعد ذلك سوف تسقط تماما. لا أوصي بهذه الممارسة للاستخدام.

var throwSmth = Expression.Throw(Expression.Constant(new Exception(), typeof(Exception)));
var log = Expression.Call(null, typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) }), Expression.Constant("Hi", typeof(string)));
var read = Expression.Call(null, typeof(Console).GetMethod(nameof(Console.ReadLine)));
var fault = Expression.TryFault(throwSmth, Expression.Block(new[] { log, read }));
Expression<Action> expr = Expression.Lambda<Action>(fault);
Action compiledExpression = expr.Compile();
compiledExpression();

وصف موجز لأنواع شجرة التعبير المتوفرة

الطاولة


نوعوصف قصير
الرئيسية
التعبير, . ,
Expression<TDelegate>
BinaryExpression(+, — )
UnaryExpression(+, -), throw
ConstantExpression
ParameterExpression
MethodCallExpression, MethodInfo
IndexExpression
BlockExpression, .
ConditionalExpression— if-else
LabelTargetgoto
LabelExpression, . LabelTarget. , GotoExpression, — . void, .
GotoExpression. . ( .. «break»)
LoopExpression, «break»
SwitchCaseSwitchExpression
SwitchExpressionswitch/case
TryExpressiontry/catch/finally/fault
CatchBlock, ,
ElementInitIEnumerable. ListInitExpression
ListInitExpression+
DefaultExpression
NewArrayExpression+
NewExpression
/
MemberAssignment
MemberBinding, , ,
MemberExpression/
MemberInitExpression
MemberListBinding/
MemberMemberBinding/ , /
LambdaExpression
InvocationExpression-
DebugInfoExpression.
SymbolDocumentInfo, .
DynamicExpression( )
RuntimeVariablesExpression/
TypeBinaryExpression, (is)



  • ExpressionVisitor — . .
  • DynamicExpressionVisitor — DynamicExpression ( VisitDynamic)


هذه المعلومات كافية لبدء مقارنة طرق العمل مع أشجار التعبير. قررت تحليل كل هذا بمثال العثور على المشتق. لم أتوقع جميع الخيارات الممكنة - فقط الخيارات الأساسية. ولكن إذا قرر شخص ما تعديله واستخدامه لسبب ما ، فسأكون سعيدًا لمشاركة التحسينات من خلال الطلب إلى مستودعي .


نمط مطابقة


لذا ، فإن المهمة هي عمل حساب مشتقات. يمكنك تقدير ما يلي: هناك زوجان من القواعد لإيجاد المشتق لأنواع مختلفة من العمليات - الضرب ، القسمة ، إلخ. اعتمادًا على العملية ، يجب عليك تحديد صيغة معينة. في مثل هذه الصيغة المبتذلة ، يتم وضع المهمة بشكل مثالي على التبديل / الحالة . وفي أحدث إصدار من اللغة ، تم تقديم التبديل / الحالة 2.0 أو مطابقة النمط .

من الصعب مناقشة شيء هنا. على لوحة الوصل ، تبدو مثل هذه الكمية من التعليمات البرمجية مرهقة وقراءتها القراءة ، لذلك أقترح النظر إلى جيثب . للحصول على مثال مشتق ، اتضح مثل هذا:

مثال
    public class PatterntMatchingDerivative
    {
        private readonly MethodInfo _pow = typeof(Math).GetMethod(nameof(Math.Pow));
        private readonly MethodInfo _log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        private readonly ConstantExpression _zero = Expression.Constant(0d, typeof(double));
        private readonly ConstantExpression _one = Expression.Constant(1d, typeof(double));
		
        public Expression<Func<double, double>> ParseDerivative(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(ParseDerivative(function.Body), function.Parameters);
        }

        private Expression ParseDerivative(Expression function) => function switch
        {
            BinaryExpression binaryExpr => function.NodeType switch
            {
                ExpressionType.Add => Expression.Add(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),

                ExpressionType.Multiply => (binaryExpr.Left, binaryExpr.Right) switch
		{	
	  	    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression _) => constant,
		    (ParameterExpression _, ConstantExpression constant) => constant,
		    _ => Expression.Add(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right)))
		},

                ExpressionType.Divide => (binaryExpr.Left, binaryExpr.Right) switch
		{
		    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression parameter) => Expression.Divide(constant, Expression.Multiply(parameter, parameter)),
		    (ParameterExpression _, ConstantExpression constant) => Expression.Divide(_one, constant),
		    _ => Expression.Divide(Expression.Subtract(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right))
	        },
            },
            MethodCallExpression methodCall when methodCall.Method == _pow => (methodCall.Arguments[0], methodCall.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(methodCall, Expression.Call(null, _log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, _pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(ParseDerivative(expression), methodCall), Expression.Call(null, _log, constant)),
             },
             _ => function.NodeType switch
            {
                ExpressionType.Constant => _zero,
                ExpressionType.Parameter => _one,
                _ => throw new OutOfMemoryException("Bitmap best practice")
             }
        };
    }


تبدو غير عادية بعض الشيء ، ولكنها مثيرة للاهتمام. كان من دواعي سروري أن أكتب هذا - جميع الشروط تتناسب عضويًا في سطر واحد.

المثال يتحدث عن نفسه ، لا يمكنك وصفه بشكل أفضل بالكلمات.

زائر ساذج


في مثل هذه المهمة ، يتبادر إلى الذهن زائر شجرة التعبير على الفور ، مما يجعل الكثير من الضوضاء
وقليلاً من الذعر بين الهواة لمناقشة رشيقة في المطبخ. لا تخف الجهل بل المعرفة الزائفة. من الأفضل عدم معرفة أي شيء من التفكير في الحقيقة ما هو غير صحيح ". تذكر هذه العبارة الرائعة من Tolstoy ، مع الاعتراف بالجهل وتجنيد دعم Google ، يمكنك العثور على الدليل التالي .

لدي هذا الرابط هو الأول (بعد سيبيريا عام 1949) لطلب البحث "زائر شجرة التعبير".
للوهلة الأولى ، هذا هو بالضبط ما نحتاجه. يناسب عنوان المقالة ما نريد القيام به ، ويتم تسمية الفئات في الأمثلة باسم اللاحقة زائر .

بعد مراجعة المقالة وعملها ، عن طريق القياس على مثالنا مع المشتقات ، نحصل على:
Link to github.

مثال
public class CustomDerivativeExpressionTreeVisitor
    {
        public Expression<Func<double, double>> Visit(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(Visitor.CreateFromExpression(function.Body).Visit(), function.Parameters);
        }
    }

    public abstract class Visitor
    {
        protected static readonly MethodInfo Pow = typeof(Math).GetMethod(nameof(Math.Pow));
        protected static readonly MethodInfo Log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        protected readonly ConstantExpression Zero = Expression.Constant(0d, typeof(double));
        protected readonly ConstantExpression One = Expression.Constant(1d, typeof(double));
        public abstract Expression Visit();
        public static Visitor CreateFromExpression(Expression node)
            => node switch
            {
                BinaryExpression be => new BinaryVisitor(be),
                MethodCallExpression mce when mce.Method == Pow => new PowMethodCallVisitor(mce),
                _ => new SimpleVisitor(node),
            };
        
    }

    public class BinaryVisitor : Visitor
    {
        private readonly BinaryExpression _node;
        
        public BinaryVisitor(BinaryExpression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => _node.NodeType switch
            {
                ExpressionType.Add => Expression.Add(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(ParseDerivative(binaryExpr.Left), ParseDerivative(binaryExpr.Right)),

                ExpressionType.Multiply => (binaryExpr.Left, binaryExpr.Right) switch
		{	
	  	    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression _) => constant,
		    (ParameterExpression _, ConstantExpression constant) => constant,
		    _ => Expression.Add(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right)))
		},

                ExpressionType.Divide => (binaryExpr.Left, binaryExpr.Right) switch
		{
		    (ConstantExpression _, ConstantExpression _) => _zero,
		    (ConstantExpression constant, ParameterExpression parameter) => Expression.Divide(constant, Expression.Multiply(parameter, parameter)),
		    (ParameterExpression _, ConstantExpression constant) => Expression.Divide(_one, constant),
		    _ => Expression.Divide(Expression.Subtract(Expression.Multiply(ParseDerivative(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, ParseDerivative(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right))
	        },
            };
    }

    public class PowMethodCallVisitor : Visitor
    {
        private readonly MethodCallExpression _node;

        public PowMethodCallVisitor(MethodCallExpression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => (_node.Arguments[0], _node.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(_node, Expression.Call(null, Log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, Pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(CreateFromExpression(expression).Visit(), _node), Expression.Call(null, Log, constant)),
            };
    }

    public class SimpleVisitor : Visitor
    {
        private readonly Expression _node;

        public SimpleVisitor(Expression node)
        {
            _node = node;
        }

        public override Expression Visit()
            => _node.NodeType switch
            {
                ExpressionType.Constant => Zero,
                ExpressionType.Parameter => One,
            };
    }


في الواقع - نشرنا حالات التبديل لفئات مختلفة. لم يكن هناك أقل منهم ، لم يظهر السحر. كل نفس الحالات ، خطوط أكثر بكثير. وأين هو الإرسال المزدوج الموعود؟

زائر كلاسيكي وإرسال مزدوج


هنا من الجدير أن نقول عن نموذج الزائر نفسه ، وهو أيضًا الزائر ، وهو أساس زائر شجرة التعبير . دعونا نحلل ذلك فقط على مثال أشجار التعبير.

للمرة الثانية ، لنفترض أننا قمنا بتصميم أشجار التعبير. نريد أن نمنح المستخدمين القدرة على التكرار من خلال شجرة التعبير ، واعتمادًا على أنواع العقد (أنواع التعبير) ، وتنفيذ إجراءات معينة.

الخيار الأول هو عدم القيام بأي شيء. أي إجبار المستخدمين على استخدام التبديل / الحالة. هذا ليس خيارا سيئا. ولكن هنا يوجد فارق بسيط: نشرنا المنطق المسؤول عن نوع معين. ببساطة ، تعدد الأشكال والتحديات الافتراضية ( المعروفة أيضًا باسم الربط المتأخر) تجعل من الممكن تغيير تعريف النوع إلى وقت التشغيل وإزالة عمليات التحقق هذه من التعليمات البرمجية الخاصة بنا. يكفي بالنسبة لنا أن يكون لدينا منطق يخلق نسخة من النوع المطلوب ، ثم سيتم القيام بكل شيء في وقت التشغيل بالنسبة لنا.

الخيار الثاني.الحل الواضح هو سحب المنطق إلى طرق افتراضية. من خلال تجاوز الطريقة الافتراضية في كل خليفة ، يمكننا نسيان التبديل / الحالة. إن آلية المكالمات متعددة الأشكال ستقرر لنا. سيعمل جدول الطريقة هنا ، وسيتم استدعاء الأساليب بواسطة الإزاحة الموجودة فيه. لكن هذا موضوع لمقال كامل ، لذلك دعونا لا ننجر. يبدو أن الأساليب الافتراضية تحل مشكلتنا. لكن لسوء الحظ ، يخلقون آخر. لمهمتنا ، يمكننا إضافة طريقة GetDeriviative (). ولكن الآن تبدو فئات التعبير نفسها غريبة. يمكننا أن نضيف مثل هذه الأساليب لجميع المناسبات ، لكنها لا تتناسب مع المنطق العام للفصل. وما زلنا لم نوفر الفرصة للقيام بشيء مماثل للمستخدمين (بطريقة مناسبة بالطبع). نحن بحاجة للسماح للمستخدم بتحديد المنطق لكل نوع معين ،ولكن حافظ على تعدد الأشكال (المتاح لنا).

فقط جهود المستخدم للقيام بذلك لن تنجح.

هذا هو المكان الذي يقع فيه الزائر الحقيقي. في النوع الأساسي من التسلسل الهرمي (التعبير في حالتنا) ، نحدد طريقة الشكل

virtual Expression Accept(ExpressionVisitor visitor);

في الورثة ، سيتم تجاوز هذه الطريقة.

ExpressionVisitor نفسه عبارة عن فئة أساسية تحتوي على طريقة افتراضية بنفس التوقيع
لكل نوع من التسلسل الهرمي. على سبيل المثال فئة ExpressionVisitor - VisitBinary (...) ، VisitMethodCall (...) ، VisitConstant (...) ، VisitParameter (...).

تسمى هذه الأساليب في الفئة المقابلة من التسلسل الهرمي لدينا.

أولئك. ستبدو طريقة القبول في فئة BinaryExpression كما يلي:

	
protected internal override Expression Accept(ExpressionVisitor visitor)
{
        return visitor.VisitBinary(this);
}

ونتيجة لذلك ، من أجل تحديد سلوك جديد ، يحتاج المستخدم فقط لإنشاء وريث لفئة ExpressionVisitor ، حيث سيتم إعادة تعريف الطرق المقابلة لحل مشكلة واحدة. في حالتنا ، يتم إنشاء زائر DerivativeExpressionVisitor.

علاوة على ذلك ، لدينا بعض الأشياء من خلفاء التعبير ، ولكن تلك الأشياء غير معروفة ، ولكنها ليست ضرورية.
نحن نطلق على طريقة القبول الافتراضية مع تطبيق ExpressionVisitor الذي نحتاجه ، أي مع برنامج DerivativeExpressionVisitor. بفضل الإرسال الديناميكي ، يُطلق على تطبيق تجاوز لـ Accept ، مثل وقت التشغيل ، على سبيل المثال BinaryExpression. في نص هذه الطريقة ، نتفهم تمامًا أننا في BinaryExpression ، لكننا لا نعرف أي خليفة ExpressionVisitor جاء إلينا. لكن منذ VisitBinary هو أيضًا افتراضي ، لا نحتاج إلى معرفته. مرة أخرى ، نقوم ببساطة بالاتصال بالرجوع إلى الفئة الأساسية ، ويتم إرسال المكالمة ديناميكيًا (في وقت التشغيل) ويتم استدعاء تطبيق VisitBinary الذي تم تجاوزه لنوع وقت التشغيل. الكثير من أجل الإرسال المزدوج - كرة الطاولة بأسلوب "أنت تفعل ذلك" - "لا ، أنت".

ماذا يعطينا. في الواقع ، هذا يجعل من الممكن "إضافة" الأساليب الافتراضية من الخارج ، وليس
تغيير الفصل. يبدو رائعًا ، ولكن له سلبياته:

  1. بعض اليساريين في شكل طريقة القبول ، وهي المسؤولة عن كل شيء ولا شيء في نفس الوقت
  2. التأثير المتتالي لوظيفة التجزئة الجيدة هو أنه عندما تضيف وريثًا واحدًا فقط للتسلسل الهرمي ، في أسوأ الأحوال ، يجب على الجميع إنهاء زوارهم

لكن طبيعة أشجار التعبير تسمح بهذه التكاليف بسبب تفاصيل العمل باستخدام التعبيرات ، لأن هذا النوع من الحلول هو أحد ميزاتها الرئيسية.
هنا يمكنك رؤية جميع الطرق المتاحة للتحميل الزائد.

لذا ، دعنا نرى كيف يبدو في النهاية.
رابط إلى جيثب.

مثال
public class BuildinExpressionTreeVisitor : ExpressionVisitor
    {
        private readonly MethodInfo _pow = typeof(Math).GetMethod(nameof(Math.Pow));
        private readonly MethodInfo _log = typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) });
        private readonly ConstantExpression _zero = Expression.Constant(0d, typeof(double));
        private readonly ConstantExpression _one = Expression.Constant(1d, typeof(double));

        public Expression<Func<double, double>> GetDerivative(Expression<Func<double, double>> function)
        {
            return Expression.Lambda<Func<double, double>>(Visit(function.Body), function.Parameters);
        }

        protected override Expression VisitBinary(BinaryExpression binaryExpr)
            => binaryExpr.NodeType switch
            {
                ExpressionType.Add => Expression.Add(Visit(binaryExpr.Left), Visit(binaryExpr.Right)),
                ExpressionType.Subtract => Expression.Subtract(Visit(binaryExpr.Left), Visit(binaryExpr.Right)),

                ExpressionType.Multiply when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ConstantExpression => _zero,
                ExpressionType.Multiply when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ParameterExpression => binaryExpr.Left,
                ExpressionType.Multiply when binaryExpr.Left is ParameterExpression && binaryExpr.Right is ConstantExpression => binaryExpr.Right,
                ExpressionType.Multiply => Expression.Add(Expression.Multiply(Visit(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, Visit(binaryExpr.Right))),

                ExpressionType.Divide when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ConstantExpression => _zero,
                ExpressionType.Divide when binaryExpr.Left is ConstantExpression && binaryExpr.Right is ParameterExpression => Expression.Divide(binaryExpr.Left, Expression.Multiply(binaryExpr.Right, binaryExpr.Right)),
                ExpressionType.Divide when binaryExpr.Left is ParameterExpression && binaryExpr.Right is ConstantExpression => Expression.Divide(_one, binaryExpr.Right),
                ExpressionType.Divide => Expression.Divide(Expression.Subtract(Expression.Multiply(Visit(binaryExpr.Left), binaryExpr.Right), Expression.Multiply(binaryExpr.Left, Visit(binaryExpr.Right))), Expression.Multiply(binaryExpr.Right, binaryExpr.Right)),
            };

        protected override Expression VisitMethodCall(MethodCallExpression methodCall)
            => (methodCall.Arguments[0], methodCall.Arguments[1]) switch
            {
                (ConstantExpression constant, ParameterExpression _) => Expression.Multiply(methodCall, Expression.Call(null, _log, constant)),
                (ParameterExpression param, ConstantExpression constant) => Expression.Multiply(constant, Expression.Call(null, _pow, param, Expression.Constant((double)constant.Value - 1, typeof(double)))),
                (ConstantExpression constant, Expression expression) => Expression.Multiply(Expression.Multiply(Visit(expression), methodCall), Expression.Call(null, _log, constant)),
            };

        protected override Expression VisitConstant(ConstantExpression _) => _zero;

        protected override Expression VisitParameter(ParameterExpression b) => _one;
    }


الموجودات


ربما ، كما هو الحال في معظم مهام البرمجة ، لا يمكن إعطاء إجابة محددة. كل شيء ، كما هو الحال دائمًا ، يعتمد على الحالة المحددة. أحب النمط المعتاد المطابق لمثالي ، لأنه لم أقم بتطويره إلى مستوى التنمية الصناعية. إذا كان هذا التعبير سيزداد بشكل لا يمكن السيطرة عليه ، فسيكون من المفيد التفكير في الزائر. وحتى الزائر الساذج له الحق في الحياة - بعد كل شيء ، هذه طريقة جيدة لتفريق كمية كبيرة من التعليمات البرمجية في الفصول إذا لم يقدم التسلسل الهرمي الدعم من جانبه. وحتى هنا توجد استثناءات.

وبالمثل ، فإن دعم الزائر من التسلسل الهرمي أمر مثير للجدل للغاية.

ولكن آمل أن تكون المعلومات المقدمة هنا كافية لاتخاذ القرار الصحيح.

Source: https://habr.com/ru/post/undefined/


All Articles