description |
---|
🤔 ทำงานกับข้อมูลมหาศาลใน .NET เขาทำกันยังไงนะ (สาย .NET ไม่รู้ไม่ได้) |
ในบทนี้เราจะมาทำความรู้จักกับหนึ่งในความสามารถที่ทรงพลังที่สุดของภาษา 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 คือ ชุดคำสั่งที่จะทำให้เราทำงานกับกลุ่มของข้อมูลได้ง่ายๆ เช่น ทำงานกับ ข้อมูลที่ดึงมาจากฐานข้อมูล ทำงานกับ XML หรือพวก collection ต่างๆ โดยมีภาษาที่ใกล้เคียงกับ SQL syntax นั่นเอง
ซึ่งจากประสบการณ์ที่ผมไปสอนมาพบว่า เราไปดูตัวอย่างกันแล้วจะเข้าใจ 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 ได้นั้นเราจะต้องเรียกใช้ 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 มีหัวใจการทำงานแค่ 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 เรื่องคือ
คือกลุ่มข้อมูลที่เราต้องการจะทำงานด้วย ซึ่งกลุ่มข้อมูลในที่นี้คืออะไรก็ได้ที่เป็นตระกูล collection ที่มาจาก IEnumerable นั้นเอง ซึ่ง array ก็เป็นหนึ่งในนั้น เราเลยสามารถใช้ LINQ ทำงานด้วยได้
คือคำสั่งที่เราต้องการจะไปกระทำกับ Data Source ของเรา ซึ่งจากโค้ดตัวอย่างคือเราจะทำการ filter เอาเฉพาะข้อมูลตัวที่ 2 หารลงตัวนั่นเอง
คือตัวสั่งให้ query ที่เราเตรียมไว้มันเริ่มทำงาน ซึ่งในโค้ดคือเจ้า foreach นั่นเอง มันจะเป็นตัวกระตุ้นให้โปรแกรมเริ่มวิ่งเข้าไปที่ data source เพื่อดึงข้อมูลมาตาม query ที่เขียนไว้
{% hint style="danger" %}
Query Execution
เจ้า query execution นี้มีการทำงานทั้งหมด 2 รูปแบบคือ Deferred Execution และ Forcing Immediate Execution เดี๋ยว ซึ่งทั้ง 2 แบบนี้จะต่างกันอย่างสิ้นเชิง และทำให้เหล่า developer สาย .NET ตกม้าตายมาเยอะแล้ว ซึ่งมันคือผมขอไปอธิบายไว้ด้านล่างๆครัช
{% endhint %}
เราก็แค่เรียนรู้ว่า LINQ มันสามารถเล่นอะไรกับกลุ่มของข้อมูลได้บ้างเพียงเท่านี้ก็ใช้ LINQ ได้เลย ส่วนถ้าอยากรีดศักยภาพมากขึ้นเราต้องเข้าใจการใช้ Lambda
กับ Generic
ด้วยจะดีมาก ดังนั้นเราลองไปดูว่า LINQ ทำอะไรกับกลุ่มของข้อมูลได้บ้างกัน
ถ้าเราอยากคัดกรองข้อมูลให้มันเอาเฉพาะของที่เราอยากได้เท่านั้นออกมา เราสามารถใช้คำสั่ง 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 }
ในกรณีที่ 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 }
เราสามารถสร้างทำให้มันแบ่งข้อมูลออกเป็นกลุ่มๆตามเงื่อนไขได้ เช่น จากโค้ดตัวนี้
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 ที่เราจะได้ใช้กันขอสรุปเป็นตารางไว้แบบนี้ละกัน
{% hint style="info" %}
แนะนำให้อ่าน
ตัวอย่างการทำงานจริงๆของแต่ละคำสั่งคืออะไร สามารถไปอ่านได้จากบทความนี้นะ พระคัมภีร์การใช้คำสั่ง LINQ
{% endhint %}
กลุ่มนี้ทั้งหมดเป็น 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 |
กลุ่มนี้ทั้งหมดเป็น Forcing Immediate Execution - คืออะไรไปอ่านต่อได้จากด้านล่าง
คำสั่ง | ใช้สำหรับ | ผลลัพท์ |
---|---|---|
ToArray | แปลงเป็น Array<T> | Array<T> |
ToList | แปลงเป็น List<T> | List<T> |
ToDictionary | แปลงเป็น Dictionary<K, V> | Dictionary<K, V> |
จากที่เคยอธิบายไปว่า 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 แบบ โดยแต่ละแบบทำงานกันแบบนี้
เป็นแบบที่เราคุ้นเคยกันที่สุด นั่นคือเป็นการสั่งออกไปแล้วได้ผลลัพท์กลับมาทันทีนั่นเอง (ดูกลุ่มคำสั่งนี้ได้จากตารางด้านบน) เช่น โค้ดด้านล่างนี้ เป็นการหาค่าสูงสุดของข้อมูลใน collection
var numbers = new int[] { 7, 5, 3, 1, 6, 2, 4 };
// Forcing Immediate Execution
var result = numbers.Max();
ผลลัพท์ result = 7
เป็นคำสั่งที่จะไม่ทำงานจนกว่าจะผ่านจุดที่เกิด 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 แล้วล่ะก็ มันจะกลายเป็น 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
ทุกๆครั้งที่เราไปดึงข้อมูลมาจาก data source เพื่อมาทำการประมวลผล มันจะมีการดึงข้อมูลมา 2 รูปแบบคือ
เป็นการดึงข้อมูลมาแบบต่อเนื่อง โดยมันจะค่อยๆอ่านข้อมูลจาก data source มาทำการประมวลผลเรื่อยๆ ไม่ได้ดึงมาตูมเดียวทั้งหมดนั่นเอง ซึ่งเจ้าตัวนี้จะเหมาะใช้กับการทำงานกับ data source ที่มีขนาดใหญ่ๆ
เป็นการดึงข้อมูลมาตูมเดียวจบ แล้วทำการประมวลผลเลย
เรื่องพื้นฐานสุดท้ายที่นึกออกละ คำสั่งพวก LINQ ทั้งหลายมันเป็น Extension Method ที่อยู่ใน namespace System.Linq;
ดังนั้นมันหมายความว่าคำสั่ง LINQ มันเชื่อมกันได้ เช่น ผมมี Data Source เป็นตัวเลข 1-100 ตามนี้
var numbers = Enumerable.Range(1, 100);
แล้วผมต้องการข้อมูล 4 กลุ่มตามนี้
- กลุ่มเลขคู่
- กลุ่มเลขคู่ที่ 5 หารลงตัว
- กลุ่มเลขคู่ที่ 7 หารลงตัว
- กลุ่มเลขคู่ที่ 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 %}