Question from Tapuz .Net forum: Generics and Anonymous delegates on List<T> With LINQ!
שאלה:
ב-List<T> Collection יש כל מיני Methods שמקבלות כל מיני פרמטרים ג'נאריים.
List<T>.ConvertAll מקבלת משהו בשם <Converter<T.
List<T>.Exists, List<T>.RemoveAll, List<T>.TrueForAll ו-List<T>.FindXXX מקבלים משהו בשם <Predicate<T.
List<T>.Sort מקבל <IComprar<T.
List<T>.ForEach מקבל <Action<T.
מה זה כל ה-Methods האלו? מה המחלקות האלו? צריך לעשות Inheritance מהן? שמעתי שאפשר לעשות משהו בשם Anonymous Methods? מה הקשר ל-Generics?
קראתי את התיעוד ב-MSDN, אבל הוא מעיק והדוגמאות לא ברורות...
תשובה:
זאת שאלה מצויינת שמראה את ההבדל בין 1.1 #C לבין 2.0 #C לבין 3.5 #C.
בואו נתחיל בליצור מודל בעיה - יש לנו אוסף של בקבוקי וויסקי.
בואו ניצור מחלקה שתייצג את בקבוק וויסקי.
סה"כ מחלקה עם שלושה מאפיינים (גם באנגלית: Properties) אחד שהוא string (שם היצרן), עוד אחד שהוא int (מחיר לבקבוק סטנדרטי) ועוד אחד מסוג System.Color שמייצג את צבע התווית.
בואו ניצור אוסף (גם באנגלית: Collection) שמייצג את האוסף שראינו למעלה.
List<T>.FindAll כדוגמה קלאסית
עכשיו בואו נגיד ואנחנו רוצים להשיג <List<WhiskeyBottle שמכיל את כל בקבוקי הוויסקי של Jhonny Walker.
בדוט נט 1.1 היינו עושים את זה ככה:
והיינו מקבלים אוסף עם כל הבקבוקים של Jhonny Walker.
רק מה, זאת משימה יחסית קלה ופשוטה וכתבנו בשבילה לא מעט שורות קוד, גם מדובר במשימה שכמעט תמיד חוזרת על עצמה.
נביט על List<T>.FindAll...
רואים שה-List<T>.FindAll מקבל <Predicate<T.
ה-Predicate הזה הוא Delegate שמצביע על מתודה עם חתימה מסויימת.
מסתבר שה-Delegate הזה מצביע על מתודה שמקבלת T ומחזירה ערך בוליאני. הערך הבוליאני הזה מייצג "האם האובייקט מסוג T עומד בתנאים של הפרדיקט".
יש לנו מתודה בשם JohnneyWalkerPredicate שמקבלת WhiskeyBottle ומחזירה true אם היצרן הוא JhonnyWalker או false אם לא.
המתודה הזו עומדת בתנאים של ה-delegate הג'נארי שלנו - מקבלת T כלשהו (כאשר T הוא בקבוק WhiskeyBottle) ומחזירה bool.
ולכן נוכל להכניס אותה לתוך <Predicate<T וספציפית לתוך <Predicate<WhiskeyBottle.
נשלח את ה-Predicate שלנו ל-FindAll.
ונראה איך זה נראה בהרצה:
מסתבר שקיבלנו אוסף של כל המחלקות שעונות לתנאי שהגדרנו ב-Predicate הג'נארי שלנו.
חשוב להבין שה-<Predicate<T ששולחים ל-<List<T חייב להיות עם אותו T.
עכשיו ניקח את זה עוד צעד קדימה ונגיד ככה: לא בא לי להתחיל לעבוד עם Delegates ולכתוב את ה-Predicate שלי במתודה נפרדת.
אנחנו רוצים שהפריימוורק תדפוק את הראש בקיר על הדברים האלו ושאנחנו נעשה את המינימום הנדרש.
נפנה למשהו שנקרא Anonymous delegates ו\או Anonymous methods.
הרעיון מאחורי הדברים האנונימיים האלו זה ככה: "אתם, המפתחים, יכולים להכתוב מתודות חדשות בתוך מתודות אחרות ולהמיר אותן ל-delegates, ואני הקומפיילר אשבור את הראש להוציא אותן למתודות נפרדות".
יהפוך ל-
נעבור שורה-שורה.
אמרנו שלתוך List<T>.FindAll צריך לשלוח <Predicate<T.
כלומר לתוך List<WhiskeyBottle>.FindAll נשלח <Predicate<WhiskeyBottle.
אז אנחנו צריכים לשלוח delegate שמחזיר bool ומקבל WhiskeyBottle (לפי ההגדרה של <Predicate<T).
מילת המפתח delegate מאפשרת לנו ליצור במקום מתודה (גם באנגלית: Method) שנכנסת לתוך delegate.
עכשיו השאלה היא מה בדיוק המתודה הבלתי-נראית הזאת עושה...
לכל WhiskeyBottle שנשלח למתודה הזו נחזיר true אם היצרן הוא Jhonny Walker ו-false אם לא.
נראה שזה באמת רץ, יכול להיות הרי שאני משקר לכם.
בואו נפתח את האסמבלי הדוט-נטי שלנו ב-Reflector.
אפשר לראות שהקומפיילר שלנו ייצר מתודה עם שם מוזר משהו (b__0) והיא למעשה מכילה את הקוד שכתבנו בתוך המתודה Main שלנו.
בנוסף, הוא גם ייצר Delegate שהוא <Predicate<WhiskeyBottle.
List<T>.Find ו-List<T>.FindLast
אז ראינו איך להחזיר אוסף של מחלקות שעונות על תנאי שהגדרנו ב-Predicate.
עכשיו נרצה להחזיר רק מחלקה אחת שעונה על התנאי שהגדרנו ב-Predicate.
המתודה List<T>.Find מחזירה את המחלקה הראשונה שעונה על התנאי שהגדרנו,
והמתודה List<T>.FindLast מחזירה את המחלקה האחרונה (בסידור הקיים של ה-List) שעונה על התנאי.
נראה איך הקוד שלנו יראה:
נריץ ונראה מה אנחנו מקבלים:
נזכור את הסידור שהוספנו על פיו את המחלקות לאוסף:
ובאמת שה-Red Label הוא הבקבוק הראשון בסידור הקיים של האוסף שעונה לתנאי שהגדרנו, וה-Blue Label הוא האחרון.
List<T>.FindIndex ו-List<T>.FindLastIndex
בדומה מאוד לזוג המתודות הקודמות, רק שהפעם במקום לקבל את המחלקות עצמן, נקבל את האינדקס שלהן בסידור הנוכחי של האוסף.
ובהרצה:
ואיך נידע שאלו באמת האינדקסים של המחלקות באוסף?
נפתח את חלון Quick Watch (או דרך התפריט Debug --> Windows --> Quick Watch, או דרך קיצור המקלדת Ctrl + Alt + Q).
נזין את הביטוי [bottles[firstJhonnyWalkerIndex ונראה שבאמת הוא מצביע על ה-Red Label:
List<T>.Exists
מקבל <Predicate<T ומחזיר אם התנאי נכון לפחות לאחד מהמחלקות באוסף.
ובהרצה:
וראינו שהיות ויש לנו באוסף בקבוקים שהיצרן שלהן Jhonny Walker קיבלנו true על ה-exists שלו, וקיבלנו false על ה-exists של Glenn Fiddich.
List<T>.TrueForAll
המתודה תחזיר true אם ה-Predicate שישלח אליה יחזיר true לכל המחלקות בה ו-false אם לא.
ובהרצה:
ובאמת נראה שלא כל הבקבוקים שלנו היצרן שלהם הוא Jhonny Walker, אבל לכל הבקבוקים שלנו אכן יש לפחות שם יצרן.
List<T>.RemoveAll
אנחנו כבר מתורגלים - מקבל Predicate, ומסיר מהאוסף כל מחלקה שעומדת בתנאי. (ועל הדרך מחזיר מספר מחלקות שהוצאו מהאוסף)
ובהרצה:
ואחרי ההרצה נראה שבאוסף שלנו נשארו רק הבקבוקי וויסקי שלא יוצרו ע"י Jhonny Walker.
List<T>.ForEach
OK, סיימנו עם המתודות של ה-<Predicate<T.
עכשיו נביט על List<T>.ForEach
אפשר לראות ש-ForEach מקבל <Action<T. נביט על ההגדרה של <Action<T.
מדובר על Delegate ג'נארי של T שמקבל מחלקה מסוג T.
בואו נחליט שאנחנו רוצים לייקר את המחיר של כל הבקבוקים ב-10 ש"ח.
ולאחר הרצה נוכל לראות שהמחיר אכן התייקר ב-10 ש"ח:
כאשר המחירים המקוריים הם:
בצורה זו, נוכל לבצע קוד על כל אחד מהמחלקות באוסף שלנו, בצורה שקולה לחלוטין ללולאת ForEach.
מעניין לשים לב להבדל בין ה-<Predicate<T ל-<Action<T המקומפלים.
אפשר לראות שכל ה-<Predicate<T המקומפלים שלנו מחזירים boolean וה-<Action<T הוא עם ערך החזרה void.
שימו לב שבשום מקום בקוד לא ציינו במפורשות ב-Anonymous Delegates שאנחנו מחזירים bool או void, הקומפיילר הסיק את זה לבד במהלך קומפילציה.
List<T>.ConvertAll
המתודה List<T>.ConvertAll מחזירה אוסף שהוא העתק של האוסף המקורי אחרי המרה מסוג לסוג. הגדרה מפוצצת, אז נתחיל מדוגמה קטנה.
קודם נביט על ההגדרה של List<T>.ConvertAll.
אנחנו רואים ש-ConvertAll מקבל משהו שנקרא <Converter<T, TOutput. בואו נביט על ההגדרה של ה-<Converter<T, TOutput:
אנחנו רואים שמדובר על delegate עם שני פרמטרים ג'נאריים!
לא רק T כמו מקודם, אלא גם TOutput!
אפשר לראות ה-delegate הזה יקבל מופע של מחלקה מסוג TInput, ויחזיר מופע של מחלקה מסוג TOutput.
כלומר, ה-Converter המוזר הזה יקבל מופע של מחלקה מסוג מסויים וימיר אותה למופע של מחלקה מסוג אחר.
בואו נראה דוגמה יותר פשוטה מהבקבוקי וויסקי שלנו, נראה דוגמה של המרת אוסף stringים לאוסף intים.
נרצה עכשיו לקבל <List<int.
נעבור על זה שורה שורה.
נרצה להשתמש ב-ConvertAll כדי לקבל את ה-<List<int הזה.
מה מסתבר? צריך להגיד ל-ConvertAll בצורה של פרמטר ג'נארי לאיזה סוג אנחנו רוצים להמיר כל מופע של המחלקה.
במקרה שלנו אנחנו ממירים string ל-int, ולכן נגיד ל-ConvertAll שאנחנו ממירים ל-int.
עכשיו נרצה להעביר לו את ה-anonymous delegate שאחרי על ההמרה.
ראינו בהגדרה שהוא מקבל מאפיין מסוג TInput, שבמקרה שלנו הוא ה-string שנרצה להמיר.
עכשיו נצטרך להחזיר את ה-input שלנו (שהוא string) אחרי המרה.
לפני ההמרה:
אחרי ההמרה:
נסכם:
המרנו אוסף מסוג <List<TInput לאוסף מסוג של <List<TOutput דרך המרה אחד-אחד של TInput ל-TOutput.
בואו נראה את הקוד שיצרנו ב-Reflector:
גם פה, הקומפיילר הסיק לבד שאנחנו ממירים מ-string ל-int.
לא ציינו בשום מקום בתוך המתודה שאנחנו מחזירים int, אבל הקומפיילר ידע להסיק את זה. (גם מתוך הפרמטר הג'נארי של ConvertAll וגם מתוך ה-anonymous delegate).
עכשיו נרצה לעשות משהו עם הבקבוקי וויסקי שלנו. למשל להמיר בקבוקי וויסקי לפורמט XML.
עד כאן ברור יחסית - נרצה להחזיר מערך של <List<string שייצג את ה-XMLים של כל בקבוק ובקבוק.
אמרנו ל-ConvertAll "תמיר בבקשה ל-string" וכתבנו anonymous method שמקבלת בקבוק ותחזיר string.
נרצה להשתמש ב-XmlSerializer בשביל ההמרה של WhiskeyBottle ל-XML.
דבר ראשון בשביל XmlSerializer נוסיף קונסטרקטור ריק למחלקה שלנו.
לאתחל XmlSerializer לוקח זמן (במיוחד אם משתמשים בקונסטרקטורים מסויימים שלו), אז נרצה לאתחל אותו רק פעם אחת.
מסתבר שבתוך ה-anonymous delegate שלנו יש לנו גישה לפרמטרים שנמצאים מחוץ למתודה.
יש לנו גישה לעבוד עם מחלקות שמוצהרות מחוץ ל-anonymous delegate!
מה שבחיים לא היינו יכולים לעשות עם היינו עובדים עם סתם delegate, כי היינו צריכים לשמור על החתימה שלו שלא יקבל עוד פרמטרים!
וככה יראה הקוד שלנו כעת:
למעוניינים, וזה מחוץ להיקף המאמר, הנה הקוד של ConvertToXml: (קוד שכתבתי בשנייה למטרות הדגמה, לא כותבים ככה קוד)
בואו נראה אם באמת קיבלנו מערך של XMLים שמייצגים את הבקבוקי וויסקי שלנו.
באמת קיבלנו חמש מחרוזות שמייצגות XML. אבל איך נידע שה-XML באמת קשור ל-WhiskeyBottle שלנו?
שמתם לב לזכוכית מגדלת הקטנה שם? נעמוד עליה עם העכבר.
נבחר ב-Xml Visualizer ונראה:
ומלבד ה-LabelColor (היות והוא struct) באמת קיבלנו XML שמייצג את הבקבוקי וויסקי שלנו.
נביט ב-Reflector על הקוד של ה-Anonymous delegate.
אפשר לראות שה-anonymous delegate שלנו לא ייצר הפעם מתודה בצד, אלא ייצר מחלקה שלמה בצד!
למה מחלקה שלמה? כי אנחנו משתמשים ב-XmlSerializer שלנו ואנחנו עדיין צריכה מתודה שניתן להמיר שתתאים ל-<Converter<TInput, TOutput. אז זה הפתרון של הקומפיילר למצב הזה.
וכל זה - קורה בלי התערבות שלנו.
List<T>.Sort
בואו נזכר בכיצד איתחלנו את האוסף שלנו:
כלומר, רד-לייבל הוא הראשון באוסף, בלאק-לייבל הוא שני ובסוף יושב לו ה-Jack Daniels.
בואו נסדר את האוסף הזה לפי מחיר. אבל נבלגן אותה קצת יותר למטרות הדוגמה:
נביט על המתודה List<T>.Sort:
Sort מקבל או <IComprarer<T או<Comparison<T.
נעבור על שתי האפשרויות, ונתחיל מ-<Comparison<T. נביט על ההגדרה שלו:
<Comperison<T מקבל שתי מופעים מסוג T ומחזיר איזה int מסתורי.
כנראה שה-int הזה מסמן את התוצאות של ההשוואה בין שני הפרמטרים.
לפי MSDN:
(http://msdn2.microsoft.com/en-us/library/tfakywbh.aspx)
כלומר, אם הפרמטר הראשון "קטן" מהפרמטר השני נחזיר מספר שלילי.
אם הפרמטר הראשון "גדול" מהפרמטר השני נחזיר מספר חיובי.
ואם הם "שווים" נחזיר 0.
נתחיל לכתוב את הקריאה ל-List<T>.Sort:
עד כאן אנחנו כבר מתורגלים, אז נממש את ה-Sort הזה לפי הכללים שכתובים למעלה באותיות קידוש לבנה.
ובאמת כמו שאמרנו
אם הבקבוק הראשון יותר יקר מהבקבוק השני נחזיר 1
אם הבקבוק השני יותר יקר מהבקבוק הראשון נחזיר1-
אם הם שווים במחירם נחזיר 0
נריץ את זה. לפני הסידור:
אחרי הסידור:
ובאמת סידרנו את האוסף לפי התנאי סידור שהגדרנו.
תחשבו על זה שהגדרנו תנאי נומרי (שמבוסס על השוואה נומרית בין שתי פרמטרים נומריים) אבל ההשוואה יכולה להיות כל השוואה למעשה בין דברים שבכלל לא היינו יכולים להשוות. למשל השוואה בין "אבטיח לאידואלוגיה" נוכל להגיד ש"אבטיח גדול מאידואולוגיה".
אמרנו גם שיש את ה-overload הנוסף של List<T>.Sort שמקבל <IComparer<T.
נביט על הממשק הזה:
הממשק הזה מחייב אותנו ליצור מחלקה עם המתודה Compare שמקבלת שני מופעים מאותו סוג ומחזירה int.
במקום לממש ישירות את <IComprarer<T ניצור מחלקה שיורשת מ-<Comparer<T שהיא מחלקה שיורשת בעצמה את <IComprarer<T. (ירושה מהמחלקה הזו נותנת לנו טיפול במקרי ברירת מחדל וב-nullים).
נירש את המחלקה הזו.
אנחנו חייבים לממש את המתודה Compare שדיברנו עליה.
ובדיוק אותו קוד כמו מקודם.
נשלח את המחלקה הזו כפרמטר ל-List<T>.Sort.
ובאמת נראה שהאוסף שלנו ממויין לפי התנאים שהגדרנו:
ההבדל המהותי הזה בין anonymous delegate לבין מחלקה קונקרטית עם מתודה מפורשת מעלה שאלה מאוד חשובה מבחינת הנדסת תוכנה.
מתי נשתמש ב-Annoynomus delegates ומתי נשתמש במתודות מפורשות?
חשוב לשים לב שבמאמר זה לא דנו בכלל מתי עובדים עם Anonymous delegates מול מתי עובדים עם מתודות מפורשות.
כלל האצבע המינימלי הוא - למנוע שכפול קוד.
כלומר, אם נכתוב פעמיים אם בשתי מקומות שונים את אותה Anonymous Method, אז יש לנו שכפול קוד ואנחנו רוצים להימנע מזה.
למשל ראינו לא מעט:
למשל בשני המתודות הבאות:
לא נרצה שיהיה לנו סתם שכפול קוד. אז נעתיק את הקוד לתוך מתודה שיושבת במקום שמתאים לארכיטקטורה שלנו (מחלקה מיוחדת שמחזיקה את כל ה-Predicateים, מחלקות ייעודיות, ועוד אין-ספור אפשרויות). כרגע ניצור את המתודה בתוך מחלקת ה-WhiskeyBottle:
והקוד שלנו יראה ככה כעת:
זה היה הכלל המינימלי - למנוע שכפול קוד.
הכלל המקסימלי הוא - למנוע ערבוב אחריות וסמכויות בין מחלקות.
למשל, השאלה "האם המתודה שבוחרת את כל בקבוקי ה-Jhonny Walker צריכה לדעת כיצד הבחירה מתבצעת? האם היא צריכה להכיר את המחרוזת "Jhonny Walker" ואת המאפיין Manufacture?".
אם עניתם שלא, המתודה שבוחרת לא צריכה לדעת כיצד הבחירה מתבצעת בפועל (רק להבין את המשמעות שלה), אז לעולם לא נשתמש ב-Anonymous delegates.
אם עניתם שכן, אז נשמור רק על הכלל המינימלי של להימנע משכפול קוד.
איפה LINQ נכנס לעניין?
בדוט נט 3.5 קיבלנו את שפת ה-LINQ שאחד מהיישומים שלה היא LINQ for objects.
במקום הקוד הזה בדוט נט 1.1:
ובמקום הקוד הזה שכרגע למדנו בדוט נט 2.0:
נוכל לרשום בדוט נט 3.5:
LINQ for objects בעניין הזה מחליף לנו את צורת הכתיבה בכל הקשור לפרדיקטים.
נוכל לכתוב סינטקס דמוי SQL עם Intellisense מלא שיאפשר לנו לבצע שאילתות על אוספים של מחלקות מידע בזכרון.
למשל במקום הפרדיקט של Find (שהוא למעשה FindFirst) נוכל לכתוב שאילתא שקולה ב-LINQ.
כמו שב-SQL יש לנו Top 1 כדי להגיד שאנחנו רוצים רק את הרשומה האחרונה, נוכל להגיד ב-LINQ
הקוד שעבדנו עליו זמין להורדה כאן - http://www.JustinAngel.Net/files/Example-ExploringGeneircList.zip.