#C,  SOLID

איך עובד Linq ב- #C

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

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

אז איך Linq עובד?

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

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

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

אז מה עושים?

כאשר חיפשו במיקרוסופט את המכנה המשותף הנמוך ביותר (מבחינת היררכיית ירושה) של מבני נתונים, הם בחרו את ה Interface IEnumerable<T>.

IEnumerable<t> הוא האינטרפייס המתאים ביותר להוסיף לו מתודות הקשורות לנתונים מכיוון שהוא מכיל מתודה אחת ויחידה – GetEnumerator(). זו מתודה המחזירה אובייקט המאפשר לעבור על נתונים, אחד בכל פעם. כמו למשל כאשר עוברים על מערך בלולאת ForEach.

במיקרוסופט החליטו שלא להוסיף מתודות ל IEnumerable<T> אלא להשתמש בטכניקה שנקראת Extension Methods, ועל כך נרחיב מעט.

Extension Methods

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

איך זה עובד:

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

אם כן, ירושה אינה באה בחשבון כאשר רוצים להרחיב פונקציונליות של DateTime.

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

האפשרות שאותה נדגים היא על ידי שימוש ב Extension Methods.

נניח שיש לי מופע של DateTime מאותחל לתאריך כלשהו ואני רוצה לדעת כמה ימים נשארו עד לסוף החודש הזה.

אני יכול לחשב את זה באופן הבא:

   1: DateTime date = new DateTime(2014, 8, 8);

   2: int daysUntilEndOfMonth = DateTime.DaysInMonth(date.Year, date.Month) - date.Day;

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

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

דוגמא למחלקת עזר:

   1: public static class DateTimeExtensions

   2: {

   3:     public static int DaysToEndOfMonth(DateTime date)

   4:     {

   5:         return DateTime.DaysInMonth(date.Year, date.Month) - date.Day;

   6:     }

   7: }

דוגמא לשימוש במחלקת עזר:

   1: DateTime date = new DateTime(2014, 8, 8);

   2: int daysUntilEndOfMonth = DateTimeExtensions.DaysToEndOfMonth(date);

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

ועכשיו לקסם….

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

כעת המתודה תראה כך:

   1: public static int DaysToEndOfMonth(this DateTime date)

   2: {

   3:     return DateTime.DaysInMonth(date.Year, date.Month) - date.Day;

   4: }

והשימוש בה יראה פשוט כך:

   1: int daysUntilEndOfMonth = date.DaysToEndOfMonth();

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

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

בחזרה לLINQ

הזכרנו את IEnumareble<T> כמועמד פוטנציאלי להחזקת מתודות לשאילתות, אבל אמרנו שלא רצו לשנות אותה, ולכן הדרך הנכונה היא ליצר מחלקה ייעודית לעניין להשתמש ב Extension Methodל IEnumareble<T>.

למשל היה אפשר ליצר מתודה שמקבלת ומחזירה IEnumerable<string> ומסננת רק את אלו ברשימה שמתחילים באות מסויימת, באופן הזה:

   1: public static class FilterExtensions

   2: {

   3:     public static IEnumerable<string> StringThatStartWith

   4:     (this IEnumerable<string> input,string start )

   5:     {

   6:         foreach (var s in input)

   7:         {

   8:             if (s.StartsWith(start))

   9:             {

  10:                 yield return s;

  11:             }

  12:         }

  13:     }

  14: }

ואז להשתמש בזה למשל באופן הזה:

   1: IEnumerable<string> employees = new[] {"Avi", "Danny", "Moshe"};

   2: var startWithA = employees.StringThatStartWith("A");

וזה כמובן בתנאי שיש לי using ל namespace שבו נמצאים הextensions שלי.

בדיוק בדרך הזו LINQ עובד. מוסיפים את הnamespace system.Linq ובכך מתווספות אפשרויות רבות למערך שלנו.

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

נחזור לדוגמא שלנו:

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

אם כך נשנה את החתימה של המתודה למשהו גנרי יותר (בעזרת T) ובמקום לקבל כפרמטר את הקריטריון לסינון (אות ההתחלה במקרה שלנו) נגדיר delegate שאף הוא מקבל T כפרמטר ומחזיר bool כתוצאה (וזה בשביל לפלטר – כלומר לדעת האם item כלשהו קיים ברשימה).

כעת הקוד יראה כך:

   1: public static class FilterExtensions

   2: {

   3:     public static IEnumerable<T> Filter<T>

   4:     (this IEnumerable<T> input,FilterDelegate<T> issue)

   5:     {

   6:         foreach (var item in input)

   7:         {

   8:             if (issue(item))

   9:             {

  10:                 yield return item;

  11:             }

  12:         }

  13:     }

  14:  

  15:     public delegate bool FilterDelegate<T>(T item);

  16: }

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

