พระคัมภีร์การใช้คำสั่ง LINQ

🤔 คำสั่งของ LINQ ที่ได้ใช้บ่อยๆมีไรบ้างนะ

บทความนี้เป็นบทความที่แยกออกมาจากเรื่อง LINQ ซึ่งเป็นหนึ่งในคำสั่งเทพเจ้าของสาย .NET ซึ่งมันจะทำให้ developer ทำงานได้สบายลงแบบฝุดๆ ดังนั้นใครยังไม่รู้เรื่อง LINQ ให้กลับไปอ่านบทความนี้ก่อนเน่อ Saladpuk - LINQ 101

Filtering Data

Where - เป็นการเลือกเอาเฉพาะข้อมูลที่เราสนใจออกมา เช่น มี data source เป็นเลข 1~100 แล้วต้องการเอาเฉพาะเลขที่ 5 และ 7 หารลงตัวออกมา ก็จะเขียนออกมาได้เป็นแบบนี้

var collection = Enumerable.Range(1, 100);
var qry = collection.Where(it => it % 5 == 0 && it % 7 == 0);
// ผลลัพท์: { 35, 70 }

Projection Operations

Select - เป็นการเลือกว่า data source ที่เราไปดึงข้อมูลมา เราจะดัดแปลงแก้ไข หรือ เลือกเอาเฉพาะข้อมูลบางส่วนออกมาใช้

เช่นมี collection เป็นเลข 1~5 ตอนที่เราจะเอามาทำงานด้วยเราจะแก้ให้มันถูก คูณด้วย 10 ก่อนค่อยเอามาใช้งาน ก็จะเขียนได้แบบนี้

var collection = new int[] { 1, 2, 3, 4, 5 };
var qry = collection.Select(it => it * 10);
// ผลลัพท์: { 10, 20, 30, 40, 50 }

หรือ จะให้มันเปลี่ยนเป็นข้อมูลอีกประเภทนึงเลยก็ได้

var qry = collection.Select(it => new Student{ Id = it });
public class Student
{
public int Id { get; set; }
}

ส่วนถ้าข้อมูลใน data source มันวุ่นวายเกินไป เราก็สามารถเลือกแค่บางส่วนของมันมาใช้ก็ได้นะ เช่น เราอยากได้แค่ Name ที่อยู่ใน collection มาใช้เท่านั้น ก็เขียนเป็นแบบนี้ได้

var collection = new[]
{
new { Id = 1, Name = "A", Age = 10 },
new { Id = 2, Name = "B", Age = 15 },
new { Id = 3, Name = "C", Age = 20 },
};
var qry = collection.Select(it => it.Name);
// ผลลัพท์: { "A", "B", "C" }

SelectMany - เป็นการเลือกเข้าไปถึงหน่วยย่อยของ collection ที่ซ้อนภายใน collection อีกทีนึง

var collection = new[]
{
new [] { 1, 2, 3, 4 },
new [] { 5, 6, 7, 8 },
};
var qry = collection.SelectMany(it => it);

แนะนำให้อ่าน คำสั่ง SelectMany สำหรับคนที่พึ่งหัดใช้ LINQ อาจจะ งงๆ หน่อยแต่ถ้าเราได้ทำงานร่วมกับพวก collection ซ้อน collection แล้วล่ะก็ควรจะทำความเข้าใจมันเอาไว้นะ ซึ่งอ่านได้จากลิงค์นี้เลย Microsoft document - Projection Operations

Element Operations

ในบางทีเราอยากจะทำงานกับข้อมูลแค่ตัวใดตัวหนึ่งหรือส่วนหนึ่งที่อยู่ใน collection เราก็สามารถใช้คำสั่งที่อยู่ด้านล่างได้ เช่น เรามี data source ที่มีข้อมูลเป็นเลข 1~100

var collection = Enumerable.Range(1, 100);

First - เอาเฉพาะตัวแรกออกมา

var result = collection.First();
// ผลลัพท์: { 1 }

FirstOrDefault - เหมือนกับ First ทุกประการ ต่างกันแค่ถ้ามันดึงค่าออกมาไม่ได้มันจะส่งค่า default ของ data type นั้นๆกลับมา

var result = collection.FirstOrDefault();
// ผลลัพท์: { 1 }

Last - เอาเฉพาะตัวสุดท้ายออกมา

var result = collection.Last();
// ผลลัพท์: { 100 }

LastOrDefault - เหมือนกับ Last ทุกประการ ต่างกันแค่ถ้ามันดึงค่าออกมาไม่ได้มันจะส่งค่า default ของ data type นั้นๆกลับมา

