DCSIMG
Question from Tapuz .Net forum: Generics and Anonymous delegates on List<T> With LINQ! - Justin myJustin = new Justin( Expriences.Current );

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.

בואו נתחיל בליצור מודל בעיה - יש לנו אוסף של בקבוקי וויסקי.

 

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

    public class WhiskeyBottle

    {

        public WhiskeyBottle(Color labelColor, string manufacture, int pricePer750MLBottle)

        {

            this.LabelColor = labelColor;

            this.Manufacture = manufacture;

            this.PricePer750MLBottle = pricePer750MLBottle;

        }

 

        private Color _labelColor = Color.Empty;

        public Color LabelColor

        {

            get { return _labelColor; }

            set { _labelColor = value; }

        }

 

        private string _manufacture;

        public string Manufacture

        {

            get { return _manufacture; }

            set { _manufacture = value; }

        }

 

        private int _pricePer750MLBottle;

        public int PricePer750MLBottle

        {

            get { return _pricePer750MLBottle; }

            set { _pricePer750MLBottle = value; }

        }

 

        public override string ToString()

        {

            return string.Format("{0} {1} costs {2}", Manufacture, LabelColor.ToString(), PricePer750MLBottle);

        }

    }

 סה"כ מחלקה עם שלושה מאפיינים (גם באנגלית: Properties) אחד שהוא string (שם היצרן), עוד אחד שהוא int (מחיר לבקבוק סטנדרטי) ועוד אחד מסוג System.Color שמייצג את צבע התווית.

בואו ניצור אוסף (גם באנגלית: Collection) שמייצג את האוסף שראינו למעלה.

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

 

List<T>.FindAll כדוגמה קלאסית

עכשיו בואו נגיד ואנחנו רוצים להשיג <List<WhiskeyBottle שמכיל את כל בקבוקי הוויסקי של Jhonny Walker.

בדוט נט 1.1 היינו עושים את זה ככה:

            List<WhiskeyBottle> JhonnyWalkerBottls = new List<WhiskeyBottle>();

            foreach (WhiskeyBottle bottleToCheckIfJhonnyWalkerAndAddToResult in bottles)

            {

                if (bottleToCheckIfJhonnyWalkerAndAddToResult.Manufacture == "Jhonny Walker")

                    JhonnyWalkerBottls.Add(bottleToCheckIfJhonnyWalkerAndAddToResult);

            }

והיינו מקבלים אוסף עם כל הבקבוקים של Jhonny Walker.

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

נביט על List<T>.FindAll...

רואים שה-List<T>.FindAll מקבל <Predicate<T.

ה-Predicate הזה הוא Delegate שמצביע על מתודה עם חתימה מסויימת.

 מסתבר שה-Delegate הזה מצביע על מתודה שמקבלת T ומחזירה ערך בוליאני. הערך הבוליאני הזה מייצג "האם האובייקט מסוג T עומד בתנאים של הפרדיקט".

            Predicate<WhiskeyBottle> pred = JhonnyWalkerPredicate;

          ....

 

        private static bool JhonnyWalkerPredicate(WhiskeyBottle bottle)

        {

            return (bottle.Manufacture == "Jhonny Walker");

        }

יש לנו מתודה בשם JohnneyWalkerPredicate שמקבלת WhiskeyBottle ומחזירה true אם היצרן הוא JhonnyWalker או false אם לא.

המתודה הזו עומדת בתנאים של ה-delegate הג'נארי שלנו - מקבלת T כלשהו (כאשר T הוא בקבוק WhiskeyBottle) ומחזירה bool.
ולכן נוכל להכניס אותה לתוך <Predicate<T וספציפית לתוך <Predicate<WhiskeyBottle.

נשלח את ה-Predicate שלנו ל-FindAll.

            Predicate<WhiskeyBottle> pred = JhonnyWalkerPredicate;

            List<WhiskeyBottle> JhonnyWalkerBottles = bottles.FindAll(pred);

ונראה איך זה נראה בהרצה:

מסתבר שקיבלנו אוסף של כל המחלקות שעונות לתנאי שהגדרנו ב-Predicate הג'נארי שלנו.

חשוב להבין שה-<Predicate<T ששולחים ל-<List<T חייב להיות עם אותו T.

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

