Quick guide to LINQ (Language Integrated Query)

5 באוקטובר 2010

8 תגובות




המדריך המהיר ל – LINQ.


מטרת הפוסט:


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

 

 

תוכן עניניים




  • מה זה לינק ומה נותן לנו השימוש בו.


  • לפני LINQ.



    • var keyword


    • Anonymous Types


    • Extension Methods


    • Lambda Expressions


  • כתיבת קוד שמדמה LINQ


  • תחביר LINQ



    • Extension Methods


    • Syntactic sugar


  • PLINQ

 


מה זה LINQ ומה נותן לנו השימוש בו.


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

 



IEnumerable<int> arr = Enumerable.Range(1, 100).ToArray();


List<int> list = new List<int>();


 


foreach (int item in arr)


{


    if (item % 2 == 0)


    {


        list.Add(item);


    }


}



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



List<int> list = arr.Where(item => item % 2 == 0).ToList();


 

 


מאוחר יותר נבין את המשמעות של כל תג ופסיק מהמשפט, אבל גם בלי להבין את LINQ אפשר לראות שאיכשהו אנחנו מפעילים שאילתת חיתוך (Where) על המערך וכותבים שאנחנו רוצים רק את המספרים שמתחלקים בשניים item % 2 == 0.

לפני LINQ


var keyword


אפשר להשתמש ב – var במקום לכתוב את ה – type המלא, זה יכול לחסוך כתיבה לדוגמא:



var list = new List<string>();




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



var list = null;




או קוד כזה



static void f(var num)


 



מכיוון שהקומפיילר לא יודע באיזה טיפוס מדובר.
לכאורה זה נראה כמיותר אבל למעשה צריך את ה – var לשימוש ה – Anonymous Types כפי שנראה בהמשך ובנוסף כשאתם מגדירים טיפוס מורכב (כמו Dictionary עם כל מיני טיפוסים) ה – var יכול לפשט את קריאות הקוד.

Anonymous Types



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



var pi = new


{


    Name = "Shlomo",


    Age = 25


};


 


Console.WriteLine("Name: {0}, Age: {1}", pi.Name, pi.Age);




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


למעשה הסיבה שהמציאו את var היא מכיוון שבלעדיה אי אפשר היה להגדיר ולהשתמש ב – Anonymous Types, כמובן שאפשר היה להשתמש ב – object (מה שאנחנו צריכים לעשות במידה ונרצה לשלוח את המשתנה למתודה אחרת) אבל במקרה כזה הגישה למאפיינים היא רק בעזרת reflection שזה לא יעיל כמובן.

 

בנוסף אפשר לכתוב קוד כזה:

 



class MyClass


{


    public int Age { get; set; }


    public string Name { get; set; }


}


 


class YourClass


{


    public float Salary { get; set; }


}


 



MyClass mc1 = new MyClass();


YourClass yc1 = new YourClass();


 


var pi1 = new


{


    mc1.Age,


    mc1.Name,


    yc1.Salary


};


 



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

לכאורה כל הסיפור הזה נראה מיותר – בהמשך כשנלמד LINQ נראה שלפעמים אין ברירה וחייבים להשתמש בתחביר הזה.

 


Extension Methods



נניח שיש מחלקה שלא אנחנו כתבנו (כמו string) ואנחנו רוצים להרחיב אותה (כלומר להוסיף לה פונקציות) נוכל כמובן לרשת ממנה (במקרה של string אי אפשר – זוהי דוגמא) אבל במקרה הזה בכל מקום באפליקציה שלנו נצטרך להשתמש בטיפוס שלנו וכמובן מה קורה אם באמת רוצים להוסיף ל – string שכאמור אי אפשר לרשת ממנה, בשביל זה יש את ה – Extension Methods, לדוגמא:

 



static class StringExtension


{


    public static int ToInt(this string s)


    {


        return int.Parse(s);


    }


}




וכעת נוכל לכתוב

int num = Console.ReadLine().ToInt();


 



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

 

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



  • להגדיר מחלקה סטטית.


  • להגדיר מתודה סטטית שהפרמטר הראשון שלה מוגדר עם this וכל המופעים של הטיפוס שלו (הפרמטר הראשון) יקבלו את המתודה החדשה.


  • ה – namespace שבו הוגדרה המתודה החדשה צריכה להיות מוגדרת בקובץ שבו רוצים להשתמש במתודה החדשה, כלומר אם המחלקה הסטטית תוגדר ב – namespace בשם a ובקובץ אחר באפליקציה לא יהיה using ל – a המתודה החדשה לא תופיע, כדי לגרום לכך שלא יצטרכו באמת להוסיף using שונים אפשר להגדיר namespace בשם MyExtension ותחתיו להגדיר את כל המחלקות החדשות, אופציה נוספת היא לתת את ה – namespace של המערכת למשל – אם מדובר בתוספות ל – string לעטוף את המחלקה ב – namespace של system וכן הלאה.

 



