Lambda Expressions

קהל יעד

תוכניתני C# 2 העובדים ב-Generics וכן מכירים מספיק טוב שימוש ב-delegates ומעוניינים לבחון את האפשרות לעבור ל-C# 3, או כאלה שכבר עובדים ב-C# 3 ואוהבים לקרוא בנושא.

כללי

Lambda Expression היא יכולת מסוימת שאם ננסה להסבירה בצורה פשוטה, נגדיר זאת כך: היכולת שלנו לכתוב Anonymous Delegates/Methods בצורה פשוטה יותר.

Anonymous Methods?

נניח שאנו קולטים ב-Console Application מספרים מהמשתמש לתוך מערך של string. לאחר שקלטנו מערך של מחרוזות (strings), אנו נרצה להמיר אותם למספרים. דרך אחת, היא באמצעות foreach:

// input in string format
string[] input = new string[] { "1", "2", "3", "4", "5", "6" };           

// convert to int typed list
List<int> list = new List<int>();
foreach (string s in input)
   list.Add(int.Parse(s));           

// to array...
int[] result = list.ToArray();

 

דרך נוספת, היא להשתמש ב- Array.ConvertAll< >:

// input in string format
string[] input = new string[] { "1", "2", "3", "4", "5", "6" };    

// to array...
int[] result = Array.ConvertAll<string, int>(input, MyConvert);
private static int MyConvert(string s)
{
   return int.Parse(s);
}

Array.ConvertAll< > סה"כ מבצע בדיוק את מה שעשינו קודם לכן: foreach וקריאה ל-MyConvert, היא הפונקציה שלנו, המתרגמת מחרוזת בודדת ל-int. קצת מסורבל. כתבנו פונקציה "שלמה" לטובת מטרה פשוטה: לבצע int.Parse.

באמצעות Anonymous Method אפשר לפשט את הדברים:

// input in string format

string[] input = new string[] { "1", "2", "3", "4", "5", "6" };

   

// to array...

int[] result = Array.ConvertAll<string, int>(input, delegate(string s)

{

   return int.Parse(s);

});

 

למעשה, שמנו את גוף הפונקציה MyConvert ישירות בתור ארגומנט ל-Array.ConvertAll< >. אם נסדר קצת את הקוד ונצמצם שורות מיותרות, נגיע לתוצאה הבאה:

int[] result = Array.ConvertAll<string, int>(input, delegate(string s) { return int.Parse(s); });

 

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

Lambda Expressions

באמצעות C# 3, ניתן לכתוב Lambda Expressions אשר יפשטו עוד יותר את הדברים. לפני שרצים לקוד, כדאי להקדים ולומר, שהקומפיילר הולך ונהיה יותר ויותר "חכם". למעשה, הוא כ"כ חכם, שהוא פשוט מסיק הרבה דברים לבד. בדוגמא הקודמת, הקומפיילר הסיק בעצמו שערך ההחזר הוא מסוג int (לפי ה-Converter delegate וה-generics שצוינו). הקומפיילר נהיה עוד יותר "חכם" בגרסה 3, ולכן מתאפשרת הפשטה של הקוד. אנו נדגים זאת בשלבים:

  1. השלב הראשון דווקא לא קשור ל-lambda expression. הקומפיילר לבדו מבין שמדובר ב-string input ו-int output, שכן הוא מבחין בסוגי הפרמטרים הנדרשים. לכן, ניתן להוריד את ההגדרות המפורשות:

int[] result = Array.ConvertAll<string, int>(input, delegate(string s) { return int.Parse(s); });

 

  1. ברור לקומפיילר שמדובר ב-delegate. הוא מסיק זאת לפי הארגומנט המופיע בחתימה של Array.ConvertAll< >. לכן ניתן להשמיט את המילה המיותרת:

int[] result = Array.ConvertAll(input, delegate(string s) { return int.Parse(s); });

 

  1. בצורה דומה, אפשר להשמיט את ה-string בארגומנט של ה-delegate, כיוון שהקומפיילר מסיק זאת לפי העובדה ש-input הוא מערך של מחרוזות:

int[] result = Array.ConvertAll(input, (string s) { return int.Parse(s); });

 

  1. העובדה שמדובר ב-delegate שמחזיר ערך, ברורה לקומפיילר מהחתימה של Converter delegate (בדיוק כפי שהוא הסיק שמדובר בערך החזר מסוג int). לכן גם המילה return מיותרת:

int[] result = Array.ConvertAll(input, (s) { return int.Parse(s); });

 

  1. מה שנשאר הוא ארגומנט s וגוף הפונקציה:

int[] result = Array.ConvertAll(input, (s) { int.Parse(s); });

  1. במטרה להשמיט את הסוגריים המיותרים הן מהארגומנט והן מגוף הפונקציה, C# 3 מספק לנו אופרטור חדש. התוצאה, קרויה lambda expression:

int[] result = Array.ConvertAll(input, s => int.Parse(s));

 

 

  1. בהשוואה אל מול ה-anonymous delegate שכתבנו קודם (ללא ה-generics):

int[] result = Array.ConvertAll(input, delegate(string s) { return int.Parse(s); });

אל מול:

int[] result = Array.ConvertAll(input, s => int.Parse(s));

 

כאשר מבינים lambda expression כהפשטה של Anonymous Method, כאשר צד שמאל של האופרטור <= הוא הארגומנט, וצד ימין הוא גוף הפונקציה, הדברים ברורים למדי. מה שנשאר הוא לתרגל ולהפנים.

סיכום

Lambda Expressions היא דרך שבה אנחנו יכולים לפשט ולייעל את השימוש ב-Anonymous Methods. כתוצאה מהשימוש הזה, לומדים מהר מאוד כיצד לקצר את הקוד שלנו ועדיין להשאיר אותו קריא. בשילוב עם LINQ, אפשר להגיע לתוצאות נהדרות בקוד.

מה צריך בשביל זה?

.NET Framework 3.5 ו-Visual Studio 2008 או מאוחר יותר.

קישורים נוספים

Anonymous Methods

Lambda Expressions

C# 3.0 and LINQ - Expression Trees

Currently rated 4.6 by 7 people

  • Currently 4.571429/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by: eladv
Posted on: 12/22/2008 at 12:36 AM
Tags: , , , , ,
Actions: E-mail | Kick it! | DZone it! | del.icio.us
Post Information: Permalink | Comments (3) | Post RSSRSS comment feed

LINQ (To Objects) Revolutions

קהל יעד

תוכניתני C# 2 המעוניינים לבחון את האפשרות לעבור ל-C# 3, או כאלה שכבר עובדים ב-C# 3 או LINQ  ואוהבים לקרוא בנושא.

כללי

