وبسایت شخصی حسن هاشمی

برنامه نویس. ایران. قم :))

آشنایی با ExpressionTree در دات نت

توی این پست میخوایم یه آشنایی مختصری با ExpressionTree داشته باشیم.

 (وبلاگ داشت خاک میخورد، از یه طرفی هم گفتم به یه درد بشریت بخورم بد نیست:) )

اگه برنامه نویس دات نت هست حتما تا حالا از Linq استفاده کردید. و به احتمال 99.9% تا حالا عبارت ExpressionTree به گوشتون رسیده. ولی چون غالبا خیلی عریان ازش استفاده نمی کن همکارا احتمالا درک خیلی دقیق ازش نداشته باشن.

خب یه تعریف ساده ای که یه ذره من درآوردی هم هست (یه جورایی درک خودم ازش) اینه که ExpressionTree چیزی نیست جز یه درختی که نودهاش از نوع Expression هستن، در نتیجه اگه بدونیم Expression چی هست اونموقع دقیقا متوجه میشیم که با چی طرف هستیم.

بنظر تعریفی که ویکیپدیا خودش تا حد خیلی زیادی گویا هست. ما هم از همین شروع می کنیم:

An expression in a programming language is a combination of one or more explicit valuesconstantsvariablesoperators, and functions that the programming language interprets (according to its particular ruleof precedence and of association) and computes to produce ("to return", in a stateful environment) another value. This process, as for mathematical expressions, is called evaluation.

به عبارت دیگه Expression مجموعه ای از پارامترها، مقادیر ثابت، و عملگرها هست که یه زبان برناهه نویسی اون رو اجرا می کنه و مقدار برمیگردونه. مهمه که دو فاکتور اصلی تعریف کننده Expression رو متوجه بشید:

مجموعه ای عملگرها و پارامترها و مقادیر ثابت که پس از اجرا یه مقداری رو برمیگردونن، حالا بیاین چندتا مثال ببینیم:

 این یکی  یه عملگر و دو عملوند داره که یکی از نوع ثابت و اون یکی از نوع پارامتر هست  // x + 2
این یکی هیچ عملگری نداره و فقط یه پارامتر که بعد همون رو برمیگردونه // x
    این یکی دو تا عملگر داره و سه عملوند و حاصلضرب اینارو برمیگردونه // x * x * x
 این یکی از نوع عبارت بولی هست و دو تا عملگر داره، عملوند سمت چپ خودش یه عبارت مستقل دیگه هست // x && (100%2==0)     f

با این تفاسیر الان میتونیم بگیم یه expression tree یه درختی هست که نودهاش از اشیائی با ساختار بالا ساخته شدن. یعنی فرض کنید این عبارت آخر رو اگه بخوایم مدلش سنتی درختش رو رسم کنیم:



خب با توجه به شماره گذاری که روی نودها کردم در مورد تصویر بالا می تونیم بگیم:
نود شماره ((1)) یه عبارت هست و چون ریشه هست یه مقدار خاص تر تقسیم بندی میشه و اونم اینه که شاخه سمت چپش یه نود دیگه هست از نوع پارامتر و شاخه سمت راست خودش بدنه عبارت هست.
نود شماره ((2)) هم یه عبارت هست از نوع پارامتر
نود شماره ((3)) یه عبارت از نوع باینری && هست، عبارات باینری عبارت هایی هستن که اصطلاحا دوتا عملوند دارن فقط.
نود شماره ((4)) شاخه چپ نود سوم هست که خودش باز دوباره یه عبارت باینری != با دو عملوند x از نوع پارامتر و 2 از نوع ثابت هست.
نود شماره ((5)) یه عبارت باینری هست از نوع مقایسه که دو تا عملوند داره، عملوند سمت چپ خودش دوباره یه نود دیگه  از نوع Binary هست و عملوند سمت چپش هم یه نود از نوع ثابت هست.
نود شماره ((8)) دوباره یه عبارت از نوع باینری Modulo هست که دو عملوند 100و x رو داره.

خب تا اینجای کار ما تونستیم یه عبارت به صورت یه درخت مدل کنیم. و این خیلی عالیه، چون وقتی تونستیم کد رو به صورت درخت مدل کنیم حتما میتونیم درخت رو serialize هم بکنیم و در نتیجه می تونیم کد رو serialize کنیم!!

یعنی مثلا فرض کنید شما این تکه کد رو توی سی شارپ دارید:
x => new HttpClient().Get("mysite.com/Post/x");   //a

خب توی حالت عادی شما این رو میریزید توی یه delegate و اون رو اجرا می کنید و نه بیشتر. حالا اگه میتونستید یه کاری خیلی بیشتر از صرفا اجرای این تکه کد بکنید چه فایده ای داشت. خب تابلوترین فایده ش اینه که شما میتونید درختی که از روی عبارت بالا تولید می کنید رو به یه زبان دیگه کامپایل کنید یا حتی به اون درخت در زمان اجرا نود اضافه یا کم کنید و خلاصه یه منطق دلخواه رو، روی این کد سوار کنید.

مثلا به این یکی دقت کنید:
Where(x => x.Name.Contains("Hellow"); //a

خب طبق تعریف بالا این الان یه درخت هسته که دو تا نود داره، یکیش یه از نوع پارامتر و اون یکی هم از نوع فراخوانی.
خب اینجا نقطه حیاتیه، یعنی ببینید نود سمت راست که میشه همون نود فراخوانی متد اطلاعات مربوط به اون تایپی که متد داره روی اون فراخوانی میشه رو توی همون نود داره. در نتیجه شما به راحتی میتونید بیاید به ازای نودهایی که از نوع فراخوانی متد هستن معادلش رو پیاده سازی کنید.
مثلا برای متد Contains بیاید iike روی توی sql تولید کنید.

خب فکر کنم اگه تا اینجای کار با دقت خونده باشین حتما دیگه متوجه شدید که ExpressionTree چه امکاناتی رو فراهم می کنه، حالا بریم سراغ نحوه استفاده ازونا توی دات نت. (گفتم توی دات چون خیلی از زبان های دیگه هم یه همچین مفهومی رو ساپورت می کنن)

ازونجائی که توی دات نت لمبدا نزدیکترین ساختار به یه درخت رو فراهم می کنه، در نتیجه نقطه آغاز ساخت درخت هم با lambda هست. یعنی اگه بخوایم یه درخت بسازیم اولین قدم اینه که root ش رو با استفاده از یه لمبدا تولید کنیم (راه های دستی دیگه هم هستن البته)
کلا Expression<T> a دیتای مربوط به ریشه درخت رو نگه داری می کنه در نتیجه برای ساخت یه درخت ابتدائی:


Expression<Func<int, bool> exp = x => (x !=2) && (x % 100 ==2);    // simlpe tree

در مورد کد بالا باید گفت دلیل اینکه از Func استفاده کردیم اینه که توی سی شارپ نمایش متدها یا function pointer ها با delegate هست و در نتیجه چون یه expression یه جورایی یه delegate هست، کامپایلر فورس می کنه که پارامتر جنریک یه delegate دلخواه ��اشه حالا مرسومه که از Func استفاده می کنن شما میتونید از هر چی دلتون میخواد استفاده کنید.
دقت کنید که اگه من exp رو فقط از نوع Func تعریف می کردم دیگه درختی تولید نمیشد و در نتیجه فقط یه delegate ساده تولید میشد.
حالا برای بررسی این درخت به این تکه کد دقت کنید:



Expression<Func<int, bool>> exp = x => (x != 2) && (x % 100 == 2);
var root = (BinaryExpression)exp.Body;
var node_4 = (BinaryExpression)root.Left;
var node_5 = (BinaryExpression)root.Right;
var node_7 = (ConstantExpression)node_4.Right;
var node_8 = (BinaryExpression)node_5.Left;
var node_10 = (ParameterExpression)node_8.Left;
var node_11 = (ConstantExpression)node_8.Right;

// این یکی یه عبارت از نوع کال کردن متد هست
Expression<Func<string, bool>> isEmpty = s => String.IsNullOrEmpty(s);
var invokExp = (MethodCallExpression)isEmpty.Body;
// به اطلاعاتی که توی درخت در مورد متد و پارامترش ذخیره شده دقت کنید
Console.WriteLine($"{invokExp.Method.DeclaringType}.{invokExp.Method.Name}({invokExp.Arguments.First()})");
تا همین جای کار دیگه باید تا حد زیادی باید متوجه شده باشید که یه ابزاری مثل EF چجوری کوئری های LINQ شما رو به SQL تبدیل می کنه.
خب تا اینجای کار یه دید اولیه ازین که ExpressionTree چی هست و ما توی دات نت چجوری میتونیم بسازیمش، حالا میخوایم خود کلاس Expression رو یه مقدار بیشتر بررسی کنیم.

نودهای درخت مارو نمونه های کلاس Expression تشکیل می دن، خود کلاس Expression یه کلاس abstract هست، یعنی زیرکلاس هاش هستن که در عمل بدرد ما میخورن.
اما فایده اینکه این کلاس abstract هست اینه که می تونه پیمایش درخت رو برای ما آسون تر کنه.
ساختار وراثت زیرکلاس های Expression به این صورت هست:


بخاطر همین ساختار وراثت بود که تونستیم اون cast ها رو توی کد بالا انجام بدیم.
برای پیمایش ExpressionTree راه های زیادی هست ولی خب ساده ترین راهش، ارث بردن از کلاس ExpressionVisitor و override کردن متد دلخواهتون هست.

class ConstantVisitor : ExpressionVisitor
{
protected override Expression VisitConstant(ConstantExpression node)
{
Console.WriteLine(node.Value);
return node;
}
}

new ConstantVisitor().Visit(root); // تمام نودهای ثابت رو ویزیت خواهد کرد

خب این مبحث رو تا همین جای کار بذاریم و بریم سراغ LINQ.
موقعی که شما به صورت ساده روی یه شی که IEnumerable رو پیاده سازی کرده یه همچین متدی رو کال می کنید 
collection.Where(a => a < 10)                        f
متدی که در عمل اجرا میشه این متد در کلاس System.Linq.Enumerable که در واقع a => a<10 رو به صورت یه delegate میگیره.
حالا اگه collection ما IQueryable رو پیاده سازی کرده باشه اونوقت متد Where ی که در نهایت کال میشه extension متدهای کلاس Queryable در System.Linq.Queryable هست .. این کلاس و این متد

حالا اگه دقت کرده باشین متد Where توی کلاس Enumerable یه Delegate می گیره و متد Where توی کلاس Queryable یه Expression می گیره.
توی حالتی که Enumerable هست و شرط به صورت delegate ارسال میشه که داستان خیلی ساده هست و اون delegate به ازای هر عضو به سادگی فراخوانی میشه.

اما داستان برای حالتی که Queryable هست فرق می کنه، اگه به IQueryable دقت کرده باشین یه property از نوع Provider داره که در اصل تعیین کننده شی هست که کوئری رو در نهایت اجرا می کنه.

public interface IQueryable
{
Expression Expression { get; } // عبارتی که
// Provider
// روی اون عمل خواهد کرد

Type ElementType { get; } // نوع عنصری که بعد از اجرای کوئری برمیگرده
// the provider that created this query
IQueryProvider Provider { get; } // شیئی که در اصل
// سازنده این
// کوئری هست
}

اوکی؟
حالا یه نگاهی به سورس کد متد Where توی کلاس Queryable بندازیم.
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source,
Expression<Func<TSource, bool>> predicate) {
if (source == null)
throw Error.ArgumentNull("source");

if (predicate == null)
throw Error.ArgumentNull("predicate");

return source.Provider.CreateQuery<TSource>(
null,
GetMethodInfo(Queryable.Where, source, predicate),
new Expression[] { source.Expression, Expression.Quote(predicate) }
));
}