Lambda Expressions



בגרסאות הראשונות של השפה במידה והשתמשנו  ב – delegate היינו צריכים לכתוב מתודה מיוחדת לשליחה, לדוגמא

 



static void Print(int number)


{


    Console.WriteLine(number);


}


 



Action<int> action = new Action<int>(Print);


action(10);


 



(כמובן שלא היה קיים generic אבל לצורך המחשת נושא ה – delegate נניח שהיה קיים.)
במידה ורצינו להשתמש ב – delegate של Action היינו צריכים להגדיר מתודה שעומדת בחתימה ולשלוח אותה כפרמטר ל – ctor של ה – delegate.

 

בגרסה 2.0 של השפה קבלנו את המושג של anonymous methods ויכולנו לכתוב קוד כזה.

 



Action<int> action = delegate(int num1)


{


    Console.WriteLine(num1);


};


 



action(10);


 



הסיבה המרכזית לתמיכה ב – anonymous methods היא היכולת להשתמש במשתנים לוקליים של הפונקציה שקוראת ל – delegate לדוגמא:

 



int userNum = int.Parse(Console.ReadLine());


Action<int> action = delegate(int num1)


{


    if (userNum == 10)


        num1++;


 


    Console.WriteLine(num1);


};


 


action(10);


 



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



החל מהגרסאות המתקדמות של השפה ניתן לכתוב Lambda Expressions שזהו תחביר אחר ל – anonymous methods (אחרי קומפילציה קיים רק anonymous methods) לדוגמא:

 



Action<int> action1 = num1 => Console.WriteLine(num1);


 


Action<int> action2 = num1 =>


{


    if (userNum == 10)


        num1++;


 


    Console.WriteLine(num1);


};


 



נפרק את השורה.

 





  1. Action<int> action1 =






  2. num1 =>







  3. Console.WriteLine(num1);


 



השורה הראשונה זה כמובן המופע של ה – delegate,

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

השורה השלישית מכילה את הקוד עצמו, במידה ויש רק שורה אחת לא צריך סוגריים מסולסלות ואם ה – delegate אמור להחזיר ערך לא צריך לכתוב את המילה return, למשל:

 



Func<int, int> mul = num1 => num1 * 10;


int res = mul(50);


 



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

 

 

כתיבת קוד שמדמה LINQ


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

 



static List<int> Filter(List<int> source, Predicate<int> prediacte)


{


    var newList = new List<int>();


 


    foreach (var item in source)


    {


        if (prediacte(item))


            newList.Add(item);


    }


 


    return newList;


}


 


והשימוש בה יהיה כך:

 



List<int> list = Enumerable.Range(0, 50).ToList();


var evenList = Filter(list, x => x % 2 == 0);


 


כמו שאפשר לראות השתמשנו במתודה ושלחנו אליה את האובייקט המקורי ומימוש ל Lambda Expressions ל – Prediacte (אמורה לקבל פונקציה שתקבל T ותחזיר bool)

אם נרצה להיות יותר חכמים נגדיר את Filter כ – Extension Methods לכל List<int ובמקרה הזה נכתוב כך:


static List<int> Filter(this List<int> source, Predicate<int> prediacte)


{


    //….


}


 



var evenList = list.Filter(x => x % 2 == 0);


 

 


וזה בעצם מה שקרה – הוגדרו מספר רב של Extension Methods לאובייקט:


IEnumerable<T>


 


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


if (list.Any(x => x == 10)) { }



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


var evenIdsAsString = list.Where(x => x % 2 == 0)


                          .Select<int, string>(x => x.ToString());


 


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

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

 

 

תחביר LINQ


Extension Methods



את Extension Methods ראינו כבר בחלק הקודם, נראה עוד דוגמה אחת יחסית מורכבת, נניח שאנחנו רוצים לחפש את כל התיקיות שיש להם מעל 10 קבצים ולפחות קובץ אחד גדול יותר מ – 20 KB, בנוסף נרמה לייצר מחלקה משלנו ולא להחזיר אובייקט של DirectoryInfo.

 

נגדיר מחלקה שנראת כך:

class MyDirectoryInfo


{


    public int FileNumbers { get; set; }


    public string DirectoryName { get; set; }


}


 



var x1 = Directory.GetDirectories("path", "", SearchOption.AllDirectories)


            .Select(x => new DirectoryInfo(x))


            .Where(x =>


                {


                    var files = x.GetFiles();


                    return files.Length >= 10 && files.Any(fi => fi.Length > 10);


                })


            .Select(x => new MyDirectoryInfo()


            {


                DirectoryName = x.Name,


                FileNumbers = x.GetFiles().Length


            });


 



