390. Implementing Eval using Mono CSharp

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

למי שלא מכיר, Mono זה מימוש Open Sourceי של NET Framework.

בפרויקט הזה הם מימשו את כל הרכיבים של הFramework – את הCompiler, את הCLR ואת כל הBCL.

אחד הפרויקטים שהם כתבו זה פרויקט בשם CSharp Compiler (mcs) – זהו קומפיילר שכתבו בC# שבעזרתו בין השאר מקמפלים את Mono (בערך 4 מיליון שורות קוד)

הקומפיילר הוא קומפיילר המכיל באופן מלא את כל התכולות של C# 1.0-4.0. המימוש שלו משתמש באופן כבד בReflection ובReflection.Emit

החל מאיזשהי גרסה של Mono, הם מאפשרים להשתמש בCompiler כService וכך ניתן להפעילו גם בזמן ריצה.

נוכל להשתמש בו לביצוע 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
Evaluator evaluator =
new Evaluator(
new CompilerSettings()
{
AssemblyReferences = new List<string>()
{
"System.Core"
}
},
new Report(new ConsoleReportPrinter()));
evaluator.Run("using System;");
evaluator.Run("using System.Linq;");
while (true)
{
string line = Console.ReadLine();
object result;
bool resultSet;
evaluator.Evaluate(line, out result, out resultSet);
if (resultSet)
{
Console.WriteLine(result);
}
}

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

לדוגמה אם נכתוב int x = 1 * 2 *3;

ולאחר מכן x;

נקבל 6.

נוכל לנצל את זה למימוש Evaluator ככה:

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
44
45
46
47
48
49
50
51
52
53
54
55
public class CodeEvaluator
{
private readonly Evaluator mEvaluator;
public CodeEvaluator()
{
StringBuilder log = new StringBuilder();
mEvaluator =
new Evaluator(
new CompilerSettings()
{
AssemblyReferences = new List<string>()
{
"System.Core"
}
},
new Report(new StreamReportPrinter(new StringWriter(log))));
mEvaluator.Run("using System;");
mEvaluator.Run("using System.Linq;");
}
public T Evaluate<T>(string expression)
{
string expressionToRun =
string.Format("{0} result = {1};",
FormatType(typeof (T)),
expression);
mEvaluator.Run(expressionToRun);
return (T) mEvaluator.Evaluate("result;");
}
public static string FormatType(Type type)
{
if (!type.IsGenericType && !type.IsArray)
{
return type.FullName;
}
else if (type.IsArray)
{
return string.Format("{0}[{1}]", FormatType(type.GetElementType()),
new string(',', type.GetArrayRank() - 1));
}
else if (type.IsGenericType)
{
return string.Format("{0}<{1}>", type.FullName.Substring(0, type.FullName.IndexOf('`')),
string.Join(", ", type.GetGenericArguments().Select(x => FormatType(x))));
}
return null;
}
}

הפעם צרפתי את המתודה FormatType שכתבתי עליה בטיפ מספר 388.

כעת נוכל להשתמש במחלקה כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CodeEvaluator evaluator = new CodeEvaluator();
double three =
evaluator.Evaluate<double>("3");
Func<double, double> sine =
evaluator.Evaluate<Func<double, double>>
("x => Math.Sin(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" });
Expression<Func<double, double>> cosineExpression =
evaluator.Evaluate<Expression<Func<double, double>>>
("x => Math.Cos(x)");

מגניב ממש!


היתרון על הדברים שראינו קודם:

  • לעומת CodeDom – הכל כאן Managed!
  • לעומת Roslyn – כל הFeatureים של Eval כבר ממומשים

החסרונות:

  • נראה שנוצר Assembly עבור כל שורה. ייתכן וניתן לשלוט על זה בCompilerSettings, אבל לא חקרתי את זה לעומק.
  • כאשר Roslyn יהיה גמור, הוא יהיה יותר בשל מהסיפור הזה

בינתיים זו אופציה יחסית לא רעה לעומת שאר האופציות. כמובן, כאשר יצא Roslyn, יהיה עדיף להשתמש בו.

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

שתף

389. Eval Roslyn project implementation

בהמשך לפעמים הקודמות, נראה כעת עוד דרך לממש Eval בC#.

הדרך היא באמצעות ספריה שנכון לרגעים אלה עדיין בפיתוח במיקרוסופט. לספריה קוראים Roslyn והרעיון שלה הוא לאפשר פתרון “Compiler as service”.

הכוונה ב”Compiler as service” הוא הכוונה הבאה:

קומפילציה היא תהליך שבו מתקבלים כקלט קבצי קוד ויוצא כפלט Assembly (המייצג קוד שפת מכונה/שפת Virtual Machine).