همونجوری که گفتیم ساخت IQueryable<T>    a به عهده Provider ش هست.

خود دات نت به صورت دیفالت یه Provider داره که حتما همتون شنیدید Linq to Objects هست. که همون کلاس EnumerableQuery هست. یعنی هر وقت که شما روی Enumerable ها AsQueryable میزنید یه instance ازین کلاس رو به عنوان Provider دارید ست می کنید.
حالا کاری که تکه کد بالا می کنه اینه که یه نود "MethodCall" میسازه، همیــــن و پارامتراش هم به ترتیب: درختی هست که میخواد بهش اضافه بشه، که توی این حالت null فرستاده شده، چون متد static هست و نود ساخته شده یه اخر درخت اضافه میشه، پارامتر دومش متدی هست که قراره به عنوان نود اضافه بشه و پارامتر سوم هم پارامترهایی هست که قراره به نود اضافه بشه (مثال اول - خط آخر رو ببینید- یه Arguments هست اونجا)

احتمالا به ادامه همین نوشتن یه Provider سفارشی هم در آینده اضافه کنم، برای حالا همینجا توقف می کنم چون خیلی طولانی شد.

نظرات (3) -

  • مهدی مقیمی

    03/23/1397 03:59:59 ب.ظ | پاسخ به این نظر

    با سلام
    ممنون از به اشتراک گذاشتن علمتون
    من هم همچین سایتی در یک حوزه دیگه دارم. دوستان اگر علاقه دارن تشریف بیارن
    http://research-moghimi.ir

Loading