נפנה למשהו שנקרא Anonymous delegates ו\או Anonymous methods.
הרעיון מאחורי הדברים האנונימיים האלו זה ככה: "אתם, המפתחים, יכולים להכתוב מתודות חדשות בתוך מתודות אחרות ולהמיר אותן ל-delegates, ואני הקומפיילר אשבור את הראש להוציא אותן למתודות נפרדות".

            Predicate<WhiskeyBottle> pred = JhonnyWalkerPredicate;

            List<WhiskeyBottle> JhonnyWalkerBottles = bottles.FindAll(pred);

 

 

        private static bool JhonnyWalkerPredicate(WhiskeyBottle bottle)

        {

            return (bottle.Manufacture == "Jhonny Walker");

        }

יהפוך ל-

            List<WhiskeyBottle> JhonnyWalkerBottles =

                                    bottles.FindAll(delegate(WhiskeyBottle bottle)

                                                  {

                                                      return (bottle.Manufacture == "Jhonny Walker");

                                                  });

נעבור שורה-שורה.

אמרנו שלתוך List<T>.FindAll צריך לשלוח <Predicate<T.
כלומר לתוך List<WhiskeyBottle>.FindAll נשלח <Predicate<WhiskeyBottle.
אז אנחנו צריכים לשלוח delegate שמחזיר bool ומקבל WhiskeyBottle (לפי ההגדרה של <Predicate<T).

מילת המפתח delegate מאפשרת לנו ליצור במקום מתודה (גם באנגלית: Method) שנכנסת לתוך delegate.

עכשיו השאלה היא מה בדיוק המתודה הבלתי-נראית הזאת עושה...

            List<WhiskeyBottle> JhonnyWalkerBottles =

                                    bottles.FindAll(delegate(WhiskeyBottle bottle)

                                                        {

                                       return (bottle.Manufacture == "Jhonny Walker");

                                                        });

 
לכל 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) שעונה על התנאי.

נראה איך הקוד שלנו יראה:

            WhiskeyBottle firstJhonnyWalker = bottles.Find(delegate(WhiskeyBottle bottle)

                                                        {

                                               return (bottle.Manufacture == "Jhonny Walker");

                                                        });

 

 

            WhiskeyBottle lastJhonnyWalker = bottles.FindLast(delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            });

נריץ ונראה מה אנחנו מקבלים:

נזכור את הסידור שהוספנו על פיו את המחלקות לאוסף:

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

ובאמת שה-Red Label הוא הבקבוק הראשון בסידור הקיים של האוסף שעונה לתנאי שהגדרנו, וה-Blue Label הוא האחרון.

List<T>.FindIndex ו-List<T>.FindLastIndex

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

            int firstJhonnyWalkerInbdex = bottles.FindIndex(delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            });

 

 

            int lastJhonnyWalkerIndex = bottles.FindLastIndex(delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            });

ובהרצה:

 

 

ואיך נידע שאלו באמת האינדקסים של המחלקות באוסף?
נפתח את חלון Quick Watch (או דרך התפריט Debug --> Windows --> Quick Watch, או דרך קיצור המקלדת Ctrl + Alt + Q).

