LINQ

🤔 ทำงานกับข้อมูลมหาศาลใน .NET เขาทำกันยังไงนะ (สาย .NET ไม่รู้ไม่ได้)

ในบทนี้เราจะมาทำความรู้จักกับหนึ่งในความสามารถที่ทรงพลังที่สุดของภาษา C# เลยก็ได้ว่า นั่นก็คือเจ้าสิ่งที่เรียกว่า LINQ ซึ่งย่อมาจาก Language Integrated Query นั่นเอง โดยเจ้าตัวนี้เป็นหนึ่งมหากาพย์ที่ทำให้เราลดโค้ดจากร้อยๆบรรทัดให้เหลือแค่เพียงไม่กี่บรรทัดได้ และยังช่วยให้โค้ดที่เขียนกลายเป็น Clean Code อีกด้วยนะ เราลองไปทำความเข้าใจเรื่องของ LINQ กันเลยเลยดีกว่าครัช

ถ้าคิดว่าใช้งาน LINQ คล่องแล้ว และรู้จักการทำงานแบบ Declarative กับ Imperative ของ LINQ และการทำ Chain แล้วละก็ข้ามเรื่องนี้ไปได้เลย

LINQ มีหลายคนเลยสงสัยว่ามันออกเสียงว่ายังไง เจ้าตัวนี้ออกเสียงว่า ลิงค์ ครับ (อ้างจากสำเนียงเมกา) ไม่ได้ออกเสียงว่า ลิน หรือ ลินคิว ใดๆทั้งสิ้น จำง่ายๆว่ามันออกเสียงเหมือน link ที่ไม่ออกเสียงตัว k อ่ะ

🤔 LINQ คือไรหว่า ?

แบบสั้นๆก่อนเจ้า LINQ คือ ชุดคำสั่งที่จะทำให้เราทำงานกับกลุ่มของข้อมูลได้ง่ายๆ เช่น ทำงานกับ ข้อมูลที่ดึงมาจากฐานข้อมูล ทำงานกับ XML หรือพวก collection ต่างๆ โดยมีภาษาที่ใกล้เคียงกับ SQL syntax นั่นเอง

🤔 LINQ ใช้ไงหว่า ?

ซึ่งจากประสบการณ์ที่ผมไปสอนมาพบว่า เราไปดูตัวอย่างกันแล้วจะเข้าใจ LINQ ได้เร็วกว่าอ่านทฤษฎีครับ ดังนั้นผมจะใช้ตัวอย่างนี้อธิบายเอานะ โดยโจทย์ของผมคือถ้าผมมีข้อมูลตัวเลข 1~7 อยู่ใน array ตามโค้ดด้านล่าง

var numbers = new int[] { 1, 2, 3, 4, 5, 6, 7 };

แล้วถ้าเกิดผมอยากดึงเฉพาะเลขคู่ออกมาจากตัวแปร numbers ล่ะต้องทำยังไง ? ซึ่งถ้าเขียนโค้ดแบบปรกติเราก็จะเขียนออกมาได้ราวๆนี้

🔥 เขียนแบบปรกติ

ผมก็จะสร้าง array ขึ้นใหม่ เพื่อเอาไว้เก็บค่าเฉพาะเลขคู่ไว้ยังไงล่ะ ตามโค้ดด้านล่างเลย

var evenIndex = 0;
var evenNumbers = new int[3];
for (int i = 0; i < numbers.Length; i++)
{
    if (numbers[i] % 2 == 0)
    {
        evenNumbers[evenIndex++] = numbers[i];
    }
}

🔥 เขียนแบบใช้ LINQ

คราวนี้เราก็จะมาลองเอา LINQ มาแก้โจทย์เดียวกันดูบ้างนะ ซึ่งการที่จะใช้ LINQ ได้นั้นเราจะต้องเรียกใช้ using System.Linq; ไว้ด้านบนสุดด้วยนะ และการเขียน LINQ เราสามารถเขียนได้ 2 วิธีตามนี้

1.เขียน LINQ แบบเต็มๆ

var evenNumbers = from it in numbers
                  where it % 2 == 0
                  select it;

