Skip to content

Latest commit

 

History

History
412 lines (288 loc) · 29.8 KB

File metadata and controls

412 lines (288 loc) · 29.8 KB
description
🤔 ทำงานกับข้อมูลมหาศาลใน .NET เขาทำกันยังไงนะ (สาย .NET ไม่รู้ไม่ได้)

LINQ

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

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

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

🤔 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);

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

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

🤔 หัวใจของ 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 ที่เขียนไว้

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

🤔 อยากเขียน 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 ที่เราจะได้ใช้กันขอสรุปเป็นตารางไว้แบบนี้ละกัน

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

คำสั่งที่เอาไว้ทำงานกับ 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 แนวคิดในการเขียนโค้ดนั่นเอง

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

ซึ่งการทำงานของ 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# หากินเป็นอาชีพไม่รู้ไม่ได้

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