נזין את הביטוי [bottles[firstJhonnyWalkerIndex ונראה שבאמת הוא מצביע על ה-Red Label:

List<T>.Exists

מקבל <Predicate<T ומחזיר אם התנאי נכון לפחות לאחד מהמחלקות באוסף.

            bool weHaveJhonnyWalker = bottles.Exists(delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            });

 

            bool weHaveGleenFiddich = bottles.Exists(delegate(WhiskeyBottle bottle)

                                {

                                    return (bottle.Manufacture == "Glenn Fiddich");

                                });

ובהרצה:

וראינו שהיות ויש לנו באוסף בקבוקים שהיצרן שלהן Jhonny Walker קיבלנו true על ה-exists שלו, וקיבלנו false על ה-exists של Glenn Fiddich.

List<T>.TrueForAll

המתודה תחזיר true אם ה-Predicate שישלח אליה יחזיר true לכל המחלקות בה ו-false אם לא.

            bool allBottlesAreJhonnyWalker = bottles.TrueForAll(delegate(WhiskeyBottle bottle)

                                {

                                    return (bottle.Manufacture == "Jhonny Walker");

                                });

 

            bool allBottlesHaveManufacture =  bottles.TrueForAll(delegate(WhiskeyBottle bottle)

                                            {

                                                return (!string.IsNullOrEmpty(bottle.Manufacture));

                                            });

ובהרצה:

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

List<T>.RemoveAll

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

            int bottlesRemoved = bottles.RemoveAll(delegate(WhiskeyBottle bottle)

                                {

                                    return (bottle.Manufacture == "Jhonny Walker");

                                });

ובהרצה:

ואחרי ההרצה נראה שבאוסף שלנו נשארו רק הבקבוקי וויסקי שלא יוצרו ע"י Jhonny Walker.

List<T>.ForEach

OK, סיימנו עם המתודות של ה-<Predicate<T.

עכשיו נביט על List<T>.ForEach

אפשר לראות ש-ForEach מקבל <Action<T. נביט על ההגדרה של <Action<T.

מדובר על Delegate ג'נארי של T שמקבל מחלקה מסוג T.

בואו נחליט שאנחנו רוצים לייקר את המחיר של כל הבקבוקים ב-10 ש"ח.

            bottles.ForEach(delegate(WhiskeyBottle bottle)

                                {

                                    bottle.PricePer750MLBottle += 10;

                                });

ולאחר הרצה נוכל לראות שהמחיר אכן התייקר ב-10 ש"ח:

כאשר המחירים המקוריים הם:

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

בצורה זו, נוכל לבצע קוד על כל אחד מהמחלקות באוסף שלנו, בצורה שקולה לחלוטין ללולאת 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<string> strings = new List<string>();

            strings.Add("1");

            strings.Add("2");

            strings.Add("3");

נרצה עכשיו לקבל <List<int.

            List<int> ints = strings.ConvertAll<int>(delegate(string input)

                                                        {

                                                            return Convert.ToInt32(input);

                                                        });

נעבור על זה שורה שורה.

 List<int> ints =

נרצה להשתמש ב-ConvertAll כדי לקבל את ה-<List<int הזה.

List<int> ints = strings.ConvertAll

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

List<int> ints = strings.ConvertAll<int>

עכשיו נרצה להעביר לו את ה-anonymous delegate שאחרי על ההמרה.
ראינו בהגדרה שהוא מקבל מאפיין מסוג TInput, שבמקרה שלנו הוא ה-string שנרצה להמיר.

            List<int> ints = strings.ConvertAll<int>(delegate(string input)

                                                        {

 

                                                        });

עכשיו נצטרך להחזיר את ה-input שלנו (שהוא string) אחרי המרה.

            List<int> ints = strings.ConvertAll<int>(delegate(string input)

                                                        {

                                                            return Convert.ToInt32(input);

                                                        });

לפני ההמרה:

אחרי ההמרה:

נסכם:

המרנו אוסף מסוג <List<TInput לאוסף מסוג של <List<TOutput דרך המרה אחד-אחד של TInput ל-TOutput.

בואו נראה את הקוד שיצרנו ב-Reflector:

גם פה, הקומפיילר הסיק לבד שאנחנו ממירים מ-string ל-int.
לא ציינו בשום מקום בתוך המתודה שאנחנו מחזירים int, אבל הקומפיילר ידע להסיק את זה. (גם מתוך הפרמטר הג'נארי של ConvertAll וגם מתוך ה-anonymous delegate).

עכשיו נרצה לעשות משהו עם הבקבוקי וויסקי שלנו. למשל להמיר בקבוקי וויסקי לפורמט XML.

            List<string> bottlesXml = bottles.ConvertAll<string>(delegate(WhiskeyBottle bottle)

                                                                    {

 

                                                                    });

עד כאן ברור יחסית - נרצה להחזיר מערך של <List<string שייצג את ה-XMLים של כל בקבוק ובקבוק.
אמרנו ל-ConvertAll "תמיר בבקשה ל-string" וכתבנו anonymous method שמקבלת בקבוק ותחזיר string.

נרצה להשתמש ב-XmlSerializer בשביל ההמרה של WhiskeyBottle ל-XML.
דבר ראשון בשביל XmlSerializer נוסיף קונסטרקטור ריק למחלקה שלנו.

    public class WhiskeyBottle

    {

        public WhiskeyBottle()

        {

        }

 

       ...

    }


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

            XmlSerializer bottleSerializer = new XmlSerializer(typeof(WhiskeyBottle));

            List<string> bottlesXml = bottles.ConvertAll<string>(delegate(WhiskeyBottle bottle)

                                                                    {

 

                                                                    });

מסתבר שבתוך ה-anonymous delegate שלנו יש לנו גישה לפרמטרים שנמצאים מחוץ למתודה.

יש לנו גישה לעבוד עם מחלקות שמוצהרות מחוץ ל-anonymous delegate!
מה שבחיים לא היינו יכולים לעשות עם היינו עובדים עם סתם delegate, כי היינו צריכים לשמור על החתימה שלו שלא יקבל עוד פרמטרים!

וככה יראה הקוד שלנו כעת:

            XmlSerializer bottleSerializer = new XmlSerializer(typeof(WhiskeyBottle));

            List<string> bottlesXml = bottles.ConvertAll<string>(

                                        delegate(WhiskeyBottle bottle)

                                        {

                                            return ConvertToXml(bottle, bottleSerializer);

                                        });

למעוניינים, וזה מחוץ להיקף המאמר, הנה הקוד של ConvertToXml: (קוד שכתבתי בשנייה למטרות הדגמה, לא כותבים ככה קוד)

        private static string ConvertToXml(object bottle, XmlSerializer serializer)

        {

            using (MemoryStream memoryStream = new MemoryStream())

            {

                XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);

 

                serializer.Serialize(memoryStream, bottle);

 

                return UTF8ByteArrayToString(memoryStream.ToArray());

            }

        }

 

        private static string UTF8ByteArrayToString(Byte[] characters)

        {

            UTF8Encoding encoding = new UTF8Encoding();

            String constructedString = encoding.GetString(characters);

            return (constructedString);

        }

בואו נראה אם באמת קיבלנו מערך של XMLים שמייצגים את הבקבוקי וויסקי שלנו.

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

נבחר ב-Xml Visualizer ונראה:

ומלבד ה-LabelColor (היות והוא struct) באמת קיבלנו XML שמייצג את הבקבוקי וויסקי שלנו.

נביט ב-Reflector על הקוד של ה-Anonymous delegate.

אפשר לראות שה-anonymous delegate שלנו לא ייצר הפעם מתודה בצד, אלא ייצר מחלקה שלמה בצד!

למה מחלקה שלמה? כי אנחנו משתמשים ב-XmlSerializer שלנו ואנחנו עדיין צריכה מתודה שניתן להמיר שתתאים ל-<Converter<TInput, TOutput. אז זה הפתרון של הקומפיילר למצב הזה.

וכל זה - קורה בלי התערבות שלנו.

List<T>.Sort

בואו נזכר בכיצד איתחלנו את האוסף שלנו:

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

כלומר, רד-לייבל הוא הראשון באוסף, בלאק-לייבל הוא שני ובסוף יושב לו ה-Jack Daniels.

בואו נסדר את האוסף הזה לפי מחיר. אבל נבלגן אותה קצת יותר למטרות הדוגמה:

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

נביט על המתודה 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:

            bottles.Sort(delegate(WhiskeyBottle x, WhiskeyBottle y)

                            {

                                // return SomeInt;

                            });

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

            bottles.Sort(delegate(WhiskeyBottle x, WhiskeyBottle y)

                            {

                                if (x.PricePer750MLBottle > y.PricePer750MLBottle)

                                    return 1;

                                else if (y.PricePer750MLBottle > x.PricePer750MLBottle)

                                    return -1;

                                else

                                    return 0;

                            });

ובאמת כמו שאמרנו

אם הבקבוק הראשון יותר יקר מהבקבוק השני נחזיר 1

אם הבקבוק השני יותר יקר מהבקבוק הראשון נחזיר1-

אם הם שווים במחירם נחזיר 0

נריץ את זה. לפני הסידור:

אחרי הסידור:

ובאמת סידרנו את האוסף לפי התנאי סידור שהגדרנו.

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

אמרנו גם שיש את ה-overload הנוסף של List<T>.Sort שמקבל <IComparer<T.

נביט על הממשק הזה:

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

במקום לממש ישירות את <IComprarer<T ניצור מחלקה שיורשת מ-<Comparer<T שהיא מחלקה שיורשת בעצמה את <IComprarer<T. (ירושה מהמחלקה הזו נותנת לנו טיפול במקרי ברירת מחדל וב-nullים).

נירש את המחלקה הזו.

        public class WhiskeyBottlesPriceSorter : Comparer<WhiskeyBottle>

        {

        }

אנחנו חייבים לממש את המתודה Compare שדיברנו עליה.

        public class WhiskeyBottlesPriceSorter : Comparer<WhiskeyBottle>

        {

            public override int Compare(WhiskeyBottle x, WhiskeyBottle y)

            {

            }

        }

ובדיוק אותו קוד כמו מקודם.

        public class WhiskeyBottlesPriceSorter : Comparer<WhiskeyBottle>

        {

            public override int Compare(WhiskeyBottle x, WhiskeyBottle y)

            {

                if (x.PricePer750MLBottle > y.PricePer750MLBottle)

                    return 1;

                else if (y.PricePer750MLBottle > x.PricePer750MLBottle)

                    return -1;

                else

                    return 0;

            }

        }

נשלח את המחלקה הזו כפרמטר ל-List<T>.Sort.

            bottles.Sort(new WhiskeyBottlesPriceSorter());

ובאמת נראה שהאוסף שלנו ממויין לפי התנאים שהגדרנו:

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

מתי נשתמש ב-Annoynomus delegates ומתי נשתמש במתודות מפורשות?

חשוב לשים לב שבמאמר זה לא דנו בכלל מתי עובדים עם Anonymous delegates מול מתי עובדים עם מתודות מפורשות.

כלל האצבע המינימלי הוא - למנוע שכפול קוד.

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

                          delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            }