ตาไม่ได้ฝาดไปหรอกครับ โค้ดมันเหลือแค่นั้นจริงๆ และมันเขียนแทบจะเหมือนภาษา SQL syntax เลยยังไงล่ะ ดังนั้นใครที่เขียน SQL syntax เป็นอยู่แล้วรับรองครับว่าสบายเลย

อธิบายโค้ดตามบรรทัด บรรทัดที่ 1 เราเลือกว่าจะทำงานกับกลุ่มข้อมูลตัวไหน ซึ่งในที่นี้คือ numbers นั่นเอง โดยข้อมูลแต่ละตัวในกลุ่มข้อมูลนั้นเราจะใช้ตัวแปรที่ชื่อว่า it เข้าไปไล่ค่ามัน (เหมือน foreach แหละ) บรรทัดที่ 2 เราทำการคัดกรองเอาเฉพาะข้อมูลตัวที่มันถูกหารด้วย 2 ลงตัวเท่านั้น ด้วยคำสั่ง where บรรทัดที่ 3 ข้อมูลไหนที่ผ่านเงื่อนไขจากบรรทัดที่ 2 เราจะทำการเอาข้อมูลเหล่านั้นมาใช้

2.เขียน LINQ แบบย่อๆ ที่เห็นมันสั้นแล้วจริงๆมันยังเขียนให้กระชับลงได้อีกตามโค้ดด้านล่างเลย

var evenNumbers = numbers.Where(it => it % 2 == 0);

ไม่ว่าจะเป็น แบบเต็ม หรือ แบบย่อ การทำงานมันเหมือนกันครับ ขึ้นอยู่กับว่าถนัดแบบไหน ซึ่งปรกติผมแนะนำว่าให้เขียนแบบย่อครับ

Clean Code จะเห็นว่าไม่ว่าจะเขียนแบบเต็มหรือแบบย่อนั้น มันอ่านง่ายสบายตากว่า การเขียนแบบปรกติเยอะม๊วกกกก ดังนั้นการทำงานอะไรก็แล้วแต่ที่ทำงานกับกลุ่มข้อมูล ผมแนะนำว่าให้ใช้ LINQ ไปเลยถ้าใช้ได้

🤔 หัวใจของ LINQ มีไรบ้าง ?

อยากเขียน LINQ เป็นจริงๆนั้นง่ายมาก เพราะ LINQ มีหัวใจการทำงานแค่ 3 เรื่องเท่านั้น โดยผมจะใช้โค้ดที่ใช้แก้โจทญืในตอนแรก มาเขียนกำกับหัวใจการทำงานลงไปละกัน ชึบๆ

// 1. Data source
var numbers = new int[] { 1, 2, 3, 4, 5, 6, 7 };

// 2.Query
var evenNumbers = numbers.Where(it => it % 2 == 0);

// 3.Query execution
foreach (int item in evenNumbers)
{
    Console.WriteLine(item);
}

ซึ่งจากโค้ดด้านบนจะเห็นว่ามันมีการแบ่งงานออกเป็น 3 เรื่องคือ

🔥 Data Source

คือกลุ่มข้อมูลที่เราต้องการจะทำงานด้วย ซึ่งกลุ่มข้อมูลในที่นี้คืออะไรก็ได้ที่เป็นตระกูล collection ที่มาจาก IEnumerable นั้นเอง ซึ่ง array ก็เป็นหนึ่งในนั้น เราเลยสามารถใช้ LINQ ทำงานด้วยได้

🔥 Query

คือคำสั่งที่เราต้องการจะไปกระทำกับ Data Source ของเรา ซึ่งจากโค้ดตัวอย่างคือเราจะทำการ filter เอาเฉพาะข้อมูลตัวที่ 2 หารลงตัวนั่นเอง

🔥 Query execution

คือตัวสั่งให้ query ที่เราเตรียมไว้มันเริ่มทำงาน ซึ่งในโค้ดคือเจ้า foreach นั่นเอง มันจะเป็นตัวกระตุ้นให้โปรแกรมเริ่มวิ่งเข้าไปที่ data source เพื่อดึงข้อมูลมาตาม query ที่เขียนไว้