באופן הזה:

   1: static void Main(string[] args)

   2: {

   3:     IEnumerable<string> employees = new[] { "Avi", "Danny", "Moshe" };

   4:     var startWithA = employees.Filter(StringThatStartWithA);

   5: }

   6:  

   7: private static bool StringThatStartWithA(string s)

   8: {

   9:     return s.StartsWith("A");

  10: }

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

את הבעיה הזו נפתור על ידי שימוש ב anonymous delegate.

במקום לשלוח מתודה ספציפית לפילטר נכתוב כך:

   1: var startWithA = employees.Filter(delegate(string item)

   2: {

   3:     return item.StartsWith("A");

   4: });

   5:  

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

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

כמו כן ידוע שהפרמטר הוא מסוג string בגלל שהמתודה הופעלה על מערך של string, ולכן אפשר להיפטר גם מהמילה string.

גם מהמילה return נוכל להיפטר, וכן מהסוגריים המסולסלות.

נוסיף את האופרטור => (goes to) על מנת להגדיר לקומפיילר שזה המשתנה שלנו.

כעת הקוד נראה כך:

   1: var startWithA = employees.Filter((item)=>item.StartsWith("A"));

נראה קצת מוכר? סימן שאנחנו מתקרבים ליעד…

נשארנו עם אותו delegate אבל עם פחות קוד. צורת כתיבה זו נקראת Lambda Expression שבעזרתה כותבים delegate החל מ C# 3.0.

הצד השמאלי של המשוואה מסמן את החתימה של המתודה – מתודה המקבלת כפרמטר string (מכיוון שהיא הופעלה על מחרוזת של string). והצד הימני הוא הפעולה עצמה.

שימוש ב Func

הטיפוס Func הוא טיפוס מורכב המכמס (Encapsulate) delegate.

החתימה של Func מתארת delegate כאשר היא מכילה שני חלקים – החלק הראשון, או החלקים הראשונים, מסמן את הפרמטרים שנשלחים אל הdelegate. החלק השני מסמן את הערך המוחזר מה delegate.

למשל הביטוי Func<int, bool> מתאר delegate שמקבל פרמטר יחיד מסוג int ומחזיר bool.

הביטוי Func<int, int, bool> מתאר delegate שמקבל שני int כפרמטרים ומחזיר bool.

וכן הלאה.

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

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

זה יראה כך:

   1: Func<int, int, int> Multi = (x, y) => x*y;

   2: Console.WriteLine(Multi(3,5));

כאמור – ה int האחרון מסמן את הערך המוחזר.

אם אין ערך מוחזר ניתן להשתמש ב Action במקום ב Func. הם זהים למעט של Action אין ערך מוחזר.

בהקשר של LINQ, השימוש ב Func עוזר לנו להיפטר מה delegate שלנו ובמקום זה לקבל Func כפרמטר באופן הבא:

   1: public static class FilterExtensions

   2: {

   3:   public static IEnumerable<T> Filter<T>

   4:   (this IEnumerable<T> input, Func<T,bool> issue)

   5:   {

   6:       foreach (var item in input)

   7:       {

   8:           if (issue(item))

   9:           {

  10:               yield return item;

  11:           }

  12:       }

  13:   }

  14: }

באופן הזה LINQ מאפשר לנו לשלוח באופן חופשי לחלוטין את הפרמטרים על פיהם אנו רוצים לסנן ואף ליצור סינון על גבי סינון באופן הבא:

   1: var result = employees.Where(e => e.StartsWith("A"))

   2:                       .OrderByDescending(e => e.Length);

   3:  

Linq ו Entityframework

כאשר משתמשים ב Entityframework, שהוא עצמו משתמש בLINQ, נשאלת השאלה איך הם משתמשים ב LINQ על מנת לעבוד מול מסד נתונים כל כך גדול.

אין הרבה הגיון בהבאת ה Data Base כולו לזיכרון על מנת שנוכל לתשאל אותו.

ולכן Entityframework משתמש בדרך מעט שונה.

למעשה Entityframework משתמש בLINQ בצורה כזו שהקומפיילר לא מממש את השאילתא על ידי הבאת הנתונים כולם, אלא מתרגם אותה לשפת שאילתות שתלך אחר כך ל SQL. הדבר מתרחש על ידי שימוש במושג שנקרא Expression במקום ב Func.

Expression עוטפים טיפוסים מסוג Func והקומפיילר יודע לתרגם Func שעטוף בExpression לשפת שאילתות.

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

על מנת להרחיב את ההבנה, נציין ש Entityframework לא משתמש ב IEnumerable<T> כמו בLINQ רגיל, אלא ב IQuetyable<T>. שאמנם יורש מ IEnumerable אלא שבמקום לקבל Func הוא מקבל Expression.

כאשר הקומפיילר רואה Func הוא מתרגם אותו לdelegate ואז ל IL.

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

Entityframework ודומיו יודעים לקחת את הנתונים האלה ולהפוך אותם לשאילתא מול בסיס הנתונים.

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *