วันนี้ผมมีเทคนิคเด็ดๆ มาฝากกันครับ
ถ้าผมต้องการจะคืนค่า เป็น int จำนวน 20 ค่า ออกมาจากฟังก์ชั่น ให้มีค่าตั้งแต่ 1 ถึง 20 ผมก็จะทำแบบนี้ครับ ถ้าเป็นเมื่อก่อนนู้น
public int[] Get20Ints()
{
int[] toReturn = new int[20];
for (int i = 0; i < 20; i++)
{
toReturn
= i + 1;
}
return toReturn;
}
หรือ บางคน ก้ออาจจะเขียนแบบนี้
public List<int> Get20Ints()
{
List<int> toReturn = new List<int>();
for (int i = 0; i < 20; i++)
{
toReturn.Add(i + 1);
}
return toReturn;
}
ซึ่งแน่นอนว่า ก้อได้ผลเหมือนกันครับ ไม่มีใครผิด และถ้าผมต้องการจะนำค่าจากทั้งสองฟังก์ฃั่นไปใช้งาน ก็ต้องทำแบบนี้ ถูกไหมครับ
public void Test()
{
int[] data = this.Get20Ints();
for (int i = 0; i < data.Length; i++)
{
Console.WriteLine( data
);
}
}
แต่เมื่อวันเวลาผ่านไป โลกเราก้อซับซ้อนมากขึ้น ผมเกิดอยากจะมีความต้องการที่จะ เอาค่า 20 ค่า นั้น มาประมวลผลต่อเล็กน้อย เพื่อจะแบ่งให้กับ 2 ฟังก์ชั่น เอาไปใช้งานต่อ โดยฟังก์ชั่นนึง อยากได้แบบเดิม อีกฟังก์ชั่นนึง อยากได้แบบใหม่ เอาละสิ ผมก้อต้องแก้โค๊ดเล็กน้อย เพื่อ Copy ข้อมูลออกมาแก้ไข
public void Test()
{
int[] data = this.Get20Ints();
int[] processedData = new int[data.Length];
for (int i = 0; i < data.Length; i++)
{
processedData
= data
* 2;
}
// send data to f1()
f1(data);
// send processedData to f2()
f2(processedData);
}
แล้วถ้าเกิดว่า ใน f2 เกิด อยากจะทำอะไรต่อ ก่อนจะคืนค่าออกมา โดยที่ไม่ไปเปลี่ยนข้อมูลที่รับเข้ามา... ก้อต้อง
public int[] f2( int[] data )
{
int[] toReturn = new int[data.Length];
for (int i = 0; i < toReturn.Length; i++)
{
toReturn
= i + 1;
}
return toReturn;
}
สรุปคือ กว่าจะจบเรื่องได้ เราต้องสร้าง Array กันถึง 3 รอบเลยทีเดียว ถ้าคิดเป็นรอบ ก้อ 60 รอบเลย และถ้าคิดเอาง่ายๆ ว่า int กินแรม 4 ไบต์ต่อตัว ก้อใช้ไป 240 ไบต์...
แล้วถ้าเกิดว่า ข้อมูลที่เราจะต้องประมวลผล มันขนาดหลายๆ พัน หลายล้านอย่างละ?!?!?! จะต้องใช้แรม กะ CPU เยอะขนาดไหน?? ผมมีวีธีทีดีกว่านั้นครับ ถ้าไม่มี ผมก้อคงไม่มาบอก จิงมะ วิธีทีว่าคือ Yield Return ครับ
การใช้ Yield Return นี่ ดูไม่มีอะไรซับซ้อน แต่ว่า มันมีประโยชน์อย่างมากครับ โดยเฉพาะอย่างยิ่ง พวกเราที่ต้องทำงานกับเครื่อง PocketPC ที่มีแรมน้อยๆ CPU ก้อไม่เร็วเท่า Desktop ลองมาดูการนำเอา Yield Return มาใช้กับฟังก์ชั่นแรกกันครับ
public IEnumerable<int> Get20Ints()
{
for (int i = 0; i < 20; i++)
{
yield return i + 1;
}
}
อย่างเพิ่งงงครับ ใช่แล้วครับ ไม่ต้องจองเมโมรี่ ไม่ต้องสร้าง Array คืนค่ากันดื้อๆ แบบนี้เลยครับ แล้วถ้าผมจะเอามาใช้งาน ก้อใช้ร่วมกับ For Each ครับ
public void Test()
{
foreach (var item in this.Get20Ints())
{
Console.WriteLine( item );
}
}
แล้วถ้าเป็น VS2008 ตัว Compiler มันจะอนุมาน Type ให้เราได้ด้วย สังเกตว่า ผมใช้เป็น var แทนที่จะเป็น int ครับ
ทีนี้แล้ว ถ้าผมจะส่งค่าต่อให้กับฟังก์ชั่นต่อไปละ? ก้อทำได้ง่ายๆ แบบนี้
public void Test()
{
IEnumerable<int> result = this.Get20Ints();
IEnumerable<int> result2 = this.f2(result);
}
ถ้าเกิดว่า จะเอาต่อกันเป็นทอดๆ ยิ่งสนุกเลยละครับ
public IEnumerable<int> Get20Ints()
{
for (int i = 0; i < 20; i++)
{
yield return i;
}
}
public IEnumerable<int> f2(IEnumerable<int> input)
{
foreach (var item in input)
{
return item * 2;
}
}
public IEnumerable<int> f3(IEnumerable<int> input)
{
foreach (var item in input)
{
return item * 6;
}
}
public void Test()
{
IEnumerable<int> finalResult = this.f3(this.f2(this.Get20Ints()));
}
แล้วที่เด็ดกว่านั้นคือ การใช้ Yield Return นั้น จะเป็นเทคนิค Lazy Evaluation ครับ นั่นคือ ฟังก์ฃั่นทั้งสาม จะยังไม่ทำงาน จนกว่า ผมจะเรียกดูค่าใน finalResult ครับ ถ้าลอง Debug ดู ก็จะพบว่า โค๊ดจะรันผ่านไปเลยอย่างหน้าตาเฉย เพราะว่าผมไม่ได้ foreach นั่นเอง
แล้วถ้าจะเอาไปใช้จริงๆ จะได้ใช้ตอนไหนบ้าง?
อันนี้ก้อแล้วแต่จังหวะครับ แต่จังหวะนึงที่ผมชอบใช้คือตอนเขียน Data Layer ที่จะเป็นตัวที่ต่อฐานข้อมูลครับ แทนที่ผมจะใช้การคืนค่าจากฟังก์ชั่นนั้นมาเป็น DataTable หรือว่า เป็น List ของคลาสที่หน้าตาเหมือนกับข้อมูลในตาราง ผมก็อาจจะทำอะไรคล้ายๆ แบบนี้แทนครับ (ถ้าขยัน ก้อจะคืนค่าเป็นคลาสแทน)
private IEnumerable<SqlCeDataReader> ReadDatabase()
{
SqlCeConnection conn = new SqlCeConnection( ... );
conn.Open();
SqlCeCommand cmd = conn.CreateCommand();
cmd.CommandText = "SELECT * FROM [...]";
SqlCeDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
yield return reader;
}
reader.Dispose();
cmd.Dispose();
conn.Dispose();
GC.WaitForPendingFinalizers();
File.Delete(sdfFile);
}
แบบนี้ ก็คือ Return ตัว Reader ออกไปให้คนอ่าน Read เอาตามชอบใจเลย แทนที่เราจะต้องมานั่งเขียนอะไรกันซ้ำๆ เราก้อสามารถใช้ foreach( SqlCeDataReader reader in ReaderDatabase) ไปเอา DataReader มาได้เลย พร้อมทั้งหลังจากที่เราใช้เสร็จ มันก้อจะโดน Dispose พร้อมตัด Connection ให้เราอีกเสร็จสรรพ
เจ๋งดีมั๊ยล่ะครับ นี่เป็นแค่ตัวอย่างหนึ่งเท่านั้นนะครับ ยังมีโอกาสให้ได้ใช้อีกเพียบ
โอกาสหน้าเจอกันครับ