במידה ולא היה לנו מחלקה שנקראת MyDirectoryInfo יכולנו להחזיר Anonymous Types.

 


 Syntactic sugar



כדי להקל על הכתיבה (לפעמים) אפשר לכתוב (חלק) משאילתות LINQ בתחביר אחר. נתחיל עם משהו פשוט יחסית, מציאת כל העובדים שהמשכורת שלהם קטנה מ – 2000

 



class Employee


{


    public string Name { get; set; }


    public double Salary { get; set; }


}


 



בתחביר הקודם היינו כותבים קוד כזה:

 



List<Employee> listEmp = GetList();


var lowSalary = listEmp.Where(x => x.Salary < 2000);


 



בתחביר החדש ניתן לכתוב כך

var lowSalary = from n in listEmp


                where n.Salary < 2000


                select n;


 



את השאילתא המורכבת (של הקבצים) ניתן לתרגם לזה

 



var directories = from n in Directory.GetDirectories("path", "", SearchOption.AllDirectories)


                  let di = new DirectoryInfo(n)


                  let files = di.GetFiles()


                  where files.Length >= 10 && files.Any(x => x.Length > 10)


                  select new MyDirectoryInfo()


                  {


                      DirectoryName = di.Name,


                      FileNumbers = files.Length


                  };


 

 

 

PLINQ


PLINQ הוא parallel linq (רק מגרסה 4.0 ומעלה) מאפשר לנו בקלות לבצע את שאילתות ה – LINQ בצורה פרללית (כלומר כמה threads שיבצעו את העבודה) אני לא ארחיב בנושא מכיוון שזה נושא בפני עצמו, אני רק אדגים דוגמא קטנה

 



List<int> list = Enumerable.Range(1, 50000000).ToList();


 


for (int i = 0; i < 4; i++)


{


    Stopwatch sw = Stopwatch.StartNew();


    var count = list.Count(x => IsPrime(x));


    sw.Stop();


    Console.WriteLine(sw.ElapsedMilliseconds);


 


    sw = Stopwatch.StartNew();


    count = list.AsParallel().Count(x => IsPrime(x));


    sw.Stop();


    Console.WriteLine(sw.ElapsedMilliseconds);


 


    Console.WriteLine("===========");


}


 



static bool IsPrime(int num)


{


    for (int i = num – 1; i > 1; i++)


    {


        if (num % i == 0)


            return false;


    }


 


    return true;


}


 


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

 

הדוגמא הראשונה משתמשת ב – LINQ רגיל לעומת הדוגנא השנייה שמתשמש ב – PLINQ בעזרת קריאה ל – AsParallel.

 

הקוד הפרללי רץ במיהירות כפולה (אצלי במחשב) מהקוד הרגיל.

 

 

סיכום


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

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

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

8 תגובות

  1. ShlomyBsh6 באוקטובר 2010 ב 9:39

    יישר כוח !

    הגב
  2. Gooloosh12 באוקטובר 2010 ב 14:20

    כל הכבוד!
    הסבר מעולה..
    ועוד בעברית 🙂

    הגב
  3. SHIR TAL10 באוקטובר 2011 ב 14:17

    ההסבר מעולה, בהיר וברור!
    מומלץ…..

    הגב
  4. גיא קרויטורו25 ביולי 2012 ב 15:00

    static bool IsPrime(int num)

    {

    for (int i = 1; i > num -1 ; i++)

    {

    if (num % i == 0)

    return false;

    }

    return true;

    }

    הגב
  5. djhenrya8 בדצמבר 2012 ב 4:08

    שגיאה בשיגרה isprime:

    private static bool IsPrime(int num)
    {
    for (int i = num – 1; i > 1; i–)
    {
    if (num%i == 0)
    return false;
    }

    return true;
    }

    הגב
  6. חיה מושקא6 במאי 2013 ב 19:11

    פוסט מעולה!

    למדתי מילת מפתח שלא הכרתי – let, חזק!
    וגם הצורה בה אתה מאתחל מערך ()Enumerable.Range – מקסים!

    רק הערה קטנה כבוד הרב,

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

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

    לסיכום:
    סופרים = ()count
    סוכמים = ()sum

    חיה מושקא (עוד אחת שלא למדה ליבה 🙂 )

    הגב
  7. יעקב4 ביוני 2013 ב 10:17

    מעולה, הסבר מקיף תודה.

    הגב
  8. יעקב4 ביוני 2013 ב 10:17

    מעולה, הסבר מקיף תודה.

    הגב