Query Execution เจ้า query execution นี้มีการทำงานทั้งหมด 2 รูปแบบคือ Deferred Execution และ Forcing Immediate Execution เดี๋ยว ซึ่งทั้ง 2 แบบนี้จะต่างกันอย่างสิ้นเชิง และทำให้เหล่า developer สาย .NET ตกม้าตายมาเยอะแล้ว ซึ่งมันคือผมขอไปอธิบายไว้ด้านล่างๆครัช

🤔 อยากเขียน LINQ ต้องทำไง ?

เราก็แค่เรียนรู้ว่า LINQ มันสามารถเล่นอะไรกับกลุ่มของข้อมูลได้บ้างเพียงเท่านี้ก็ใช้ LINQ ได้เลย ส่วนถ้าอยากรีดศักยภาพมากขึ้นเราต้องเข้าใจการใช้ Lambda กับ Generic ด้วยจะดีมาก ดังนั้นเราลองไปดูว่า LINQ ทำอะไรกับกลุ่มของข้อมูลได้บ้างกัน

🔥 คัดกรองข้อมูล (Filtering)

ถ้าเราอยากคัดกรองข้อมูลให้มันเอาเฉพาะของที่เราอยากได้เท่านั้นออกมา เราสามารถใช้คำสั่ง Where ในการคัดกรองได้ เช่น ถ้าเรามีโค้ดเป็นแบบนี้

var numbers = new int[] { 1, 2, 3, 4, 5, 6, 7 };

แล้วเราอยากได้เฉพาะตัวเลขที่มากกว่า 4 ขึ้นไป เราก็จะเขียนโค้ดออกมาเป็นตามนี้

// เขียนแบบเต็ม
var fullQry = from it in numbers
              where it > 4
              select it;

// เขียนแบบย่อ
var shortenQry = numbers.Where(it => it > 4);

ผลลัพท์ { 5, 6, 7 }

หรือเราอยากคัดกรองเอาเฉพาะ เลขคี่ ที่มากกว่า 2 ขึ้นไป

// เขียนแบบเต็ม
var fullQry = from it in numbers
              where it % 2 != 0 && it > 2
              select it;
              
// เขียนแบบย่อ
var shortenQry = numbers.Where(it => it % 2 != 0 && it > 3);

ผลลัพท์ { 3, 5, 7 }

🔥 เรียงลำดับ (Ordering)

ในกรณีที่ data source ของเราไม่ได้เรียงลำดับมา เราสามารถทำให้มันเรียงลำดับให้เราได้ เช่นผมมี data source เป็นแบบนี้

var numbers = new int[] { 7, 5, 3, 1, 6, 2, 4 };

เรียงจากน้อยไปมาก

// เขียนแบบเต็ม
var fullQry = from it in numbers
              orderby it
              select it;

// เขียนแบบย่อ
var shortenQry = numbers.OrderBy(it => it);

ผลลัพท์ { 1, 2, 3, 4, 5, 6, 7 }

เรียงจากมากไปหาน้อย

// เขียนแบบเต็ม
var fullQry = from it in numbers
              orderby it descending
              select it;

// เขียนแบบย่อ
var shortenQry = numbers.OrderByDescending(it => it);

ผลลัพท์ { 7, 6, 5, 4, 3, 2, 1 }

🔥 จัดกลุ่ม (Grouping)

เราสามารถสร้างทำให้มันแบ่งข้อมูลออกเป็นกลุ่มๆตามเงื่อนไขได้ เช่น จากโค้ดตัวนี้

var numbers = new int[] { 1, 2, 3, 4, 5, 6, 7 };

ผมต้องการจะให้มันแบ่งออกเป็น 2 กลุ่มคือ กลุ่มเลขคู่ กับ กลุ่มเลขคี่ ก็จะเขียนออกมาได้ประมาณนี้ (ขี้เกียจเขียนแบบเต็มแล้วนะ)

var qry = numbers.GroupBy(it => it % 2 == 0);

ผลลัพท์ กลุ่ม false จะมีข้อมูลเป็น 1, 3, 5, 7 กลุ่ม true จะมีข้อมูลเป็น 2, 4, 6