var result = collection.LastOrDefault();
// ผลลัพท์: { 100 }

ElementAt - เป็นการดึงค่าที่อยู่ใน index ที่กำหนดออกมา

var result = collection.ElementAt(3);
// ผลลัพท์: { 4 }

ElementAtOrDefault - เหมือนกับ ElementAt ทุกประการ ต่างกันแค่ถ้ามันดึงค่าออกมาไม่ได้มันจะส่งค่า default ของ data type นั้นๆกลับมา

var result = collection.ElementAtOrDefault(9999);
// ผลลัพท์: { 0 }

อันตราย ถ้า data source เป็น collection ว่าง แล้วไปใช้คำสั่งพวก First, Last, ElementAt มันจะทำให้เกิด Exception ได้ครับ ดังนั้นโดยปรกติผมจะแนะนำให้ใช้คำสั่ง FirstOrDefault, LastOrDefault, ElementAtOrDefault แทนมากกว่า เพราะค่า overhead ในการจัดการกับ error มันสูงกว่าครับ

Partitioning Data

เวลาที่เราทำงานกับ data source ปริมาณมากๆ เราสามารถที่จะทำการแบ่งข้อมูลออกเป็นส่วนๆ เพื่อให้ง่ายในการทำงานได้ เช่น มี collection ตัวเลข 1~100 อยู่ตามด้านล่าง

var collection = Enumerable.Range(1, 100);

Take - เป็นการสั่งให้ดึงข้อมูลจาก data source ออกมาเท่าที่เรากำหนดไว้ เช่น เราอยากดึงข้อมูลมาแค่ 5 ตัวแรกก่อน เราก็จะเขียนได้ว่า

var qry = collection.Take(5);
// ผลลัพท์: { 1, 2, 3, 4, 5 }

TakeLast - เหมือนกับ Take แต่จะดึงมาจากด้านหลังสุด เช่น อยากจะดึงข้อมูล 5 ตัวจากด้านหลังสุดออกมา

var qry = collection.TakeLast(5);
// ผลลัพท์: { 96, 97, 98, 99, 100 }

TakeWhile - เป็นการสั่งให้มันดึงข้อมูลจาก data source ออกมาเรื่อยๆจนกว่าจะเจอตัวแรกที่ทำให้เงื่อนไขไม่เป็นจริง เช่น ให้ดึงมาเรื่อยๆถ้าเลขที่ดึงมามันยังน้อยกว่า 8

var qry = collection.TakeWhile(it => it < 8);
// ผลลัพท์: { 1, 2, 3, 4, 5, 6, 7 }

Skip - สั่งให้ข้ามข้อมูลเท่ากับที่เรากำหนด เช่น เราต้องการข้ามข้อมูล 4 ตัวแรกไป

var qry = collection.Skip(4);
// ผลลัพท์: { 5, 6, 7 ... 100 }

SkipLast - เหมือนกับ Skip ต่างกันแค่มันจะข้ามเฉพาะตัวด้านหลังสุด เช่น อยากจะข้ามข้อมูล 4 ตัวสุดท้ายไป

var qry = collection.SkipLast(4);
// ผลลัพท์: { 1, 2, 3 ... 96 }

SkipWhile - เป็นการสั่งให้มันข้ามข้อมูลไปเรื่อยๆ ถ้าเงื่อนไขยังเป็นจริงอยู่ และจะหยุดข้ามเมื่อเจอข้อมูลตัวแรกที่ไม่ตรงเงื่อนไข เช่น อยากจะข้ามไปเรื่อยๆจนกว่าจะเจอตัวแรกที่มากกว่า 50

var qry = collection.SkipWhile(it => it < 50);
// ผลลัพท์: { 50, 51, 52 ... 100 }

Set Operations

เราสามารถทำงานกับ data source ที่เป็น 2 กลุ่มให้มาทำงานร่วมกันได้ 3 แบบคือ

เช่นเรามีข้อมูลกลุ่ม a กับกลุ่ม b เป็นแบบนี้

var a = new[] { 1, 2, 3, 4, 5 };
var b = new[] { 4, 5, 6, 7, 8 };

Intersect - ตามรูปเลยคือ เอาเฉพาะที่มันเหมือนกันออกมา

var intersect = a.Intersect(b);
// ผลลัพท์: { 4, 5 }

Union - ตามรูปเลยคือ เอาทั้งสองกลุ่มมารวมกัน

var union = a.Union(b);
// ผลลัพท์: { 1, 2, 3, 4, 5, 6, 7, 8 }

