388. Eval CodeDom Implementation

בהמשך לפעם הקודמת,

נראה עכשיו דרך אחת לממש Eval בC#.

הדרך הראשונה היא באמצעות CodeDom:

הנה הסבר קצר יחסית על CodeDom למי שלא מכיר:

CodeDom זו ספריה בFramework המייצגת שפה של אובייקטים שבאמצעותם אנחנו יכולים לתאר ולהגדיר קוד.

למשל, אם אנחנו נרצה לכתוב אובייקט בCodeDom שמתאר את הפונקציה הבאה:

1
2
3
4
5
6
7
8
9
public static int Factorial(int number)
{
if (number == 0)
{
return 1;
}
return number * Factorial(number - 1);
}

זה יראה בערך ככה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
CodeMemberMethod method = new CodeMemberMethod();
method.Name = "Factorial";
method.Attributes = MemberAttributes.Public |
MemberAttributes.Static;
method.ReturnType = new CodeTypeReference(typeof (int));
method.Parameters.Add(new CodeParameterDeclarationExpression(typeof (int), "number"));
CodeConditionStatement condition =
new CodeConditionStatement
(new CodeBinaryOperatorExpression(new CodeVariableReferenceExpression("number"),
CodeBinaryOperatorType.ValueEquality,
new CodePrimitiveExpression(0)),
new[] {new CodeMethodReturnStatement(new CodePrimitiveExpression(1))},
new[]{
new CodeMethodReturnStatement(
new CodeBinaryOperatorExpression(
new CodeVariableReferenceExpression("number"),
CodeBinaryOperatorType.Multiply,
new CodeMethodInvokeExpression(
new CodeMethodReferenceExpression(null, "Factorial"),
new[]
{
new CodeBinaryOperatorExpression(
new CodeVariableReferenceExpression("number"),
CodeBinaryOperatorType.Subtract,
new CodePrimitiveExpression(1))
})))
});
method.Statements.Add(condition);

כעקרון זה די לא קשור לEval, אבל זה נותן תמונה כללית טובה על מה זה CodeDom: למשל כדי לעשות return מפונקציה, אנחנו יוצרים CodeMethodReturnStatement וכדי לקרוא למתודה, אנחנו יוצריםCodeMethodInvokeExpression וכו’.


אפשר לעשות עם CodeDom הרבה דברים טובים: הFeature העיקרי זה לג’נרט קוד משלנו שיעשה ניסים ונפלאות (ולבחור שפה – C# או VB!).

אבל קיימת עוד אופציה – אנחנו יכולים לקמפל את הקוד בזמן ריצה ולקבל ממנו Assembly – זה מתבצע ע"י CodeDom ע"י קריאה לcsc.exe – זה הC# Compiler.

בנוסף, אפשר לקמפל בעזרת CodeDom גם string חופשי – ופה נכנס לסיפור הEval:

אנחנו יכולים להשתמש בסיפור הזה כדי להריץ Eval משלנו, זה יראה בערך ככה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static T Evaluate<T>(string expression)
{
string code =
string.Format(
@"using System;
using System.Linq;
public class Evaluation
{{
public static {0} Evaluate()
{{
{0} value = {1};
return value;
}}
}}", FormatType(typeof(T)),
expression);
CSharpCodeProvider provider = new CSharpCodeProvider
(new Dictionary<string, string>
{
{ "CompilerVersion", "v4.0" }
});
CompilerParameters compilerParameters = new CompilerParameters();
compilerParameters.ReferencedAssemblies.AddRange(
typeof (T).Assembly.GetReferencedAssemblies()
.Concat(new[] {typeof (T).Assembly.GetName()})
// Patch for LINQ
.Concat(new[] {typeof (Enumerable).Assembly.GetName()})
.Select(
x => Path.GetFileName(x.CodeBase)).ToArray());
CompilerResults compilerResults =
provider.CompileAssemblyFromSource(compilerParameters, code);
Assembly assembly = compilerResults.CompiledAssembly;
MethodInfo method =
assembly.GetType("Evaluation").GetMethod("Evaluate");
object result = method.Invoke(null, null);
return (T)result;
}

מה שעשינו פה קצת מגעיל: יצרנו מחלקה ממחרוזת שיש בה פונקציה סטטית שאנחנו רוצים, קימפלנו אותה, הרצנו אותה, והחזרנו את ערך ההחזר.

מה שקורה במתודה זה שאנחנו מוסיפים את כל הReferenceים שצריך, דואגים להתקמפל לFramework 4.0, מקמפלים את המתודה, וקוראים לפונקציה בReflection

הפונקציה FormatType שלא צורפה כאן היא פונקציה שמקבלת Type וכותבת אותו בצורה C#ית יפה (עם למשל System.Func<System.Double, System.Double> במקום הFullName שמחזיר

System.Func`2[[System.Double, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Double, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

איך משתמשים בזה? ככה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double three =
Evaluator.Evaluate<double>("3");
Func<double, double> sine =
Evaluator.Evaluate<Func<double, double>>
("x => Math.Sin(x)");
Expression<Func<double, double>> cosineExpression =
Evaluator.Evaluate<Expression<Func<double, double>>>
("x => Math.Cos(x)");
Func<IEnumerable<string>, IEnumerable<string>> linq =
Evaluator.Evaluate<Func<IEnumerable<string>, IEnumerable<string>>>
("collection => from x in collection " +
"where x.Length >= 3 " +
"select x ");
IEnumerable<string> test =
linq(new string[] {"a", "ab", "abc", "abcd", "abcde"});

ככה אפשר לעשות דברים ממש מגניבים, למשל ליצור מתודות ואובייקטים מקלט של המשתמש 😃

יש פה כמה חסרונות:

אחד החסרונות הוא שהקימפול מתבצע ע"י הcsc.exe, כך שאנחנו יוצאים לexe חיצוני שהוא unmanaged בדרך. היה יותר נחמד אם הכל היה מתבצע בצורה Managed.

בנוסף, חסרון נוסף שיש כאן, הוא שכל פעם שאנחנו קוראים למתודה הזאת, נטען Assembly נוסף לAppDomain. הבעיה בטעינת Assemblyים לAppDomain, הוא שאי אפשר לעשות להם unload, וככה אפשר לפוצץ את הAppDomain בAssemblyים.

אבל בכל אופן זה די מגניב.

המשך יום מוערך טוב.

שתף