รู้จัก Linq แบบถึงกึ๋น!!
เมื่อประมาณ 4-5 ปีที่แล้ว มีรุ่นพี่คนนึงซึ่งผมนับถือเป็นเทพ IT แนะนำให้ผมรู้จัก Cω (C-Omega) ซึ่งพี่เค้าอธิบาย Concept ของ C-Omega ให้ฟังว่า Relational คือ สี่เหลี่ยม, XML คือ สามเหลี่ยม, Object คือ วงกลม การจัดการข้อมูลต่างกัน ทำให้ไม่เคยรวมกันได้สักที C-Omega คือ ภาษาที่มีจุดมุ่งหมายให้ ทั้ง 3 รูปแบบรวมเข้าด้วยกันอย่างลงตัวที่สุด
C-Omega เดิมทีชื่อว่า Xen ภายหลังรวมเข้ากับ Polyphonic C# (ทำ Parallel Programming) จึงเปลี่ยนไปใช้ชื่อ C-Omega C-Omega นี่แหละครับรากเหง้าของ Linq ที่ทำให้การทำ Object-Relational Mapping (ORM) ง่ายมาก แถมเป็น Type-safe คือ ไม่มี Error จากการ assign ค่า
นอกจากนี้ส่วนที่ทำให้ linq ประสบความสำเร็จ คือ Functional Programming (FP) ซึ่ง C# เดิมทีเป็นประเภท Imperative Programming ใช่ไหมครับ เนื่องจากต้องปรับตัวให้เป็น FP จึงเกิด Concept ใหม่ ๆ เช่น Lambda Expression, Type Inferrence, และ Anonymous Type
เรามาดูกันว่า FP มันมีส่วนช่วย Linq ยังไง สมมติถ้าเราอยากได้ List ชื่อและที่อยู่ของลูกค้าที่อยู่ในกรุงเทพฯ ถ้าเขียนด้วย Linq เราเขียนได้ง่าย ๆ ว่า
var cusAddr = from c in customers where c.Province == "Bangkok" select new { c.Name, c.Address };
แต่ถ้าเราเขียนใน C# 2.0 เราต้องเขียนขนาดนี้
CusAddr cusAddr = Enumerable.Select<Customer, CusAddr>( Enumerable.Where<Customer>(customers, delegate(Customer c) { return c.Province == "Bangkok"; }), delegate(Customer c) { CusAddr result = new CusAddr(); result.Name = c.Name; result.Address = c.Address; return result; });
ดูแล้วไม่อยากเขียนใช่ไหมครับ แบบนี้ Linq ไม่ได้เกิดแน่ แถม Concept ของ Linq คือ ต้องเขียนเป็น Expression ไม่มีการ assign ค่าเกิดขึ้น ดูในส่วนของ delegate ชุดที่ 2 สิครับ เราต้องมีการ Assign ค่า Name กับ Address เข้าไป
อย่างนี้เลยเกิด Concept ของ Object initializers กับ Collection initializers ขึ้นมา คือ ค่าที่เขาต้องการ Assign เนี่ย ก็ประกาศไปตอน Instantiate Class เลย เขียนใหม่เป็นอย่างนี้ครับ
CusAddr cusAddr = Enumerable.Select<Customer, CusAddr>( Enumerable.Where<Customer>(customers, delegate(Customer c) { return c.Province == "Bangkok"; }), delegate(Customer c) { return new CusAddr() { Name = c.Name, Address = c.Address }; });
สั้นลงอีกหน่อย ทีนี้ภาษาที่เป็น FP มี Lambda Expression กันเยอะละ ภาษา C# มีมั่งละกัน จะได้เจ๋งกว่า Java :) Lamba Expression ก็ไม่มีอะไรหรอกครับ แค่ตัดคำว่า delegate กับวงเล็บ {} ออก แต่ทำให้อ่านง่ายขึ้นเยอะ เขียนใหม่เป็นแบบนี้
CusAddr cusAddr = Enumerable.Select<Customer, CusAddr>( Enumerable.Where<Customer>(customers, (Customer c) => c.Province == "Bangkok"), (Customer c) => new CusAddr() { Name = c.Name, Address = c.Address });
แต่ยังดูยุ่ง ๆ อยู่แฮะ แถมเรื่อง Type inference ไปด้วยละกัน (บรรยายแทน ความคิดของผู้ออกแบบ C# ) Type inference คือการรับทราบ Type จาก Parameter ที่ส่งเข้ามา กับ Result ทีคืนออกไป เขียนใหม่เป็นแบบนี้
CusAddr cusAddr = Enumerable.Select( Enumerable.Where(customers, c => c.Province == "Bangkok"), c => new CusAddr() { Name = c.Name, Address = c.Address });
อืม เริ่มสั้นลงละ แต่ประโยคเวลาอ่านมันไม่ Meaningful แฮะ Customers ต้องเป็น Object หลักสิ เพิ่ม Extension methods เข้าไปอีกดีกว่า Extension methods คือการเขียน Methods ที่ทำให้ Object เป้าหมายเรียกใช้ได้ โดยที่เวลาส่งผ่าน Parameter ส่งผ่านแค่ Parameter ที่ 2 เป็นต้นไป Parameter แรกก็คือ Object เป้าหมายนั่นเอง Extension method เป็นการทำ Partial function application นั่นเองนะครับ เขียนใหม่อีกที
CusAddr cusAddr = customers.Where(c => c.Province == "Bangkok") .Select(c => new CusAddr() { Name = c.Name, Address = c.Address });
โอ้วววว เริ่มใกล้ความจริงละ แต่ไอ้ Class ที่ขื่อ CusAddr นี่เราต้องเขียน Class เองเลยเรอะ ! ถ้าได้ใช้ครั้งเดียวคงไม่คุ้มที่จะสร้าง Class ขึ้นมา เพิ่ม Anonymous Type ให้อีกละกัน Anonymous Type ก็เป็น Type ชนิดหนึ่งไม่มีชื่อ Type มีแต่ Property เนื่องจากมันไม่มีชื่อ Type จึงไม่สามารถใช้เป็น Input หรือ Output ของ members ใน Class ได้ แต่ใช้งานใน Block ของ method ได้สบาย เขียนใหม่เป็น
var cusAddr = customers.Where(c => c.Province == "Bangkok") .Select(c => new { Name = c.Name, Address = c.Address });
โย่ว! สุดท้ายเพื่อความเนียน เพิ่ม Linq เข้าไป Linq ก็คือ syntax ต่าง ๆ ที่เปลี่ยนเป็น code ด้านบนให้
var cusAddr = from c in customers where c.Province == "Bangkok" select new { c.Name, c.Address };
โอ้วววว แจ่มแมว
ทีนี้เราจะเอา Code ด้านบนไปใช้ใน Query Provider ยังไงล่ะ? เราก็เพิ่มความสามารถให้ Lambda Expression เนี่ย สามารถสร้าง Expression Tree ได้สิ :) Expression Tree คือ Class ที่เก็บข้อมูล Code ของ Lambda นั่นแหละ เพื่อให้ Query Provider อ่าน เพื่อที่จะได้เอา Code ด้านบนไปแปลงเป็นภาษา SQL
เช่น ถ้าเราประกาศอย่างนี้
Func<int, int> square = x => x * x;
ผลลัพธ์คือจะได้ method ชื่อ square แต่ถ้าจะสร้าง Expression Tree ให้เขียนอย่างนี้
Expression<Func<int, int>> square = x => x * x;
แค่เติมคำว่า Expression ล้อมรอบ Func แค่นี้ก็ได้ Object ที่เป็น Expression Tree ชื่อ square มาละ
Expression Tree นอกจากจะบอกโครงสร้างของ Lambda Expression เรายังสามารถสร้าง Dynamic Code จาก Expression Tree ได้ด้วยนะ เช่น
ParameterExpression paramX = Expression.Parameter(typeof(int), "x"); Func<int, int> square = Expression.Lambda<Func<int, int>>( Expression.Multiply(paramX, paramX), paramX).Compile();
คำสั่ง Compile คือการ สร้าง anonymous method ขึ้นมานั่นเอง
ดังนั้นจุดมุ่งหมายของ Linq จึงแบ่งได้เป็น 2 แบบ หลัก ๆ คือ 1. การเขียน Linq เพื่อทำ FP เช่น Linq to Object (ในอนาคตก็จะมี Parallel Linq อีกแบบ) 2. การเขียนเพื่อแปลงภาษา เช่น Linq to Sql
คราวนี้มารู้จักกับ Syntax ของ Linq กัน สมมติว่าเราอยากรู้ว่า Sales Rep ของเราแต่ละคน ขายสินค้าได้ยอดเท่าไหร่กันบ้างในปีนี้ โดยเรียงลำดับจากคนที่ขายได้มากไปน้อย
var result = from s in salesReps join o in orders on s equals o.Owner where o.Date.Year == 2008 from d in o.Details group d.Amount * d.Price by s into g let t = g.Sum() orderby t descending select new { SalesRep = g.Key, Total = t };
from ถ้าอยู่ตัวแรกจะทำหน้าที่แค่ประกาศว่า จะเริ่มทำการ Query
join ทำหน้าที่ join รายการ 2 รายการเข้าด้วยกัน โดยต้องมี Key ที่ใช้ร่วมกัน ในที่นี้ join o in orders on s equals o.Owner หมายถึง ทำ Inner join กับ orders โดยที่ salesRep ตรงกับ Owner ของ orders
where ทำหน้าที่กรองข้อมูล ในที่นี้ where o.Date.Year == 2008 หมายถึงเอา orders เฉพาะปี 2008
from ถ้าไม่ได้อยู่ตัวแรก คือการทำ SelectMany SelectMany มีความหมาย 2 แบบ คือ 1. Cross Join คือ แต่ละรายการในชุดแรก join กับทุกรายการในชุดที่ 2 โดยไม่มีการเลือกในการจับคู่ เช่น {A,B} Cross Join กับ {1,2,3} จะได้ {A1,A2,A3,B1,B2,B3} 2. Nested query คือการเลือกรายการย่อยใน Query ต่างกับแบบ Cross Join คือ ด้านหลังของคำว่า in เป็น variable ของ Query เช่น บรรทัด from d in o.Details จะเห็นว่าด้านหลังของ in ใช้ variable ของ Query นั่นคือ o มีหมายความว่า จะดึง details แต่ละรายการใน orders มาคำนวนด้วย
group by คือการจัดกลุ่ม ในที่นี้ group d.Amount * d.Price by s into g หมายถึงจัดกลุ่มตาม salesRep โดยที่ เอาเฉพาะเนื้อหาคือ จำนวนเงินรวม (d.Amount * d.Price)
into มี 2 หน้าที่ 1. ช่วยในการ Join เช่นทำ Group Join หรือ Left Join ขี้เกียจอธิบาย เดี๋ยวยาว อ่านต่อ ที่นี่ 2. เพื่อเริ่ม query ใหม่ คำสั่ง group by กับคำสั่ง select จะเป็นการจบการ query แต่ถ้าเราเติม into ด้านหลังของ group by หรือ select จะเริ่มการ query ใหม่ เช่น ถ้าเรามี query 2 ตัว
var x = from a in set select a + 1; var y = from b in x select b + 1;
เราสามารถเขียนต่อเนื่องกันอย่างนี้ได้เลย
var y = from a in set select a + 1 into b select b + 1;
let คือการประกาศตัวแปรใน query ในที่นี้คือ let t = g.Sum() หมายถึงประกาศค่า t โดยที่ t เท่ากับจำนวนเงินรวม [Sum(d.Amount * d.Price)] ของแต่ละ salesRep
orderby คือการจัดเรียง ในที่นี้ orderby t descending หมายถึงเรียงจำนวนเงินรวมจากมากไปน้อย ถ้าไม่เติม descending จะเรียงจากน้อยไปมาก นอกจากนี้ เราสามารถเรียงต่อเนื่องได้ด้วย Comma เช่น orderby firstname, lastname หมายความว่า เรียงตามชื่อ และนามสกุล
สุดท้าย select คือการนำเสนอค่า ในที่นี้ select new { SalesRep = g.Key, Total = t } หมายถึงการสร้าง anonymous type ขึ้นมา โดยที่ type นี้มี Property คือ SalesRep และ Total
เวลาเอา query ไปใช้ก็ใช้ foreach ง่าย ๆ
foreach (var item in result) Console.WriteLine(item);
จบละครับ ถึงกึ๋นรึยังครับ ถ้าใครสนใจ Linq to Sql ต้องการเขียน Query Provider ใช้เอง (อย่ามัวแต่รอคนอื่นทำให้นะครับ จงเป็นผู้เริ่ม ) อ่านต่อ ที่นี่
แต่ถ้าชื่นชอบสไตล์ของ Functional Programming ใน Linq to Object คราวหน้ามาต่อกับ การเอา Linq ไปใช้กับ FP ผสานพลัง Double Action!! เร็ว ๆ นี้
* อ่านจบแล้ว กรุณา Comment ด้วยครับ เพื่อเป็น Feedback และเป็นกำลังใจ * บทความนี้คัดลอกได้ แต่ต้องมี Link หรือ Url กลับมาที่หน้านี้ และเมื่อคัดลอกแล้วกรุณา Comment แจ้งด้วยครับ
Create Date : 18 สิงหาคม 2551 |
Last Update : 23 สิงหาคม 2551 15:46:17 น. |
|
36 comments
|
Counter : 14874 Pageviews. |
|
|
ไม่มีความรู้เรื่อง C# เลย แต่ขอบคุณมากค่ะ