Except - ตามรูปเลยคือ เอาเฉพาะของที่ไม่ซ้ำกับอีกกลุ่มออกมา

var except = a.Except(b);
// ผลลัพท์: { 1, 2, 3 }

Distinct - เป็นการตัดตัวซ้ำทิ้ง

var collection = new[] { 1, 1, 2, 2, 3, 4, 4, 3 };
var qry = collection.Distinct();
// ผลลัพท์: { 1, 2, 3, 4 }

Sorting Data

การเรียงลำดับเราทำได้ 3 แบบ น้อยไปมาก มากไปน้อย และ กลับด้านข้อมูล เช่นเรามี data source เป็นแบบนี้

var collection = new int[] { 7, 5, 2, 6, 4, 1, 3 };

OrderBy - เรียงลำดับจากน้อยไปมาก หรือถ้าเป็นตัวอักษรจะเป็นการเรียงจาก a~Z

var ascending = collection.OrderBy(it => it);
// ผลลัพท์: { 1, 2, 3, 4, 5, 6, 7 }

OrderByDescending - เรียงลำดับจากมากไปน้อย

var descending = collection.OrderByDescending(it => it);
// ผลลัพท์: { 7, 6, 5, 4, 3, 2, 1 }

Reverse - เรียงลำดับแบบกลับด้าน ขวาไปซ้าย แทน

var reverse = collection.Reverse();
// ผลลัพท์: { 3, 1, 4, 6, 2, 5, 7 }

ThenBy และ ThenByDescending - แต่ถ้าข้อมูลมีความซับซ้อนมากขึ้น เราสามารถกำหนดความสำคัญในการเรียงลำดับได้ด้วย เช่น เรียงลำดับจากคะแนนน้อยไปมาก แต่ถ้าคะแนนเท่ากันให้เรียงจากชื่อตามลำดับตัวอักษรก็จะเขียนแบบนี้ได้

var collection = new[]
{
new { Score = 7, Name = "B" },
new { Score = 3, Name = "A" },
new { Score = 7, Name = "A" },
new { Score = 4, Name = "A" },
new { Score = 3, Name = "C" },
};
// น้อยไปมาก และ ตามลำดับตัวอักษร
var ascending = collection
.OrderBy(it => it.Score)
.ThenBy(it => it.Name);
// มากไปน้อย และ ตามลำดับตัวอักษร
var descending = collection
.OrderByDescending(it => it)
.ThenBy(it => it.Name);

Quantifier Operations

เราสามารถหาผลลัพท์จากข้อมูลใน collection ได้เช่น มีบางตัวไหม? หรือ ทุกตัวเป็นแบบนี้ไหม? อาจจะฟังแล้ว งงๆ ไปดูตัวอย่างเลยดีกว่า โดยสมมุติว่าผมมี data source เป็นแบบนี้

var collection = new int[] { 2, 4, 6, 8, 10 };

Any - ถามว่ามีซักตัวไหมที่เป็นแบบนี้ เช่น อยากรู้ว่ามีซักตัวไหมใน collection ที่มีค่ามากกว่า 9 ก็สามารถเขียนเป็น

var any = collection.Any(it => it > 9); // true

All - ถามว่าทุกตัวเป็นแบบนี้หรือเปล่า เช่น อยากเช็คว่าทุกตัวใน collection มากกว่า 5 หรือเปล่า

var all = collection.All(it => it > 5); // false

Contains - ถามว่าภายในนั้นมีตัวนี้อยู่หรือเปล่า เช่น collection นั้นมีเลข 8 อยู่ในนั้นหรือเปล่า

var contain = collection.Contains(8); // true

Grouping Data

GroupBy - สั่งให้มันจัดกลุ่มของข้อมูลได้ เช่น มี collection ของคนหลายๆคน แล้วเราอยากให้จัดกลุ่มคนตามอายุ เราก็จะเขียนได้ว่า

var collection = new[]
{
new { Name = "A", Age = 15 },
new { Name = "B", Age = 7 },
new { Name = "C", Age = 7 },
new { Name = "D", Age = 15 },
new { Name = "E", Age = 9 },
};
var qry = collection.GroupBy(it => it.Age);
/* ผลลัพท์
15
{ Name = A, Age = 15 }
{ Name = D, Age = 15 }
7
{ Name = B, Age = 7 }
{ Name = C, Age = 7 }
9
{ Name = E, Age = 9 }
*/

Generation Operations