במהלך התהליך קורים הרבה דברים, אבל בגדול אפשר לחלק את זה ל4 חלקים עיקריים:

  1. פרסור קוד המקור – תהליך שלוקח את קוד המקור שלנו והופך אותו לאיזשהו אובייקט המייצג אותו (בד”כ עץ)
  2. פענוח הגדרות וSymbolים (הן מהקוד שלנו, והן מקוד חיצוני) – תהליך שרץ על האובייקט הקודם, ומחפש בו הגדרות של Symbolים, כגון מחלקות, מתודות וכו’
  3. Binding – תהליך שיודע לקשר בין מתודות/מחלקות, להפניות למקומות מתאימים בזכרון
  4. Emitting – הפיכת המידע הנ”ל לAssembly

אפשר לחשוב שאולי מספיק לנו להתייחס באמת לקומפיילר כקופסה שחורה שהופכת איכשהו את הקוד שלנו לAssembly, אך היום הIDEים שלנו יודעים לעשות דברים מדהימים כגון:

  • למצוא Referenceים של Symbol מסוים (משתנה/מחלקה/מתודה וכו’)
  • לעשות Refactor
  • לצבוע לנו באדום כאשר שורה לא מתקמפלת
  • ועוד הרבה ניסים ונפלאות!

העניין הוא שכל התהליכים הנ”ל צריכים את המידע מקודם מהקומפיילר בכדי לבצע את משימתם.

מה שקורה עד עכשיו זה שPluginים כמו ReSharper שרצו לבצע חלק מהמשימות הנ”ל, היו צריכים לממש כמעט קומפיילר C# שלם בעצמם!

Roslyn היא בעצם מימוש Managed של הקומפיילר בC#, המאפשרת לנו גישה לכל המידע של החלקים הנ”ל של בתהליך הקומפילציה.

לכל אחד מהחלקים שכתבתי למעלה, יש API מקביל בRoslyn המאפשר לנו לעשות ניסים ונפלאות.

Roslyn מפותחת במיקרוסופט כבר כמה שנים, ובסוף ב2011 יצא הCTP הראשון שלה, ולפני כמה חודשים, יצא CTP נוסף.


מה הקשר לEval?

אחד הדברים שRoslyn מאפשרת Out of the box זה לעשות Eval. זה נעשה כך:

יוצרים ScriptEngine - זהו אובייקט המייצג מעין Interpreter של C#.

1
2
3
ScriptEngine scriptEngine =
new ScriptEngine(new[] { typeof(Enumerable).Assembly },
new[] { "System", "System.Linq" });

בConstructor אני ציינתי שני פרמטרים: לאיזה Assembly להוסיף Reference ולאיזה Namespaceים לעשות using בקוד.

כעת ניתן לבקש ממנו לעשות Eval לאובייקטים כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double three =
scriptEngine.Execute<double>("3");
Func<double, double> sine =
scriptEngine.Execute<Func<double, double>>
("x => Math.Sin(x)");
Func<IEnumerable<string>, IEnumerable<string>> linq =
scriptEngine.Execute<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" });
Expression<Func<double, double>> cosineExpression =
scriptEngine.Execute<Expression<Func<double, double>>>
("x => Math.Cos(x)");

למעשה הכל עובד חוץ מהדוגמה האחרונה שזורקת כרגע Exception בו כתוב שRoslyn לא מימשו עדיין Expressionים.

ממש מגניב 😃


יש כמה יתרונות על מה שראינו בפעם שעברה:

  1. הקוד שאנחנו קוראים לו הוא Managed! (לא קוראים יותר לcsc.exe)
  2. לא נוצר Assembly על כל Eval שאנחנו מפעילים – זאת בזכות המימוש של Roslyn שנעשה באמצעות Reflection.Emit:

נוצר Assembly דינאמי יחיד:

Program.exe’ (Managed (v4.0.30319)): Loaded ‘Anonymously Hosted DynamicMethods Assembly’

החסרון הגדול הוא שכרגע Roslyn עדיין רק CTP, וכפי שראינו עדיין לא מומשו כל התכולות.

נקווה שבהמשך תמומשנה כלל התכולות, ונוכל להשתמש בספריה זו לניסים ונפלאות. אני מניח שגם החבר’ה בJetBrains ישנו את הקוד שלהם שישתמש בRoslyn וכו’.


מן הסתם אפשר לעשות עוד הרבה דברים בRoslyn והראתי Feature מאוד מאוד מאוד ספציפי. למי שמתעניין, אני מציע לחקור קצת באינטרנט ולשחק עם הCTP.

יש למשל עם הCTP איזשהו Demo מגניב שבו ממירים קוד מC# לVB ולהפך.

סופ"ש מוערך לטובה.

שתף

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ים.

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

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

שתף

387. Eval

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