เอาเท่านี้ก่อนละกัน เพราะไม่งั้นมันจะเยอะมากจนบทความนี้อาจจะอ่านแล้วกระตุกเลย ผมขอเอาคำสั่งทั้งหมดไปเขียนสรุปไว้ในหัวข้อถัดไปเลยละกัน

🤔 LINQ ทำไรได้บ้าง ?

ความสามารถแค่ส่วนหลักๆของ LINQ ที่เราจะได้ใช้กันขอสรุปเป็นตารางไว้แบบนี้ละกัน

แนะนำให้อ่าน ตัวอย่างการทำงานจริงๆของแต่ละคำสั่งคืออะไร สามารถไปอ่านได้จากบทความนี้นะ พระคัมภีร์การใช้คำสั่ง LINQ

คำสั่งที่เอาไว้ทำงานกับ collection

กลุ่มนี้ทั้งหมดเป็น Deferred Execution - คืออะไรไปอ่านต่อได้จากด้านล่าง

คำสั่ง

ใช้สำหรับ

ผลลัพท์

Where

กรองข้อมูล

Collection

Select

เลือก

Collection

Distinct

ตัดตัวซ้ำ

Collection

Take

เอา

Collection

Skip

ข้าม

Collection

SkipWhile

ข้ามจนกว่า

Collection

TakeWhile

เอาจนกว่า

Collection

OrderBy

เรียงลำดับ น้อย-มาก

Collection

OrderByDescending

เรียงลำดับ มาก-น้อย

Collection

Reverse

เรียงลำดับกลับด้าน

Collection

Union

รวม 2 collection เข้าด้วยกัน

Collection

Intersect

เอาเฉพาะตัวที่ซ้ำกันใน 2 collection

Collection

Except

ตัดตัวที่ซ้ำกับ collection อื่น

Collection

คำสั่งที่ได้ผลลัพท์กลับมาเลย

กลุ่มนี้ทั้งหมดเป็น Forcing Immediate Execution - คืออะไรไปอ่านต่อได้จากด้านล่าง

คำสั่ง

ใช้สำหรับ

ผลลัพท์

Count

นับว่ามีกี่ตัว

number

Sum

หาผลรวม

number

Min

หาค่าน้อยสุด

number

Max

หาค่ามากสุด

number

Average

หาค่าเฉลี่ย

number

First

เอาข้อมูลตัวแรก

T

FirstOrDefault

เอาข้อมูลตัวแรก ถ้าไม่เจอขอ default

T หรือ default

Any

ดูว่ามีซักตัวไหมที่ตรงเงื่อนไข

bool

All

ทุกตัวตรงเงื่อนไขหรือไม่

bool

Contains

ใน collection มีตัวนี้หรือเปล่า

bool

คำสั่งในการแปลง collection

กลุ่มนี้ทั้งหมดเป็น Forcing Immediate Execution - คืออะไรไปอ่านต่อได้จากด้านล่าง

คำสั่ง

ใช้สำหรับ

ผลลัพท์

ToArray

แปลงเป็น Array<T>

Array<T>

ToList

แปลงเป็น List<T>

List<T>

ToDictionary

แปลงเป็น Dictionary<K, V>

Dictionary<K, V>

💡 Deferred vs Immediate

จากที่เคยอธิบายไปว่า LINQ มีการสั่ง execution ทั้งหมด 2 รูปแบบนั่นคือ Deferred Execution และ Forcing Immediate Execution ซึ่งทั้งสองตัวนี้แตกต่างกันสิ้นเชิง เพราะมันเกิดมาจาก 2 แนวคิดในการเขียนโค้ดนั่นเอง

แนะนำให้อ่าน เจ้า 2 แนวคิดที่ว่านั่นคือ Functional Programming กับ Imperative Programming นั่นเอง ซึ่งสามารถอ่านมันได้เต็มๆได้จากลิงค์นี้เบย Microsoft document - Functional vs Imperative

ซึ่งการทำงานของ LINQ มันจะได้ผลลัพท์กลับมาทั้งหมด 2 แบบ โดยแต่ละแบบทำงานกันแบบนี้

🔥 Forcing Immediate Execution