ถ้าเราต้องการสร้างข้อมูลที่เป็น collection ขึ้นมาแบบง่ายๆ เราก็สามารถใช้ LINQ ช่วยสร้างได้

Range - สร้างชุดตัวเลขออกมา เช่น อยากได้ collection ตัวเลขตั้งแต่ 1 ถึง 100

var qry = Enumerable.Range(1, 100);
// ผลลัพท์: { 1, 2, 3 ... 100 }

Empty - สร้าง collection ว่างออกมา เช่น เราอยากได้ collection ของตัวเลข แต่ไม่ต้องมีข้อมูลอะไรอยู่ข้างในนะ

var qry = Enumerable.Empty<int>();
// ผลลัพท์: { }

DefaultIfEmpty - ถ้าเราต้องไปทำงานกับ collection ตัวเลขซักตัว แต่ถ้า collection นั้นมันเป็นค่าว่าง เราจะกำหนดค่า 9 ให้มันไปใช้แทน

var collection = Enumerable.Empty<int>();
var qry = collection.DefaultIfEmpty(9);
// ผลลัพท์: { 9 }

Repeat - สร้างชุดข้อมูลซ้ำๆกันออกมา เช่น อยากได้ collection เลข 5 ซ้ำกัน 3 ตัว ก็เขียนแบบนี้ได้

var qry = Enumerable.Repeat(5, 3);
// ผลลัพท์: { 5, 5, 5 }

Converting Data Types

เราสามารถแปลง data source ของเราจาก data type นึงไปยังอีก data type นึงก็ได้นะ เช่น มีข้อมูล collection เลข 1~5 ตามนี้

var collection = new[] { 1, 2, 3, 4, 5 };

AsEnumerable - แปลงให้มันกลับมาเป็น IEnumerable<T> เอาไว้ช่วยแปลงจาก collection อะไรก็ตามให้กลับมาสู่ base class ของกลุ่ม collection

var qry = collection.AsEnumerable();

AsQueryable - แปลงให้คำสั่งทั้งหมดยังเป็นแค่ Query เท่านั้น ซึ่งใช้ได้ดีตอนที่ทำงานร่วมกับ database เพราะเราจะได้ส่งแต่คำสั่งไปประมวลผลที่ database เท่านั้นไม่ได้ส่งข้อมูลปริมาณมหาศาลกลับมาถล่มที่ client

var qry = collection.AsQueryable();

Cast - แปลงข้อมูลจาก data source ให้กลายเป็น data type ที่กำหนด

var collection = new[]
{
new Dog { Id = 1, OwnerName = "Saladpuk" },
};
IEnumerable<Animal> qry = collection.Cast<Animal>();
public class Animal
{
public int Id { get; set; }
}
public class Dog : Animal
{
public string OwnerName { get; set; }
}

OfType - เลือกเอาเฉพาะ data type ที่ตรงกับที่กำหนด

var collection = new object[]
{
new Dog { OwnerName = "Saladpuk" },
new Cat { IsFriendly = false },
};
var qry = collection.OfType<Dog>();
// ผลลัพท์: [ { OwnerName = 'Saladpuk' } ]
public class Dog
{
public string OwnerName { get; set; }
}
public class Cat
{
public bool IsFriendly { get; set; }
}

ToArray - แปลงให้ collection นั้นๆกลายเป็น Array

var qry = Enumerable.Range(1, 100);
int[] result = qry.ToArray();

ToList - แปลงให้ collection นั้นๆกลายเป็น List

var qry = Enumerable.Range(1, 100);
List<int> result = qry.ToList();

ToDictionary - แปลงให้ collection นั้นๆกลายเป็น Dictionary<K, V> เช่นทำการจัดกลุ่มว่าใครเรียนอยู่ห้องไหนบ้าง แล้วทำการไปสร้างเป็น dictionary

var collection = new[]
{
new { Id = 1, ClassRoom = "A", Name = "Saladpuk" },
new { Id = 2, ClassRoom = "B", Name = "Thaksin" },
new { Id = 3, ClassRoom = "C", Name = "Prayut" },
new { Id = 4, ClassRoom = "B", Name = "Yingluck" },
new { Id = 5, ClassRoom = "C", Name = "Abhisit" },
};
var result = collection
.GroupBy(it => it.ClassRoom)
.ToDictionary(it => it.Key, it => it);
/* ผลลัพท์
A
{ Id = 1, ClassRoom = A, Name = Saladpuk }
B
{ Id = 2, ClassRoom = B, Name = Thaksin }
{ Id = 4, ClassRoom = B, Name = Yingluck }
C
{ Id = 3, ClassRoom = C, Name = Prayut }
{ Id = 5, ClassRoom = C, Name = Abhisit }
*/