ראינו בפעם שעברה שניתן לכתוב מתודה שמקבלת Expression המייצג פונקציה מתמטית, ומחזירה את הנגזרת שלו כExpression אחר.

כעת יש שאלה מתבקשת: כיצד ניתן לכתוב אפליקציה המקבלת את הExpression הנ”ל כקלט מהמשתמש וכותבת לו את התוצאה?

היינו אולי מצפים שתהיה פונקציה Expression.Parse שמקבלת מחרוזת ויוצרת ממנה Expression, אך אין דבר כזה בFramework.

מה שכן, אם המשתמש שלנו היה יכול לכתוב קוד, אז היה לו יחסית פשוט לכתוב משתנה מהסוג Expression> ולאתחלו בLambda שמייצגת את הפונקציה שלו. (ראו גם פעם קודמת)

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

לפעולה כזאת קוראים Evaluation ויצא לכם בוודאי להיתקל בה:

למשל, כאשר אנחנו כותבים בWatch של Visual Studio איזשהו ביטוי, מתבצע Evaluation שלו כדי לחשב את הביטוי ולהציג לנו ערך שמייצג אותו.

לדבר הזה שימושים רבים, ביניהם:

  • כתיבת קונפיגורציה מורכבת (קונפיגורציה כזאת עשויה להכיל קטעי קוד שירוצו בתכנית שלנו – למשל, קונפיגורציה שמכילה נוסחה בה נשתמש על מנת לחשב משהו בתהליך שלנו)
    • בין השאר הכנסת שאילתות LINQ לקונפיגורציה שלנו – כך נוכל לשנות את השאילתות מול הDatabase בקונפיגורציה ובנוסף להשתמש בLINQ
  • כתיבת Interactive Shell – זוהי אפליקציה המאפשרת לנו להריץ שורות קוד בזמן ריצה, בין השאר היא מאפשרת לנו לעשות בדיקות On the fly, מבלי לקמפל ולהריץ כל פעם מחדש. כנראה מה שרובנו עושים כדי לעשות בדיקות כאלה היום זה להריץ את התוכנה בDebug ולעשות את הניסויים הנ”ל בWatch
  • יצירת תוכנות בעלות התנהגות דינאמית – תוכנות כאלה יכולות לשנות לעצמן את ההתנהגות ע”י בניית ביטויים דינאמיים בזמן ריצה, והרצתם

ואין גבול לדמיון למה שניתן לעשות עם יכולת כזאת.

בחזרה לעניינו – אנחנו מעוניינים לבצע Eval לאיזשהי מחרוזת. כיצד ניתן לעשות זאת?

בהמשך נראה מספר שיטות לבצע משימה זו.

סופ”ש עם הרבה הערכה טוב.

שתף

386. Symbolic Differentiation

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

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

איך נעשה את זה? הכרנו בעבר Expression Trees (טיפ מספר 173-175).

נשתמש בהם בשביל לייצג את הפונקציות והנגזרות שלהן.

המימוש הוא יחסית פשוט, זה נראה משהו כזה:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public static Expression Differentiate<T>(Expression<T> expression)
{
return InnerDifferentiate(expression.Body);
}
private static Expression InnerDifferentiate(Expression expression)
{
BinaryExpression binaryExpression =
expression as BinaryExpression;
switch (expression.NodeType)
{
case ExpressionType.Constant:
return Expression.Constant(0.0);
case ExpressionType.Parameter:
return Expression.Constant(1.0);
case ExpressionType.Call:
return DifferentiateUnary(expression);
case ExpressionType.Add:
return Expression.Add(InnerDifferentiate(binaryExpression.Left),
InnerDifferentiate(binaryExpression.Right));
case ExpressionType.Subtract:
return Expression.Subtract(InnerDifferentiate(binaryExpression.Left),
InnerDifferentiate(binaryExpression.Right));
case ExpressionType.Multiply:
return Expression.Add(
Expression.Multiply(InnerDifferentiate(binaryExpression.Left),
binaryExpression.Right),
Expression.Multiply(InnerDifferentiate(binaryExpression.Right),
binaryExpression.Left));
case ExpressionType.Divide:
return Expression.Divide(
Expression.Subtract(
Expression.Multiply(InnerDifferentiate(binaryExpression.Left),
binaryExpression.Right),
Expression.Multiply(InnerDifferentiate(binaryExpression.Right),
binaryExpression.Left)),
Expression.Multiply(binaryExpression.Right,
binaryExpression.Right));
}
return null;
}
private static Expression DifferentiateUnary(Expression expression)
{
MethodCallExpression methodCall = expression as MethodCallExpression;
Expression argument = methodCall.Arguments[0];
return Expression.Multiply(GetDerivative(methodCall.Method, argument),
InnerDifferentiate(argument));
}
private static Expression GetDerivative(MethodInfo function, Expression argument)
{
if (function.DeclaringType == typeof(Math))
{
switch (function.Name)
{
case "Sin":
return Expression.Call(GetMethod(() => Math.Cos(default(double))),
argument);
case "Cos":
return Expression.Negate
(Expression.Call(GetMethod(() => Math.Sin(default(double))),
argument));
case "Exp":
return (Expression.Call(GetMethod(() => Math.Exp(default(double))),
argument));
case "Log":
return Expression.Divide(Expression.Constant(1),
argument);
}
}
return null;
}
private static MethodInfo GetMethod<T>(Expression<Func<T>> expression)
{
return (expression.Body as MethodCallExpression).Method;
}

