จากคราวที่แล้ว ผมได้เขียนเกี่ยวกับเรื่องของ LINQ แบบเจาะลึกกันไปส่วนหนึ่งแล้ว ซึ่งผมได้กล่าวถึงเรื่อง
- แนวคิดแบบ Functional Programming และ Delegate
- Lambda Expression
- Local Type Inference
- Projection และ Anonymous Type
และแล้วเวลาก็ล่วงเลยมากว่าสองเดือน กว่าผมจะสามารถรวบรวมสมาธิและมาเขียนภาคที่สอง ต่อได้ เพื่อไม่ให้เป็นการเสียเวลา เรามาเริ่มดูกันดีกว่าว่า ยังมีส่วนไหนอีกบ้างของ LINQ ที่เรายังไม่ได้พูดถึงกัน
Extension Methods และ Enumerator
ในตัวอย่างตอนต้น ผมได้แสดงให้เห็นแล้วว่า อันที่จริงแล้ว LINQ ไม่ได้มีการเพิ่มฟีเจอร์ใหม่เข้าไปยัง Common Language Runtime (CLR) เพื่อรองรับการทำงานของ LINQ แต่อย่างใด แต่เป็นการพัฒนาความสามารถของตัว Compiler ให้มีความสามารถสูงขึ้น จนสามารถเปลี่ยน Syntax ของ LINQ ที่มีลักษณะคล้ายกับภาษา SQL ให้ออกมาเป็นฟังก์ชั่นได้
ลองย้อนมาดูที่ตัวอย่างอีกครั้ง จากโค๊ดด้านล่างนี้
Compiler จะทำการแปลงให้กลายเป็นลักษณะของการเรียกใช้ฟังก์ชั่น ดังนี้แทน (โค๊ดนี้ ได้จากการใช้ .NET Reflector ในการดูโค๊ดจาก Assembly
จะเห็นว่าฟังก์ชั่นที่ได้รับการเรียก คือฟังก์ชั่นที่ชื่อว่า Where และ Select ซึ่งทั้งสองฟังก์ชั่น ต่างก็รับพารามิเตอร์เป็น Delegate ที่ของฟังก์ชั่นอีกทอดหนึ่ง โดยในฟังก์ชั่น Where นั้นจะเป็นการทำงานในเชิงที่เลือกว่า ข้อมูลที่ผ่านเข้ามานั้น “ผ่านเกณฑ์” หรือไม่ โดยคืนค่าเป็น True หรือ False ซึ่งถ้าสังเกตให้ดี ในเนื้อฟังก์ชั่นนั้น ก็คือคำสั่งการเปรียบเทียบ ที่เราได้ใส่ไว้ด้านหลัง Where นั่นเอง และเช่นเดียวกับฟังก์ชั่น Select คำสั่งที่เราได้ใส่ไว้หลังคำว่า “select” ก้อมาปรากฏเป็นเนื้อของฟังก์ชั่น ที่ใช้เป็นพารามิเตอร์ของฟังก์ชั่น Select เช่นเดียวกัน
มาถึงตรงนี้ ผมเชื่อว่า หลายท่านน่าจะมีถามเกิดขึ้นแล้วว่า แล้วฟังก์ชั่น Where กับฟังก์ชั่น Select นั้น มาจากไหน? เพื่อให้โค๊ดชัดเจนขึ้น ผมขอเปลี่ยนจาก int Array เป็น List ดังนี้
และถ้าหากว่าผมใช้ .NET Reflector เปิด Assembly นี้ดูอีกครั้ง ก็จะพบว่า ยังคงได้โค๊ดแบบเดียวกับในตัวอย่างที่ผมแสดงให้ดูในข้างต้น นั่นคือมีการเรียกใช้ฟังก์ชั่น list.Where และ list.Select นั่นก็หมายความว่า ในคลาส List ควรจะต้องมี Member ที่ชื่อว่า Where และ Select ประกาศอยู่
แต่จากการใช้ Object Browser ใน Visual Studio สำรวจโครงสร้างของคลาส List ก็จะพบว่า ฟังก์ชั่นดังกล่าว ไม่ได้มีการประกาศไว้แต่อย่างใด ถึงแม้ว่าคุณจะมองเห็นฟังก์ชั่น Where และ Select ใน Intellisense ก็ตาม
นั่นก็เพราะว่า ฟังก์ชั่น Select และ Where นั้น ไม่ใช่ฟังก์ชั่นของคลาส List หรือ Array แต่ฟังก์ชั่นทั้งสองเป็นฟังก์ชั่นแบบ Extension Method ที่ถูกประกาศไว้ ใน Namespace System.Linq นั่นเอง ซึ่งถ้าคุณได้ทดลองลบบรรทัดที่มีการเรียกใช้ Namespace System.Linq ออก ก็จะพบว่า ฟังก์ชั่น Select นั้น จะหายไปทันที และจำนวนฟังก์ชั่นที่มีใน Intellisense ก็ลดลงอย่างเห็นได้ชัด
ทำไมถึงต้องใช้ Extension Method
แน่นอนว่า ผู้ที่ใช้ภาษาที่เป็น Object-Oriented Programming คงจะนึกสงสัยว่า ทำไม Microsoft ถึงได้เลือกที่จะสร้างฟีเจอร์ใหม่ ในตัวภาษา C# ขึ้นมา แทนที่จะสร้างคลาสใหม่ ที่สืบทอดมาจากคลาส List หรือ Array และประกาศฟังก์ชั่น Select และ Where อยู่ในคลาสนั้น ตามหลักการ OOP ที่ถูกต้อง
ผมเองก็ไม่ใช้ผู้ที่ออกแบบภาษา C# 3.0 แต่อย่างใด ก็คงจะไม่สามารถตอบได้อย่างถูกต้องว่า ทำไมทางผู้ออกแบบ จึงเลือกใช้วิธีนี้ แทนการสืบทอดคลาส แต่ผมมองเห็นว่า Extension Method นั้นจะมีประโยชน์อย่างมาก ในกรณีที่...
- คุณต้องการเพิ่มความสามารถให้กับคลาส โดยที่ไม่ต้องการทำลาย Compatibility
ในกรณีที่คุณมี Framework กลางของบริษัท อยู่ ซึ่งหลายๆ โปรเจคใช้งานร่วมกัน โดยการ Add Reference เข้าไปยังตัวโปรเจค การเพิ่มฟังก์ชั่น แม้แต่ฟังก์ชั่นเดียวเข้าไปใน Framework อาจจะทำให้ความ Compatible หมดลงทันที ซึ่งอาจจะฟังดูขัดกับหลักการของ OOP อยู่บ้าง (ในเมื่อโปรเจค A ไม่ได้ใช้ฟังก์ชั่นที่เพิ่มเข้่ามา ก็ไม่น่ามีปัญหาอะไร) แต่ในความเป็นจริงแล้ว ปัญหานี้ก็มีอยู่บ้างครับ เช่นกรณีที่คุณกำหนดว่า Assembly ที่ Reference มานั้น จะต้องเป็นเวอร์ชั่นที่กำหนดเท่านั้น

- คุณต้องการเพิ่มความสามารถแบบเดียวกันให้กับคลาสหลายๆ คลาส แต่ว่าคลา่สนั้น ไม่ได้สืบทอดมาในตระกูลเดียวกัน
ในกรณีของ LINQ นั้น คลาสที่คุณสามารถเขียน Query เพื่อเรียกข้อมูลออกมา สามารถเรียกได้ว่า เป็นคลาสที่สืบทอดมาจากคนละสายอย่างสิ้นเชิง เช่น Array, List, Dictionary, HashSet เป็นต้น ทั้งสี่คลาสนี้ ต่างเป็นคลาสที่ใช้เก็บข้อมูลเหมือนกัน แต่ว่ามีการทำงานที่ต่างกันโดยสิ้นเชิง และไม่มีคลาสที่เป็นคลาสในระดับต้นตระกูลร่วมกันเลย (นอกจาก object) จึงเป็นไปไม่ได้เลยที่คุณสามารถสร้างฟังก์ชั่น Select, Where ในคลาสต้นตระกูล คลาสเดียว แล้วคลาสเหล่านั้น สามารถมีฟังก์ชั่นทั้งสองเพื่อใช้งานได้ ซึ่งผมเองรู้สึกว่านี่แหละคือหลุมพรางของ OOP เพราะในบางครั้ง เรามักจะคิดว่า ถ้าต้องการสร้างความสามารถที่ใช้ร่วมกันได้ ก็เพียงแค่ไปเพิ่มฟังก์ชั่นนั้นในคลาสแม่เท่านั้นเอง โดยอาจจะลืมไปว่า บางครั้ง คลาสก็อาจจะไม่ได้สืบทอดมาจากทางเดียวกันเสมอไป (หรือถ้าทำให้ึคลาสทั้งหมด สืบทอดมาจากทางเดียวกัน ก็อาจจะยิ่งทำให้ทุกอย่างดูอลหม่านกว่าเดิม) ยังไงแล้ว ก็คงจะมีทางเดียวคือต้องทำการสร้าง Interface ชื่อ IWhereAndSelect แล้วให้คลาสเหล่านั้น ทำการ Implement Interface นี้ ซึ่งนอกจากจะยุ่งยากและมีโค๊ดแบบเดียวกันอยู่หลายๆ ที่แล้ว ยังเป็นการทำลายความ Compabitility ของคลาสเหล่านี้ กับโปรเจคอื่นๆ แทบในทันที
แต่ว่า Extension Method สามารถตอบโจทย์ข้อนี้ได้ โดยการทำให้คลาสทั้งสี่ สามารถเป็น Target ของ LINQ ได้ ทั้งหมด โดยที่ คลาสทั้งสี่ ยังคงอยู่ใน Assembly เดิม และไม่จำเป็นต้องมีการแก้ไขใดๆ และในโปรเจคที่ต้องการใช้ความสามารถของ LINQ ก็เพียงแค่นำ Assembly System.Linq เข้ามาใช้งานเท่านั้น

ด้วยเหตุนี้ ผมจึงคิดว่า นั่นน่าจะเป็นสาเหตุที่มีการคิดสร้าง Extension Method ขึ้นมาใน C# 3.0 นั่นเอง แล้วถ้าคุณต้องการมี Extension Method ของคุณบ้าง จะต้องทำอย่างไร?
การสร้าง Extension Method
การสร้าง Extension Method ของคุณเองนั้น สามารถทำได้โดยกาีรสร้างคลาสแบบ Static ขึ้นมา และสร้างฟังก์ชั่นที่จะให้เป็น Extension Method ดังนี้
สังเกตว่า ฟังก์ชั้นนั้น เป็นเพียงฟังก์ชั่นธรรมดา แต่เป็นแบบ static และ พารามิเตอร์ตัวแรกของฟังก์ชั่น ก็คือ instance ของคลาส ที่ฟังก์ชั่นนี้ จะไป Extend ซึ่งกำหนดได้ด้วยคีย์เวิร์ด this ตามด้วยชื่อคลาส
หรืออีกตัวอย่างหนึ่งที่น่าสนใจ (ผมพบจากบล็อกของ ScottGu)
แต่ต้องอย่าลืมว่า Extension Method มีผลแค่ใน Namespace ที่ คลาสที่มี Extension Method นั้น ประกาศอยู่เท่านั้น (อาจจะด้วยเหตุผลด้านความเร็วในการ Compile) ดังนั้น คุณจะต้องใช้ using เพื่อนำเอา Extension Method ที่มี มาใช้งานเสมอ
ทีนี้ผมก็ได้แสดงให้เห็นถึงที่มา และการสร้าง Extension Method ไปแล้ว ก็คงจะเห็นนะครับว่า Extension Method ทำให้การออกแบบ Framework ง่ายขึ้นมากขนาดไหน แต่ถึงเราจะทราบถึงที่มาของ Extension Method แล้ว ผมคิดว่า ยังคงมีอีกจุดหนึ่งของ Extension Method ที่น่าสนใจกว่าที่มาของมันเสียอีก นั่นคือ แล้วการส่งข้อมูลเป็นทอดๆ ระหว่าง Extension Method ของ Linq จะไม่้ทำให้ประสิทธิภาพลดลงหรือ???
การส่งต่อข้อมูล แบบเร็วที่สุดเท่าที่เราทำได้
ถ้าผมให้โจทย์ว่า ให้เขียนฟังก์ชั่น เพื่อ Return ค่าจาก Array ที่มีค่าน้อยกว่า 10 ออกมาทุกค่า คำตอบมาตรฐานที่โปรแกรมเมอร์หลายๆ ท่านจะเขียนขึ้น น่าจะเป็นแบบนี้
แน่นอนว่า ฟังก์ชั่นนี้ สามารถทำงานได้ถูกต้อง 100% และมี Time Complexity ถือว่ายอมรับได้แล้ว คือ n ครั้ง โดย n คือ จำนวนข้อมูลใน Array เพราะเราไม่อาจทราบได้ว่า array นั้น ได้รับการเรียงมาหรือไม่ หรือแม้แต่ว่าใน array นั้นมีเลข 10 อยู่หรือเปล่้า เราจึงควรจะต้องตรวจสอบทุกตัวอย่างถี่ถ้วน จึงต้องทำงาน n ครั้ง นั่นเอง
และจากนั้น ผมให้โจทย์เพิ่มว่า ให้เขียนฟังก์ชั่น ที่คำนวณค่ากำลังสอง ของ Element ทุกตัวใน Array Return ค่าออกมา ฟังก์ชั่นนั้นก็ึึุคงจะหนีไม่พ้นฟังก์ชั่นในลักษณะนี้
ฟังก์ชั่น Power2 นี้ก็มี Running Time ที่ดีที่สุดแล้ว นั่นคือ n เพราะเราต้องคำนวณทุกค่าใน Array นั่นเอง
ถ้าผมทำการเรียกใช้ฟังก์ชั้นเหล่านี้ ผมก็จะต้องเขียนโค๊ดดังนี้
หรือถ้าผมทำการแปลงให้ฟังก์ชั่นทั้งสอง เป็น Extension Method ผมก็จะสามารถเลียนแบบ Linq ได้ในระดับหนึ่งแล้ว โดย FilterLessThan10 นั้น ก็เหมือนกับฟังก์ชั่น Where และ Power2 ก็จะทำงานเหมือนกับ Select ในตัวอย่างแรก
แต่โค๊ดเหล่านั้น มีผลร้ายแรงตรงที่ การทำงานของมันรวมกัน ทั้งหมด จะต้องมีการสร้าง Array และ List ขึ้นมาใหม่ถึง 3 ครั้ง ดังเช่นในรูปนี้
และที่แย่ไปกว่านั้น คือข้อมูลที่อยู่ใน Array ที่ใช้ระหว่างทาง อาจจะไม่มีการใช้อีกต่อไป และกลายเป็นขยะในที่สุด
แน่นอนว่า ปัญหาเหล่านี้เล็กน้อยมากครับ สำหรับโปรแกรมทั่วๆ ไป เพราะเราอาจจะไม่มีความจำเป็นที่จะต้องประมวลผลข้อมูลเป็นจำนวนมาก (แรมก้อมีตั้งเยอะแยะ) และเป็นสิ่งที่ Compiler ไม่สามารถตรวจสอบให้เราได้ เราจึงไม่ได้รู้สึกว่าเป็นอะไรที่เราต้องแก้ไข และที่เลวร้ายกว่านั้นคือ ปัญหาความเชื่องช้าเหล่านี้ มักจะไม่เกิดขึ้น ในระหว่างการพัฒนา ซึ่งพอเวลาผ่านไป และระบบเริ่มเก็บข้อมูลมากขึ้น ในระดับแสน หรือล้านเรคคอร์ด เมื่อนั้นเราก็จะพบว่ามันสายไปแล้ว เพราะทางออกเดียว คือต้องรื้อโปรแกรมใหม่ หรือไม่ก้ออัดแรม อัด CPU เข้าไป เพื่อลดเวลาในการประมวลผล
LINQ อาจช่วยคุณได้
แต่สำหรับ Linq แล้ว การเลือกข้อมูลขึ้นมา จากคอลเลคชั่น จะได้การทำงานแบบนี้ครับ
นั่นก็คือ ข้อมูลจาก Array จะค่อยๆ ผ่านฟังก์ชั่นแต่ละตัว แล้วคืนค่าออกมาในที่สุด อันที่จริง ผมได้เขียนเกี่ยวกับเรื่องนี้ไว้แล้ว ที่โพสนี้
โดยสรุปก็คือฟังก์ชั่น Where, Select ใน LINQ นั้น ใช้การเข้าถึงข้อมูลในคอลลเคชั่นผ่านตัว Enumerator แทนที่จะเป็นการเข้าถึงตัวข้อมูลจริงๆ โดยตรง (เช่นการใช้ index กับคอลเลคชั่น) และตัวฟังก์ชั่นเอง ก็ไม่ได้ทำการส่งคอลเลคชั่นออกไปทั้งก้อนเหมือนกัน แต่เป็นการส่ง Enumerator ของผลลัพธ์ ออกไปอีกทอดหนึ่ง
ดังนั้น ไม่ว่าจะมีการเรียกฟังก์ชั่นซ้อนกันกี่ทอด ก็แทบจะไม่มีการใช้แรมเพิ่มขึ้นเลย และไม่ต้องมีการสร้างคอลเลคชั่นเพิ่มด้วย เพราะว่า ข้อมูลชิ้นเดิม ก็เพียงแค่ถูกส่งต่อไปเป็นทอดๆ ออกมาเท่านั้น
ทีนี้ ถ้าคุณต้องการเปลี่ยนฟังก์ชั่น FilterLessThan10 ให้มีประสิทธิภาพเหมือนกับ LINQ ก็สามารถทำได้โดยการแก้ไขเพียงเล็กน้อเท่านั้น ดังนี้
สังเกตว่า int[] และ List<int> จะถูกเปลี่ยนเป็น IEnumerable<int>แทน และการ return ค่า จะเป็นการใช้คำสั่ง yield return ค่าที่ต้องการโดยทันทีเลย แทนที่จะเป็นการสร้างคอลเลคชั่นใหม่ หรือ Array ใหม่ เพื่อเก็บผลลัพทธ์ ส่วนการทำผลลัพธ์ไปใช้งาน ก็เพียงแค่เรียกใช้คำสั่ง foreach เช่นเดียวกับที่ FilterLessThan10 และ Power2 ใช้ นั่นเอง
ดังนั้น ต่อไปนี้ถ้าคุณต้องการจะคืน8jkจากฟังก์ชั่น เป็น Array หรือ List หรือว่า รับค่า Array หรือ List เป็นพารามิเตอร์ ขอให้อย่าลืมว่า คุณสามารถใช้เทคนิค Enumerator นี้ เพื่อทำให้โปรแกรมของคุณประหยัดแรมขึ้นได้ แม้กระทั่งฟังก์ชั่นที่เป็น Web Service ก็สามารถใช้ IEnumerable ได้เช่นกัน โดยที่ฝั่ง Client จะมองเห็นเป็น Array ตามปกติ
แต่ถ้าเรื่องของประสิทธิภาพ แน่นอนว่า การใช้ Enumerator จะมี overhead ค่อนข้างสูงกว่าการใช้ Array หรือ List โดยตรง ด้วยสาเหตุที่ว่า Enumerator นั้น จะเป็นการเรียกใช้ฟังก์ชั่น ซึ่งย่อมมีต้นทุนสูงกว่าการอ่าน เขียนแรมโดยตรงอยู่แล้ว ผมจึงได้สร้างการทดสอบง่ายๆ ขึ้นมา โดยใช้ Array ขนาด 10 ล้าน Element เพื่อจะดูว่า การใช้ Enumerator นั้น ให้ผลที่คุ้มค่าขนาดไหน
จะเห็นว่า ในการทำงานแบบทั่วไป ใช้เวลาราว 1 วินาที และใช้แรมถึง 140MB ในขณะที่การใช้ Enumerator จะทำให้โปรแกรมรันช้าลงไปกว่าครึ่ง แต่ว่าไม่มีการกินแรมเพิ่มเลย และที่น่าสนใจคือ
ถ้าคุณ Compile แบบ Release ซึ่งจะเป็นการเปิดการ Optimize โปรแกรม และยกเลิกระบบ Debug ทั้งหมด ก็จะพบว่า เวลาที่ใช้นั้น แมบจะไม่แตกต่างกันแต่อย่างใด
ส่งท้ายภาคสอง
เรื่องราวของ LINQ ยังไม่จบง่ายๆ ครับ ยังมีอีกประเด็นที่น่าสนใจจะกล่าวถึง รวมไปถึงฟีเจอร์อื่นๆ ของ LINQ อีกด้วย รอติดตามอ่านกันนะครับ