Concatenation Operations

Concate - เป็นการเอา 2 collection มาต่อกันแบบดื้อๆเลย

var a = new[] { 1, 2, 3, 4, 5 };
var b = new[] { 4, 5, 6, 7, 8 };
var qry = a.Concat(b);
// ผลลัพท์: { 1, 2, 3, 4, 5, 4, 5, 6, 7, 8 }

Aggregation Operations

เป็นกลุ่มคำสั่งที่ได้ผลลัพท์กลับมาเลย และเป็นการทำงานแบบ Imperative เช่นมี data source เป็นเลข 1~10 ตามนี้

var collection = Enumerable.Range(1, 10);

Sum - หาผลรวม

var result = collection.Sum();
// ผลลัพท์: 55

Average - หาค่าเฉลี่ย

var result = collection.Average();
// ผลลัพท์: 5.5

Max - หาค่าสูงสุด

var result = collection.Max();
// ผลลัพท์: 10

Min - หาค่าต่ำสุด

var result = collection.Min();
// ผลลัพท์: 1

Count - นับว่าภายใน data source มีข้อมูลอยู่ทั้งหมดเท่าไหร่

var result = collection.Count();
// ผลลัพท์: 10

Aggregate - นำข้อมูลทั้ง collection มาดำเนินการแบบต่อเนื่องกัน

var result = collection.Aggregate((a, b) => a * b);
// ผลลัพท์: 3628800

แนะนำให้อ่าน คำสั่ง Aggregate ถ้าเราใช้เป็นจริงๆมันทรงพลังมากเลยนะ ลองศึกษาเพิ่มเติมได้จากลิงค์นี้เบย Microsoft document - Aggregation

บทสรุป Deferred vs Imperative

จากคำสั่งทั้งหมดที่เขียนมาเป็นตัวอย่าง สุดท้ายการทำงานของมันก็จะตกมาอยู่ในกลุ่ม 3 กลุ่มนั่นเองคือ

  • ทำงานโดยทันที Immediate

  • ไม่ทำงานจนกว่าจะเรียกใช้ Deferred

    • ดึงข้อมูลทั้งหมดมาก่อนค่อยทำงาน Non-Streaming

    • ค่อยทะยอยดึงข้อมูลมาเรื่อยๆ Streaming

Operators

Return Type

Immediate

Deferred Streaming

Deferred Non-Streaming

Aggregate

TSource

X

All

Boolean

X

Any

Boolean

X

AsEnumerable

IEnumerable<T>

X

Average

Single numeric value

X

Cast

IEnumerable<T>

X

Concat

IEnumerable<T>

X

Contains

Boolean

X

Count

Int32

X

DefaultIfEmpty

IEnumerable<T>

X

Distinct

IEnumerable<T>

X

ElementAt

TSource

X

ElementAtOrDefault

TSource

X

Empty

IEnumerable<T>

X

Except

IEnumerable<T>

X

X

First

TSource

X

FirstOrDefault

TSource

X

GroupBy

IEnumerable<T>

X

GroupJoin

IEnumerable<T>

X

X

Intersect

IEnumerable<T>

X

X

Join

IEnumerable<T>

X

X

Last

TSource

X

LastOrDefault

TSource

X

LongCount

Int64

X

Max

Single numeric value, TSource, or TResult

X

Min

Single numeric value, TSource, or TResult

X

OfType

IEnumerable<T>

X

OrderBy

IOrderedEnumerable<TElement>

X

OrderByDescending

IOrderedEnumerable<TElement>

X

Range

IEnumerable<T>

X

Repeat

IEnumerable<T>

X

Reverse

IEnumerable<T>

X

Select

IEnumerable<T>

X

SelectMany

IEnumerable<T>

X

SequenceEqual

Boolean

X

Single

TSource

X

SingleOrDefault

TSource

X

Skip

IEnumerable<T>

X

SkipWhile

IEnumerable<T>

X

Sum

Single numeric value

X

Take

IEnumerable<T>

X

TakeWhile

IEnumerable<T>

X

ThenBy

IOrderedEnumerable<TElement>

X

ThenByDescending

IOrderedEnumerable<TElement>

X

ToArray

TSource array

X

ToDictionary

Dictionary<TKey,TValue>

X

ToList

IList<T>

X

ToLookup

ILookup<TKey,TElement>

X

Union

IEnumerable<T>

X

Where

IEnumerable<T>

X