למשל בשני המתודות הבאות:

            List<WhiskeyBottle> JhonnyWalkerBottles =

                                    bottles.FindAll(delegate(WhiskeyBottle bottle)

                                                        {

                                                            return (bottle.Manufacture == "Jhonny Walker");

                                                        });

 

            WhiskeyBottle firstJhonnyWalker = bottles.Find(delegate(WhiskeyBottle bottle)

                                                        {

                                                            return (bottle.Manufacture == "Jhonny Walker");

                                                        });

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

    public class WhiskeyBottle

    {

        public static bool JhonnyWalkerPredicate(WhiskeyBottle bottle)

        {

            return (bottle.Manufacture == "Jhonny Walker");

        }

 

        ....

    }

והקוד שלנו יראה ככה כעת:

            WhiskeyBottle JhonnyWalkerBottles = bottles.FindAll(WhiskeyBottle.JhonnyWalkerPredicate);

            WhiskeyBottle firstJhonnyWalker = bottles.Find(WhiskeyBottle.JhonnyWalkerPredicate);

זה היה הכלל המינימלי - למנוע שכפול קוד.

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

 למשל, השאלה "האם המתודה שבוחרת את כל בקבוקי ה-Jhonny Walker צריכה לדעת כיצד הבחירה מתבצעת? האם היא צריכה להכיר את המחרוזת "Jhonny Walker" ואת המאפיין Manufacture?".