קצת הסבר: הפונקציה InnerDifferentiate יודעת לגזור פונקציות לפי חוקי הגזירה:

  • $ (u + v)’ = u’ + v’ $
  • $ (u - v)’ = u’ - v’ $
  • $ (u \cdot v)’ = u’v + v’u $
  • $ \displaystyle{(\frac{u}{v})’ = \frac{u’v - v’u}{v^2}} $

בנוסף: היא מחזירה 0 במידה והיא מקבלת קבוע, ומחזירה 1 במידה והיא מקבלת פרמטר. (הפונקציה כרגע מניחה שקיים פרמטר יחיד, לפיה היא גוזרת)

הפונקציה DifferentiateUnary יודעת לגזור קריאה לפונקציה פשוטה (למשל $ \sin $, $ \cos $, $ \exp $) עפ"י כלל השרשרת

$ (f(g(x)))’ = f’(g(x)) \cdot g’(x) $

שימו לב שהפונקציה GetDerivative משיגה MethodInfo על ידי טריק שראינו בעבר (ראו גם טיפ מספר 176)

כעקרון המימוש יחסית פשוט, ולא עושה הרבה דברים שיכל לעשות, למשל לפשט את הנגזרות, להתמודד עם מינוס לביטוי, להתמודד עם בסיסים אחרים של הלוגריתם וכו’.

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

1
2
3
4
Expression<Func<double, double>> expression =
x => Math.Sin(x) / (x * x + 1);
Expression derivative = Differentiate(expression);

זה מחזיר משהו כזה:

((((Cos(x) 1) ((x x) + 1)) - ((((1 x) + (1 x)) + 0) Sin(x))) / (((x x) + 1) ((x * x) + 1)))

שאחרי כמה פישוטים אפשר לראות שזה זהה למה שקיבלנו פעם שעברה.

אז למה זה יותר טוב?

קודם כל, אנחנו מקבלים הפעם ביטוי מתמטי ולא סתם פונקציה אמורפית, כך שאנחנו יכולים לעשות על ביטוי זה אנליזה מתמטית טהורה (לצורך העניין, אם למשל קיבלנו בשיעורי בית איזשהו ביטוי מתמטי ואנחנו צריכים לגזור אותו, אפשר בתיאוריה להשתמש בקוד כזה כדי לעשות את זה)

בנוסף, יותר קל לגזור את הביטוי מספר פעמים, במידה וזו מטרתנו (בפעם הקודמת התקשינו לגזור את הפונקציה יותר מפעם אחת)

המשך יום סימבולי טוב

שתף

385. Dual numbers

נחזור קצת לטיפים בנושאים מתמטיים:

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

למשל, אלגוריתם ניוטון רפסון מאפשר לנו למצוא אפסים של פונקציה בצורה יחסית מהירה. (דיברנו על זה קצת בטיפ 341).

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


קיימים מספר סוגי גזירות של פונקציה:

  • גזירה נומרית – מאפשרת לנו לקבל קירוב לערך של הנגזרת של הפונקציה בנקודה מסוימת
  • גזירה סימבולית – מאפשרת לנו להתייחס לפונקציה כביטוי מתמטי, וע”י פעולת הגזירה לקבל ביטוי מתמטי אחר.

על גזירה סימבולית לא נדבר הפעם, אך רק נציין שיש לגזירה זו קשר לExpression Trees שראינו ב.net 3.5 (טיפים מספר 173-181)

כעת גם גזירה נומרית מתחלקת למספר סוגים:

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

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

נדבר על הסוג השני כעת.


אז איך אפשר לחשב נגזרת של פונקציה בצורה מדויקת? שיטה אחת נקראת Dual Numbers והיא מוגדרת ככה:

נגדיר מספר דואלי בתור זוג סדור $ (x, y)$ של מספרים ממשיים. נגדיר עליו את הפעולות הבאות:

  • $ \left(u, u’ \right) + \left( v, v’ \right) = \left(u + v, u’ + v’ \right) $
  • $ \left(u, u’ \right) - \left( v, v’ \right) = \left(u - v, u’ - v’ \right) $
  • $ \left(u, u’ \right) \cdot \left( v, v’ \right) = \left(u \cdot v, u \cdot v’ + v \cdot u’ \right) $
  • $ \displaystyle{\frac { \left(u, u’ \right) }{ \left( v, v’ \right) } = \left( \frac{u}{v}, \frac{u’ \cdot v - u \cdot v’}{v^2} \right) } $