לא כולם יסכימו, אבל LINQ הוא בגדר מהפכה, בפרט LINQ To Objects.  אנו בפרופר מקודדים ומשתמשים ב-LINQ כשנה והדבר שינה באופן מהותי את שיטת הקידוד שלנו. המהפכה, לאו דווקא מתבטאת בשינוי ארכיטקטורה, אלא בצורת הקידוד היום-יומי. לפני שנים, אני זוכר שחשבתי על Object Oriented כעל מהפכה של ממש בתחום הקידוד (מהפכה ותיקה מאוד) ותמיד שאלתי את עצמי איפה תהיה המהפכה הבאה. הכוונה איננה לעוד טכנולוגיה המהווה שלב טבעי בהתפתחות הקידוד (למשל WPF, WCF וכד'), ולא מתכוון לשפות קידוד פונקציונאליות וחידושים בתחום זה, אלא משהו שממש ישנה את הקידוד הבסיסי של "שפות דור שלישי". LINQ To Objects מהווה עבורנו בפרופר מהפכה מהסוג הזה.

מה זה?

LINQ הוא ראשי תיבות של Language-Integrated Query. במשפט אחד: היכולת לשלב שאילתות בסגנון SQL בקוד שלנו. מכאן ואילך, "השמיים הם הגבול". LINQ To Objects, הוא בסיס מסוים שכדאי להכיר וללמוד גם בזכות עצמו וגם כתשתית לשימושי LINQ שונים אחרים. על מנת להבין טיפה יותר טוב, אפשר להקביל את הפעולות המבוצעות ב-LINQ לפעולות המבוצעות על טבלאות. בדיוק כפי שאנחנו רגילים לתחקר טבלאות, ולמצוא שורות ונתונים, LINQ מאפשר לנו לעשות זאת בקוד, אל מול כל סוגי הרשימות האפשרויות (IEnumerable<T>). גם רשימות ישנות יותר שאינן מממשות IEnumerable<T> אלא רק IEnumerable ניתנות לתחקור בצורה קלה ע"י הסבה פשוטה. לדוגמא, הקוד שלהלן ידפיס את המספרים במערך שערכם מעל 3:

int[] myArray = new int[] { 1, 2, 3, 4, 5, 6 };

var list = from i in myArray
             where i > 3
             select i;

foreach (int i in list)
{
   Console.Write(i);
}

"תרגיל"

נתונה רשומת לקוחות (GetCustomers() מחזירה Customer[]). יש למצוא את כל הלקוחות שה-Id שלהם קטן מ-100, ואותם למיין לפי מספר עוסק המורשה. ב-.NET 2, היינו כותבים משהו כזה:

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

foreach (Customer c in DataStore.GetCustomers())
{
   if (c.Id < 100)
      list.Add(c);
}

list.Sort(CompareCustomers);

private int CompareCustomers(Customer x, Customer y)
{
    if (x == y)
        return 0;

    if (x == null)
        return -1;

    if (y == null)
        return 1;

    if (x.RegisteredBusinessId < y.RegisteredBusinessId)
        return -1;

    if (x.RegisteredBusinessId > y.RegisteredBusinessId)
        return 1;

    return 0;
}

 

הקוד יעבוד היטב, אבל יש בו שתי בעיות:

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

באמצעות LINQ ניתן לפתור את התרגיל באופן הבא:

 

var list = from c in DataStore.GetCustomers()
             where c.Id < 100
             orderby c.RegisteredBusinessId
             select c;


 

זהו. פשוט, ברור, מדויק וקל לתחזוקה ולשינוי.

var

הרבה אנשים התפלאו על השימוש ב-var. לכאורה, חזרה אחורה ל-VB טרום עידן .NET. אלא שה-var של C# 3 מזכיר את זה שיש ב-VB, אבל שונה בפונקציונאליות בשני נושאים עיקריים: 1) בזמן קומפילציה (ולא בזמן ריצה), הקומפיילר חייב להבין מהו ה-type האמיתי של המשתנה; 2) לא ניתן להחליף את ה-type של המשתנה בהמשך הקוד. כלומר, לא ניתן לבצע:

var i = 2;      // i is int type
i = "hello";    // compilation error

 

השימוש ב-var מיועד לחסוך לנו כתיבה מפורשת של ה-type-ים החוזרים משאילתות LINQ. אלה צפויים להשתנות לפי השאילתא ולא תמיד מדובר ב-type פשוט ולכן הצורך בנוחות. למשל, שאילתת Where תחזיר type שונה ממה שתחזיר GroupBy. לא חייבים להשתמש בזה, אבל אחרי מספר ניסיונות סביר להניח ש-var יהפוך להיות האופציה המועדפת.

איך LINQ עובד?

במסגרת C# 3 בוצעו מספר "שדרוגים" משמעותיים לשפה, ניתן לומר שכמעט כולם מיועדים לאפשר את LINQ. בעיקר, הכוונה היא ל- Extension Methods. Extension Methods מאפשרים להרחיב מחלקות קיימות בפונקציונאליות שלא ניתנה להן במקור, ולא באמצעות ירושה. לדוגמא, להלן קוד הבודק את הערך עבור int מסוים:

int i = 5;

if (i => 2 && i <= 10)
{
    // do some code
}

 

אפשר גם לכתוב פונקציה לטובת העניין:

int i = 5;

if (IsBetween(i, 2, 10))
{
    // do some code
}

public static bool IsBetween(int number, int min, int max)
{
    return (number >= min && number <= max);
}

 

ב- C# 3, אפשר להוסיף את this לארגומנט הראשון, ולמעשה להנחות את הקומפיילר להבין שמדובר בהרחבה, במקרה זה ל-int (שים לב למחלקה הסטטית העוטפת – חובה):

int i = 5;

if (i.IsBetween(2, 10))
{
    // do some code
}

static class Extensions
{
    public static bool IsBetween(this int number, int min, int max)
    {
        return (number >= min && number <= max);
    }
}


 

 

רגע, אבל מה הקשר ל-LINQ? אם ניקח את הדוגמא הראשונה ונסתכל ב-Reflector על תוצאת הקומפילציה, נראה את הדבר הבא:


int[] myArray = new int[] { 1, 2, 3, 4, 5, 6 };

var list = from i in myArray
             where i > 3
             select i;

foreach (int i in list)
{
   Console.Write(i);
}


 

כלומר, הקוד המקורי ב-LINQ למעשה תורגם ע"י הקומפיילר ל-Extension Method בשם Where. המתודה Where התווספה במקרה הזה למערך מסוג int.

אם כך, ניתן גם לכתוב את אותה שאילתא בצורה הבאה:

int[] myArray = new int[] { 1, 2, 3, 4, 5, 6 };

var list = myArray.Where(i => i > 3); // lambda expression

foreach (int i in list)
{
    Console.Write(i);
}

 

כפי שניתן לראות, אפשר לפנות ל-Where ישירות (למשל באמצעות lambda expression). אם נעשה Go To Definition על הפונקציה Where, נוכל לראות שני דברים:

  • החתימה של Where היא כדלקמן:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

כלומר, כל IEnumerable<T> על כל "יורשיו", "מקבל במתנה" את הפונקציה הזו, ולא רק מערך של int.

  • עשרות Extension Methods נוספות שקיבלנו לכל ה-IEnumerable<T>. בהן נמצא גם את: Sum, Count, GroupBy ואחרות.

Extension Methods כיכולת אשר התווספה ל- C# 3, והניצול שלהן ב-LINQ, הוסיפו עשרות פונקציות אשר מאפשרות לנו לתחקר כל אובייקט מסוג רשימה (IEnumerable<T>) שלא היו זמינות לנו קודם לכן.

דוגמאות נפוצות אחרות

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

סינון (שימוש ב-Where):

IEnumerable<Customer> customers = DataStore.GetCustomers().Where(c => c.Id > 100);

האם קיים”? ("exists"):

bool exists = DataStore.GetCustomers().Any(c => c.Id == 10);

מציאת פריט ספציפי וייחודי:

Customer customer = DataStore.GetCustomers().Single(c => c.Id == 10);

מציאת פריט ספציפי וייחודי או null אם איננו קיים:

Customer customer = DataStore.GetCustomers().SingleOrDefault(c => c.Id == 1000);

if (customer == null)
    this.Response.Write("not found");
else
    this.Response.Write("found");

 

קיבוץ לקבוצות (לפי האות הראשונה בשם הלקוח) ומיון:

var group = from c in DataStore.GetCustomers()
            group c by c.CustomerName[0] into g
            orderby g.Key
            select g;

foreach (var current in group)
{
    Console.WriteLine(current.Key);
    foreach (var customer in current)
    {
        Console.WriteLine(customer.CustomerName);
    }
}

כיוונים נוספים

ייאמר לזכות מיקרוסופט שהם לא נעצרו כאן, אלא למעשה מפתחים ומקדמים את LINQ בכיוונים ואפשרויות שונות. מיקרוסופט שחררו את LINQ עם תמיכה מורחבת לנושאים שונים: LINQ To XML, LINQ To DataSets, LINQ To SQL ולאחרונה גם LINQ To Entities. כל אחד מהם הוא נושא בפני עצמו, אבל כולם מושתתים על הרעיון הבסיסי של היכולת לכתוב שאילתות בקוד, שתוצאת הקומפילציה שלהן היא קריאה ל-Extension Methods. אפילו אם לא מעוניינים לעבוד באחת מהטכנולוגיות האלה, רק השימוש ב-LINQ To Objects צפוי לחסוך הרבה כאב ראש.

סיכום

LINQ מהווה מהפכה של ממש בצורה שבה מקודדים, אם מחליטים לתרגלו ולאמצו. היכולת לתחקר, לסנן, לקבץ ולמיין "מעולם לא הייתה פשוטה יותר". בהדגמה שערכנו באחד מהמקומות להם אנו מסייעים להתקדם ל-LINQ, שם גם לא ידעו להשתמש ב-List<T> ו-List<T>.Sort( ), את קטע הקוד של סינון ומיון הלקוחות אשר הודגם קודם לכן, ללא LINQ, לקח לראשונים לסיים לאחר כחצי שעה. כאשר התבקשו לכתוב אותו ב-LINQ To Objects, הראשונים סיימו תוך כשלוש דקות.

יש עוד ל-LINQ. מה שהוסבר והודגם הוא רק "על קצה המזלג". אנחנו מאוד ממליצים.

מה צריך בשביל זה?

.NET Framework 3.5 ו-Visual Studio 2008 או מאוחר יותר.

קישורים נוספים

The LINQ Project (MS official site)

101 LINQ Samples

C# 3.0 and LINQ - Expression Trees

LINQ to JavaScript  

Currently rated 4.7 by 3 people

  • Currently 4.666667/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by: eladv
Posted on: 12/16/2008 at 1:53 AM
Tags: , , , , ,
Actions: E-mail | Kick it! | DZone it! | del.icio.us
Post Information: Permalink | Comments (0) | Post RSSRSS comment feed