אם עניתם שלא, המתודה שבוחרת לא צריכה לדעת כיצד הבחירה מתבצעת בפועל (רק להבין את המשמעות שלה), אז לעולם לא נשתמש ב-Anonymous delegates.

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

איפה LINQ נכנס לעניין?

בדוט נט 3.5 קיבלנו את שפת ה-LINQ  שאחד מהיישומים שלה היא LINQ for objects.

במקום הקוד הזה בדוט נט 1.1:

            List<WhiskeyBottle> JhonnyWalkerBottls = new List<WhiskeyBottle>();

            foreach (WhiskeyBottle bottleToCheckIfJhonnyWalkerAndAddToResult in bottles)

            {

                if (bottleToCheckIfJhonnyWalkerAndAddToResult.Manufacture == "Jhonny Walker")

                    JhonnyWalkerBottls.Add(bottleToCheckIfJhonnyWalkerAndAddToResult);

            }

 ובמקום הקוד הזה שכרגע למדנו בדוט נט 2.0: 

            List<WhiskeyBottle> JhonnyWalkerBottles =

                                    bottles.FindAll(delegate(WhiskeyBottle bottle)

                                                        {

                                           return (bottle.Manufacture == "Jhonny Walker");

                                                        });

נוכל לרשום בדוט נט 3.5:

            IEnumerable<WhiskeyBottle> JhonnyWalkerBottles =

                    from bottle in bottles

                    where bottle.Manufacture == "Jhonny Walker"

                    select bottle;

LINQ for objects בעניין הזה מחליף לנו את צורת הכתיבה בכל הקשור לפרדיקטים.
נוכל לכתוב סינטקס דמוי SQL עם Intellisense מלא שיאפשר לנו לבצע שאילתות על אוספים של מחלקות מידע בזכרון.

למשל במקום הפרדיקט של Find (שהוא למעשה FindFirst) נוכל לכתוב שאילתא שקולה ב-LINQ.

            WhiskeyBottle firstJhonnyWalker = bottles.Find(delegate(WhiskeyBottle bottle)

                                                        {

                                               return (bottle.Manufacture == "Jhonny Walker");

                                                        });

 

כמו שב-SQL יש לנו Top 1 כדי להגיד שאנחנו רוצים רק את הרשומה האחרונה, נוכל להגיד ב-LINQ

            var JhonnyWalkerBottles =

                    (from bottle in bottles

                    where bottle.Manufacture == "Jhonny Walker"

                    select bottle).Take(1);

הקוד שעבדנו עליו זמין להורדה כאן - http://www.JustinAngel.Net/files/Example-ExploringGeneircList.zip.

Published Monday, June 18, 2007 10:06 AM by Justin-Josef Angel [MVP]

Comments

No Comments