בנוסף נגדיר לפונקציה גזירה f פונקציה דואלית F $ F\left(u, u’ \right) = \left(f(u), f’(u) \cdot u’ \right) $, למשל

  • $ \sin \left(u, u’ \right) = \left(\sin u, \cos u \cdot u’ \right) $
  • $ \cos\left(u, u’ \right) = \left(\cos u, -\sin u \cdot u’ \right) $
  • $ \exp\left(u, u’ \right) = \left(\exp u, \exp u \cdot u’ \right) $

בנוסף נתייחס לקבוע $ C $ בתור הערך $ \left( C, 0 \right) $


למה זה טוב? נגדיר ואנחנו רוצים לחשב את הנגזרת של הפונקציה $ \displaystyle{\frac{\sin x}{x^2 + 1}} $:

נתחיל עם הזוג $ \left( x, 1 \right) $ (כי הנגזרת של הפונקציה x בכל נקודה היא 1).

נבצע עליה פעולת $ \sin $ ונקבל $ \left( \sin x, \cos x \right) $.

בנוסף נבצע $ \left( x, 1 \right) \cdot \left( x, 1 \right) = \left( x \cdot x, x \cdot 1 + 1 \cdot x \right) = \left( x^2, 2x \right) $ ונוסיף לזה $ \left(1, 0\right) $ ונקבל $ \left(x^2 + 1, 2x \right) $. כעת נבצע את הפעולה $ \frac{\left(\sin x, \cos x \right)}{\left(x^2 + 1, 2x \right)}$ ונקבל $ \displaystyle{ \left(\frac{\sin x}{x^2 + 1}, \frac{\cos x \cdot (x^2 + 1) - \sin x \cdot (2x)}{(x^2 + 1)^2} \right) } $

שימו לב שקיבלנו שהנגזרת של $ \displaystyle{\frac{\sin x}{x^2 + 1}} $ היא $ \displaystyle{ \frac{\cos x \cdot (x^2 + 1) - \sin x \cdot (2x)}{(x^2 + 1)^2} } $

קיבלנו את זה בלי להשתמש ישירות בנוסחאות של התיכון, אלא רק בנוסחאות שרשמתי מעלה.

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

הפעולות הבינאריות והאונאריות שהוגדרו למעלה, פועלות “כרגיל” על החלק שמייצג את הערך של הפונקציה בנקודה, ומכבדות את חוקי הגזירה בחלק שמייצג את הנגזרת בנקודה.