เป็นแบบที่เราคุ้นเคยกันที่สุด นั่นคือเป็นการสั่งออกไปแล้วได้ผลลัพท์กลับมาทันทีนั่นเอง (ดูกลุ่มคำสั่งนี้ได้จากตารางด้านบน) เช่น โค้ดด้านล่างนี้ เป็นการหาค่าสูงสุดของข้อมูลใน collection

var numbers = new int[] { 7, 5, 3, 1, 6, 2, 4 };
// Forcing Immediate Execution
var result = numbers.Max();

ผลลัพท์ result = 7

🔥 Deferred Execution

เป็นคำสั่งที่จะไม่ทำงานจนกว่าจะผ่านจุดที่เกิด Query Execution ขึ้นเท่านั้น ซึ่งคำสั่งตระกูลนี้จะได้ผลลัพท์กลับมาเป็น collection ของ IEnumerable<T> นั่นเอง (ดูกลุ่มคำสั่งนี้ได้จากตารางด้านบน) เช่นโค้ดตัวอย่างจะทำการดึงค่าเฉพาะเลขคู่ออกมาเท่านั้น แต่ผมจะเพิ่มว่าทุกๆ loop มันจะมีตัวนับเลขถูกเพิ่มค่าเข้าไปเรื่อยๆ ตามนี้

var runner = 0;
var qry = numbers.Where(it => it % 2 == 0 && runner++ > 0);
Console.WriteLine(runner);

คำถามคือ runner มีค่าเป็นเท่าไหร่ถ้าผม run โค้ดเพียงเท่านี้เป๊ะๆเลย ?

เฉลย runner จะมีค่าเป็น 0 ครับ

เพราะคำสั่งในกลุ่มนี้มันเป็นการจำว่ามันจะต้องไปทำอะไรกับ data source อย่างเดียวเท่านั้น มันจะไม่ดำเนินการอะไรเลย จนกว่ามันจะผ่าน Query Execution ซักตัวนั่นเอง

จากที่ว่ามาผมก็เลยเพิ่ม query execution แบบง่ายๆเข้าไปนั่นคือ foreach แบบโง่ๆเลยตามนี้

var runner = 0;
var qry = numbers.Where(it => it % 2 == 0 && runner++ > 0);
Console.WriteLine($"Before Query Execution, Runner: {runner}");
foreach (var item in qry)
{
}
Console.WriteLine($"After Query Execution, Runner: {runner}");

คำถามคือ ในบรรทัดที่ 3 กับบรรทัดที่ 7 มันจะโชว์เลขอะไรออกมา ?

เฉลย Before Query Execution, Runner: 0 After Query Execution, Runner: 3

สาเหตุที่บรรทัดที่ 3 มันโชว์เลข 0 เพราะค่า query มันยังไม่ผ่าน Query Execution นั่นเอง ส่วนบรรทัดที่ 7 ที่มีนโชว์เลข 3 เพราะมันผ่าน Query Execution แล้วนั่นเองมันเลยไปเพิ่มค่า runner ยังไงล่ะ

🔥 Deferred Execution + Forcing Immediate Execution

ถ้าในกรณีที่เราเขียนคำสั่งเป็น Deferred Execution แต่เราจบด้วย Forcing Immediate Execution แล้วล่ะก็ มันจะกลายเป็น Forcing Immediate Execution โดยทันที เช่นโค้ดเดิมด้านบน ผมเอามาเขียนให้มันจบโดยใช้คำสั่ง .Count() ซึ่งเป็นคำสั่งของ forcing immediate execution ผมจะได้ผลลัพท์ออกมาแบบนี้

var runner = 0;
var qry = numbers.Where(it => it % 2 == 0 && runner++ > 0).Count();
Console.WriteLine($"Runner: {runner}");

ผลลัพท์ Runner: 3

💡 Streaming vs Non-Streaming

ทุกๆครั้งที่เราไปดึงข้อมูลมาจาก data source เพื่อมาทำการประมวลผล มันจะมีการดึงข้อมูลมา 2 รูปแบบคือ

🔥 Streaming

เป็นการดึงข้อมูลมาแบบต่อเนื่อง โดยมันจะค่อยๆอ่านข้อมูลจาก data source มาทำการประมวลผลเรื่อยๆ ไม่ได้ดึงมาตูมเดียวทั้งหมดนั่นเอง ซึ่งเจ้าตัวนี้จะเหมาะใช้กับการทำงานกับ data source ที่มีขนาดใหญ่ๆ