אז איך נשתמש בזה בתכנות? קל לכתוב מחלקה שתממש את הפונקציונאליות, למשל:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
public class DualNumber
{
#region Data Members
private double m_FunctionValue;
private double m_DerivativeValue;
private static readonly DualNumber m_Zero = new DualNumber(0, 0);
private static readonly DualNumber m_One = new DualNumber(1, 0);
#endregion
#region Constructors
public DualNumber(double functionValue,
double derivativeValue)
{
m_FunctionValue = functionValue;
m_DerivativeValue = derivativeValue;
}
public DualNumber(DualNumber source) :
this(source.FunctionValue,
source.DerivativeValue)
{
}
#endregion
#region Constants
public static DualNumber ONE
{
get
{
return m_One;
}
}
public static DualNumber ZERO
{
get
{
return m_Zero;
}
}
#endregion
#region Binary Methods
private DualNumber Add(DualNumber givenDual)
{
return new DualNumber(this.FunctionValue + givenDual.FunctionValue,
this.DerivativeValue + givenDual.DerivativeValue);
}
private DualNumber Sub(DualNumber givenDual)
{
return new DualNumber(this.FunctionValue - givenDual.FunctionValue,
this.DerivativeValue - givenDual.DerivativeValue);
}
private DualNumber Mul(DualNumber givenDual)
{
return new DualNumber(this.FunctionValue * givenDual.FunctionValue,
this.DerivativeValue * givenDual.FunctionValue+
this.FunctionValue * givenDual.DerivativeValue);
}
private DualNumber Div(DualNumber givenDual)
{
return new DualNumber(this.FunctionValue / givenDual.FunctionValue,
(this.DerivativeValue * givenDual.FunctionValue -
this.FunctionValue * givenDual.DerivativeValue)/
(givenDual.FunctionValue * givenDual.FunctionValue));
}
private DualNumber Power(int exp)
{
if (exp >= 0)
{
return this.PositivePower(exp);
}
else
{
return (this.Opposite.PositivePower(-exp));
}
}
private DualNumber Opposite
{
get
{
return new DualNumber(1/this.FunctionValue,
-this.DerivativeValue/(this.FunctionValue * this.FunctionValue));
}
}
private DualNumber PositivePower(int exp)
{
DualNumber powerResult = DualNumber.ONE;
DualNumber loopPower = new DualNumber(this);
int loopCurrentPower = exp;
while (loopCurrentPower != 0)
{
if (loopCurrentPower % 2 == 1)
{
powerResult = powerResult.Mul(loopPower);
}
loopPower = loopPower.Mul(loopPower);
loopCurrentPower /= 2;
}
return powerResult;
}
public override bool Equals(object obj)
{
if (!(obj is DualNumber))
{
return false;
}
else
{
DualNumber comparedDual = (DualNumber)obj;
return (this.FunctionValue.Equals(comparedDual.FunctionValue) &&
this.DerivativeValue.Equals(comparedDual.DerivativeValue));
}
}
public override int GetHashCode()
{
return (this.FunctionValue.GetHashCode() ^
this.DerivativeValue.GetHashCode());
}
#endregion
#region Unary Methods
public double FunctionValue
{
get
{
return m_FunctionValue;
}
}
public double DerivativeValue
{
get
{
return m_DerivativeValue;
}
}
#endregion
#region Operators
public static DualNumber operator +(DualNumber z1, DualNumber z2)
{
return z1.Add(z2);
}
public static DualNumber operator -(DualNumber z1, DualNumber z2)
{
return z1.Sub(z2);
}
public static DualNumber operator *(DualNumber z1, DualNumber z2)
{
return z1.Mul(z2);
}
public static DualNumber operator /(DualNumber z1, DualNumber z2)
{
return z1.Div(z2);
}
public static DualNumber operator ^(DualNumber z, int exp)
{
return z.Power(exp);
}
public static bool operator ==(DualNumber z1, DualNumber z2)
{
return z1.Equals(z2);
}
public static bool operator !=(DualNumber z1, DualNumber z2)
{
return (!z1.Equals(z2));
}
#endregion
#region Elementary functions
public static DualNumber Cos(DualNumber argument)
{
return new DualNumber(Math.Cos(argument.FunctionValue),
-Math.Sin(argument.FunctionValue)*
argument.DerivativeValue);
}
public static DualNumber Sin(DualNumber argument)
{
return new DualNumber(Math.Sin(argument.FunctionValue),
Math.Cos(argument.FunctionValue) *
argument.DerivativeValue);
}
public static DualNumber Exp(DualNumber argument)
{
return new DualNumber(Math.Exp(argument.FunctionValue),
Math.Exp(argument.FunctionValue) *
argument.DerivativeValue);
}
#endregion
}

כעת נוכל לכתוב פונקציה שמחשבת את הנגזרת של הפונקציה $ \frac{\sin x}{x^{2}+1} $ בנקודה נתונה בצורה תכנותית. זה יראה כך:

1
2
3
4
5
6
7
8
9
public static double Derivative(double x)
{
DualNumber number = new DualNumber(x, 1);
DualNumber function =
DualNumber.Sin(number) / (number*number + DualNumber.ONE);
return function.DerivativeValue;
}

או אם נרצה לחשב את הנגזרת של הפונקציה $ e^{-\frac{x^{2}}{2}}\sin x $ זה יראה ככה:

1
2
3
4
5
6
7
8
9
10
public static double Derivative(double x)
{
DualNumber number = new DualNumber(x, 1);
DualNumber function =
DualNumber.Exp(number * number * new DualNumber(-0.5, 0)) *
DualNumber.Sin(number);
return function.DerivativeValue;
}

כמובן, אפשר לשפר את הסינטקס, אבל זה ממש מגניב!


היתרון הגדול בשיטה היא הפשטות שלה: מאוד קל לגזור ככה פונקציות – פשוט נכתוב ביטוי מתמטי ונקבל חינם את החישוב של הנגזרת שלו.

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

  • אם מעניין אותנו נגזרת מסדר גבוה יותר, אפשר לעשות טריק דומה וליצור שלשות שבקואורדינטה השלישית שלהן מכבדות את חוקי הנגזרת השנייה. הבעיה שהחוקים עבור נגזרת שנייה ושלישית מתחילים להיות מאוד מסובכים ולא פשוטים כפי שהיה כאן.
  • בנוסף, השיטה נותנת את הערך בנקודה, אבל לא נותנת לנו ביטוי מתמטי שמייצג את הנגזרת. החסרון הוא שלא נוכל למשל לפשט את הביטוי או לבצע בו אופטימיזציות כך שיהיה יותר מדויק/יותר מהיר וכו’.
    • בנוסף, לא נוכל להסתכל בעין מזוינת ולבצע בו ניתוח מתמטי – איפה הוא מוגדר, מתי הוא מקבל מינימום או מקסימום וכו’.
  • לבסוף, אנחנו תלויים בדיוק של המחשב בו אנחנו עובדים – במקרים מסוימים אנחנו עשויים לסטות בהרבה מהתוצאה המדויקת של הנגזרת של הפונקציה בנקודה, וזאת רק מאחר ובצענו למשל יותר מדי פעולות חילוק או פעולות שלא שומרות על הדיוק.

שיהיה סופ"ש דואלי טוב.

שתף

384. Happy new year and happy second birthday

תוכן חסר

שתף

383. Preserving the stack trace of an exception

ראינו בטיפ מספר 65 מהי הדרך הנכונה לזרוק Exception שתפסנו.

ראינו שקיים הבדל בין throw; לthrow ex;.

במקרים ממש נדירים, לא נתפוס Exception, אלא נקבל אותו כערך מפונקציה/מקום אחר. במקרים כאלה כאשר נזרוק את הException היינו מעוניינים לשמר את הStack trace המקורי של הException.

לדוגמה, Castle כתבו ספריה שנקראת Castle Windsor Wcf Facilities.

אחד הדברים שהם עושים שם בין השאר זה להחליף את הProxy הסטנדרטי שמגיע עם Wcf (שממומש ע”י RealProxy – ראו גם טיפ מספר 348) במימוש שלהם ע”י מימוש בזמן ריצה את הממשק של הServiceContract. (ראו גם טיפים 349, 347)

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

איך הם עשו זאת? הם מחזיקים את הProxy הרגיל בתור Member וקוראים לפונקציית הניתוב שלו.

פונקציית הניתוב מחזירה מדי פעם Exception, אבל בתור פרמטר של הReturnMessage. הבעיה בזה היא שאם זורקים את הException בצורה נאיבית ע”י throw message.Exception;, יעלם הStack Trace המקורי.

Castle מצאו איזשהו פתרון נחמד.

מסתבר שיש במחלקה Exception איזושהי מתודה בשם InternalPreserveStackTrace שאם נקרא לה, אז הStackTrace ישמר.

הבעיה – המתודה היא internal.

ככה נראה הפתרון שלהם:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class ExceptionHelper
{
private static readonly MethodInfo PreserveStackTraceMethod =
typeof(Exception).GetMethod("InternalPreserveStackTrace",
BindingFlags.Instance |
BindingFlags.NonPublic);
public static Exception PreserveStackTrace(Exception exception)
{
if (PreserveStackTraceMethod != null && exception != null)
{
PreserveStackTraceMethod.Invoke(exception, null);
}
return exception;
}
}

הם פשוט קוראים למתודה בReflection 😃


בגדול זה צורך שהוא לא כל כך נפוץ, ומן הסתם זה די Bad Practice לקרוא למתודה שהיא Private בReflection.

עם זאת, מעניין לראות שיש תמיכה בדבר כזה. אם נסתכל בReflector על המימוש של המתודה, נראה שהוא בסה"כ מאתחל איזשהו שדה של הException שנקרא _remoteStackTraceString.

ע"י טיפה יותר מחקר, ניתן להבין שזהו שדה שהמציאו בRemoting על מנת להעביר את הStack Trace של Exception שקפץ בצד Server לClient. לא הצלחתי למצוא שימוש במתודה InternalPreserveStackTrace בReflector, אבל כנראה Remoting או dll אחר של הFramework משתמש בזה.

המשך יום משמר עקבות של מחסנית טוב.

שתף

382. Memento design pattern

Design Pattern נוסף שקיים הוא הDesign Pattern ששמו Memento.

הDesign Pattern בא לפתור את הבעיה הבאה: נניח שיש לנו מחלקה עם State, ואנחנו מעוניינים לשמור את הState הנוכחי כדי שנוכל לשחזר אותו.

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

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


תיאור הPattern:

מה שנעשה זה ניצור איזשהו אובייקט שמכיל את הסטטוס שלנו:

נניח שזו מחלקה שמחשבת איזשהי מפה (מספר איטרציות לנקודה במישור):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class IteratedMap
{
private int[,] mPointToIterationNumber;
private int mRowsProcessed;
private int mColumnsProcessed;
private int mRowSize;
private int mColumnSize;
private int mNumberOfIterations;
public void Iterate()
{
// Do some algorithm on mPointToIterationNumber here
}
}

ניצור אובייקט שמכיל את הסטטוס שלה:

1
2
3
4
5
6
public class IteratedMapState
{
public int[,] PointToIterationNumber { get; set; }
public int RowsProcessed { get; set; }
public int ColumnsProcessed { get; set; }
}