🔥 Non-Streaming

เป็นการดึงข้อมูลมาตูมเดียวจบ แล้วทำการประมวลผลเลย

🤔 มีไรที่ควรรู้อีกไหม ?

เรื่องพื้นฐานสุดท้ายที่นึกออกละ คำสั่งพวก LINQ ทั้งหลายมันเป็น Extension Method ที่อยู่ใน namespace System.Linq; ดังนั้นมันหมายความว่าคำสั่ง LINQ มันเชื่อมกันได้ เช่น ผมมี Data Source เป็นตัวเลข 1-100 ตามนี้

var numbers = Enumerable.Range(1, 100);

แล้วผมต้องการข้อมูล 4 กลุ่มตามนี้

  1. กลุ่มเลขคู่

  2. กลุ่มเลขคู่ที่ 5 หารลงตัว

  3. กลุ่มเลขคู่ที่ 7 หารลงตัว

  4. กลุ่มเลขคู่ที่ 5 และ 7 หารลงตัว

เราก็จะสามารถใช้ความสามารถในการเชื่อมกันออกมาแบบนี้ได้

// 1.กลุ่มเลขคู่
var evenNumberQry = numbers
    .Where(it => it % 2 == 0);

// 2.กลุ่มเลขคู่ที่ 5 หารลงตัว
var eventWithDividableBy5NumberQry = evenNumberQry
    .Where(it => it % 5 == 0);

// 3.กลุ่มเลขคู่ที่ 7 หารลงตัว
var eventWithDividableBy7NumberQry = evenNumberQry
    .Where(it => it % 7 == 0);

// 4.กลุ่มเลขคู่ที่ 5 และ 7 หารลงตัว
var eventWithDividableBy5N7NumberQry = 
    eventWithDividableBy5NumberQry
    .Union(eventWithDividableBy7NumberQry);

จะเห็นว่ากลุ่มที่เป็นเลขคู่จะใช้เป็นตัวตั้งต้นตัวแรก แล้วที่เหลือจะเอาผลลัพท์ของตัวแรกมาใช้ต่อเรื่อยๆได้ หรือในข้อ 4 เราจะเขียน Chain กันในรูปแบบนี้ก็ได้เหมือนกัน

// 4.กลุ่มเลขคู่ที่ 5 และ 7 หารลงตัว
var eventWithDividableBy5N7NumberQry = evenNumberQry
                .Where(it => it % 5 == 0)
                .Where(it => it % 7 == 0);

🎯 บทสรุป

LINQ เป็นมหากาพย์ตัวนึงที่ดูเหมือนว่ามันจะเยอะมาก แต่ถ้าเราเข้าใจมันทั้งหมดแล้วเราจะพบว่า มันไม่มีอะไรเลย และไม่ต้องไปนั่งไล่จำอะไรเลย ขอแค่รู้หัวใจหลัก 3 เรื่องของมันก็พอ Data Source Query Query Execution เท่านั้นเอง เพียงเท่านี้โค้ดของเราก็จะกระชับและทรงพลังมาก เพราะมันสามารถไปเชื่อมใช้งานกับสิ่งต่างๆได้อีกเยอะเลย เช่น ทำ Query Database ทำงานร่วมกับ Entity Framework หรือแม้กระทั่งการทำงานกับ Reactive เช่น Reactive Extension (Rx) ก็ยังได้ คือจริงๆมันสารพัดประโยชน์มากจริงๆนะเจ้าตัวนี้ จนแทบจะเรียกว่าใครเขียน C# หากินเป็นอาชีพไม่รู้ไม่ได้

แนะนำให้อ่าน ถ้าเพื่อนๆอยากเข้าใจการทำงานจริงๆของ LINQ หรือดูตัวอย่างหลายๆแบบแล้วล่ะก็สามารถเข้าไปดูเพิ่มเติมได้จากลิงค์ด้านล่างนี้เลยครัช (เนื้อหาแน่นปึก) และนอกจากนี้ยังมีตัวอย่างของภาษาอื่นๆอีกนะเช่น VB Microsoft document - LINQ

Last updated