בנוסף ניצור שתי פונקציות למחלקה:

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
public class IteratedMap
{
private int[,] mPointToIterationNumber;
private int mRowsProcessed;
private int mColumnsProcessed;
private int mRowSize;
private int mColumnSize;
private int mNumberOfIterations;
// ...
public IteratedMapState SaveState()
{
return new IteratedMapState()
{
PointToIterationNumber = (int[,]) mPointToIterationNumber.Clone(),
ColumnsProcessed = mColumnsProcessed,
RowsProcessed = mRowsProcessed
};
}
public void RecoverState(IteratedMapState state)
{
mPointToIterationNumber = (int[,]) state.PointToIterationNumber.Clone();
mColumnsProcessed = state.ColumnsProcessed;
mRowsProcessed = state.RowsProcessed;
}
}

כך נוכל לשמור ולשחזר את המצב של המחלקה שלנו בשלב כלשהו בתכנית.


נשים לב שעל הState שלנו להיות Deep Clone של הMemberים של האובייקט שלנו, כדי שאם נשנה אותו, נוכל לשחזר את הState הקודם.

בנוסף, נשים לב שמומלץ שSaveState יהיה Thread-safe כדי שהState יהיה אמין במידה ונקרא לו מThread אחר כאשר המחלקה שלנו באמצע תהליך.

לבסוף, כדאי להמנע משימוש בPattern הזה כדי לעשות Undo לנזק שעשינו על מחלקות, מאחר וזה יכול לגרום לקוד שלנו להיות מאוד מסורבל.

המשך יום goto טוב.

שתף

381. Null Object pattern

נראה הפעם איזשהו Pattern שMartin Fowler כתב עליו ב1999.

הPattern נקרא Null Object והמטרה שלו היא לפתור את הבעיה הזאת:

לעתים איזושהי פונקציה בקוד שלנו שמחזירה אובייקט, מחזירה null.

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

דיברנו על וואריאציות של הבעיה הזו כבר בעבר (טיפים 106-110, טיפ 48).

כעת נראה פתרון אלטרנטיבי.


הפתרון הוא ליצור אובייקט שמממש את הממשק של החתימה של הפונקציה, אבל עם התנהגות “טיפשה”, למשל נניח ויש לנו ממשק כזה:

1
2
3
4
5
6
public abstract class PeopleProvider
{
public abstract IEnumerable<Person> RetrievePeopleByAge(int age);
public abstract void InsertPerson(Person person);
public abstract void UpdatePerson(Person person);
}

נוכל לממשו כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class NullPeopleProvider : PeopleProvider
{
public override IEnumerable<Person> RetrievePeopleByAge(int age)
{
return Enumerable.Empty<Person>();
}
public override void InsertPerson(Person person)
{
}
public override void UpdatePerson(Person person)
{
}
}

כך אם יש לנו פונקציה שמקבלת PeopleProvider נוכל לסכם מוסכמה שאנחנו לא מוכנים לקבל null, וכאשר המשתמש מעוניין שלא יתבצע כלום, שיעביר לנו NullPeopleProvider.


בFramework יש כמה Null Objectים שיצא לנו להכיר:

  • מערך ריק: בC# קיים מערך שהוא ריק – זה בעצם סוג של "Null Object" כי קריאה לכל המתודות של המערך לא עושה כלום
  • מחרוזת ריקה: דומה למה שהוזכר קודם – במידה ונרצה למשל לגשת לLength של הString, לא נקרוס אם מדובר בString.Empty, וכך הקוד שלנו דומה לשני המקרים.

למה זה טוב:

  • נניח שיש לנו מערכת שמורכבת מכמה מודולים שתלויים אחד בשני, ואנחנו מעוניינים להריץ את המערכת בלי אחד המודולים, אז נוכל להחליף את אחד המודולים במימוש NullObject שלו מבלי לבדוק בכל שורה שנייה בכל מודול האם אחד המודולים הוא Null או לא.
    • לדוגמה, נניח שיש לנו עמדת משתמש שמכילה מפה גיאוגרפית, וכל המערכת יודעת לגשת למפה בכדי להציג עליה נתונים – נוכל במקום שבכל מקום במערכת יבדקו האם המפה קיימת או לא (מה שמסרבל מאוד את הקוד), נחליף את המימוש של המפה במימוש NullObject.
  • הNullObjectים האלה יכולים לשמש כMocking לUnit Tests. (ראו גם טיפ מספר 350)

עם זאת:

  • כדאי מאוד להזהר בשימוש של הPattern הזה – הוא עשוי ליצור באגים לא צפויים במידה ומשתמשים בו לא בזהירות.
  • שימו לב שלפעמים כן נרצה לבצע התנהגות שונה במידה וקיבלנו NullObject – את זה נוכל לבצע ע"י בדיקה על האובייקט (כמו השוואתו לstring.Empty, בדיקת סוגו וכו’)

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

שתף