มีอะไรใหม่ใน .NET Core 2 และ C# 7 : Pattern Matching ด้วยคำสั่ง Switch

การตรวจสอบเพื่อการจับคู่รูปแบบ (Pattern Matching: PM)
ในภาษา C# เวอร์ชัน 7.0 การตรวจสอบเพื่อการจับคู่รูปแบบ (Pattern Matching: PM) ด้วยคำสั่ง if และ switch ได้รับการพัฒนาให้ดียิ่งขึ้น ยืดหยุ่นกว่าเดิม เขียนโค้ดได้สะดวกขึ้น
แต่เดิมการทำ PM ด้วยหลักวัตถุวิธีเราจะสร้างคลาสฐานเป็นแบบ “abstract” จากนั้นจะใช้กรรมวิธีสืบคุณสมบัติเป็นคลาสลูกหลาย ๆ แบบตามต้องการ
ส่วนเก็บข้อมูลและโค้ดเพื่อจัดการข้อมูลจะถูกผนึกไว้เป็นหน่วยเดียวกัน ซึ่งเป็นการ “เชื่อมแน่น” (tight coupling)
ในกรณีที่เราต้องการการ “เชื่อมหลวม” (loose coupling) เราจะแยก ส่วนเก็บข้อมูลและโค้ดเพื่อจัดการข้อมูลออกจากกัน
จากนั้นทำ PM ด้วยคำสั่ง if และ switch ซึ่งหากมีรูปแบบจำนวนวนมาก โค้ดจะยืดยาวเยิ่นเย้อ
แต่เดิมการทำ PM ด้วยหลักวัตถุวิธีเราจะสร้างคลาสฐานเป็นแบบ “abstract” จากนั้นจะใช้กรรมวิธีสืบคุณสมบัติเป็นคลาสลูกหลาย ๆ แบบตามต้องการ
ส่วนเก็บข้อมูลและโค้ดเพื่อจัดการข้อมูลจะถูกผนึกไว้เป็นหน่วยเดียวกัน ซึ่งเป็นการ “เชื่อมแน่น” (tight coupling)
ในกรณีที่เราต้องการการ “เชื่อมหลวม” (loose coupling) เราจะแยก ส่วนเก็บข้อมูลและโค้ดเพื่อจัดการข้อมูลออกจากกัน
จากนั้นทำ PM ด้วยคำสั่ง if และ switch ซึ่งหากมีรูปแบบจำนวนวนมาก โค้ดจะยืดยาวเยิ่นเย้อ
รูปที่ 1

ยกตัวอย่างเช่นหากเราต้องการสร้าง object ที่มีรูปทรงเรขาคณิต อย่าง สี่เหลี่ยม วงกลม สี่เหลี่ยมผืนผ้า และสามเหลี่ยม
เราจะนิยามคลาสแบบ abstract เพื่อใช้ทำหน้าที่เป็นเบสคลาสที่เป็นรูปทรงกลาง
จากนั้นนิยามคลาสลูกให้สืบคุณสมบัติจาก base class เป็นรูปทรงต่าง ๆ หนึ่งคลาสต่อหนึ่งรูปทรง
และใส่นิยาม method จัดการกับข้อมูลของรูปทรงที่แตกต่างกันนั้น ๆ ในแต่ละคลาส
แต่ถ้าเราต้องการแยกโค้ดที่จัดการข้อมูลออกจากคลาส
เราจะนิยามคลาสรูปทรงโดยไม่สืบคุณสมบัติจาก base class
บรรทัด 16-24 คือนิยามคลาส Square ทำหน้าที่ใช้สร้างออพเจ็กต์สี่เหลี่ยม
บรรทัด 26-34 คือนิยามคลาส Circle ทำหน้าที่ใช้สร้างออพเจ็กต์วงกลม
รูปที่ 2
เราจะนิยามคลาสแบบ abstract เพื่อใช้ทำหน้าที่เป็นเบสคลาสที่เป็นรูปทรงกลาง
จากนั้นนิยามคลาสลูกให้สืบคุณสมบัติจาก base class เป็นรูปทรงต่าง ๆ หนึ่งคลาสต่อหนึ่งรูปทรง
และใส่นิยาม method จัดการกับข้อมูลของรูปทรงที่แตกต่างกันนั้น ๆ ในแต่ละคลาส
แต่ถ้าเราต้องการแยกโค้ดที่จัดการข้อมูลออกจากคลาส
เราจะนิยามคลาสรูปทรงโดยไม่สืบคุณสมบัติจาก base class
บรรทัด 16-24 คือนิยามคลาส Square ทำหน้าที่ใช้สร้างออพเจ็กต์สี่เหลี่ยม
บรรทัด 26-34 คือนิยามคลาส Circle ทำหน้าที่ใช้สร้างออพเจ็กต์วงกลม
รูปที่ 2

บรรทัด 36-46 คือนิยาม structure Rectangle ทำหน้าที่ใช้สร้าง object สี่เหลี่ยมผืนผ้า
บรรทัด 48-54 คือนิยามคลาส Triangle ทำหน้าที่ใช้สร้าง object สามเหลี่ยม
โปรดสังเกตว่าแต่ละคลาสมีความเป็นเอกเทศ คือ ไม่ได้สืบคุณสมบัติร่วมใด ๆ และไม่ได้อ้างอิงถึงกัน
แต่ละคลาสมีแต่ส่วนเก็บข้อมูล ไม่มีโค้ดจัดการข้อมูล ส่วนที่เป็นโค้ดคงมีเพียง method constructorเท่านั้น
รูปที่ 3
บรรทัด 48-54 คือนิยามคลาส Triangle ทำหน้าที่ใช้สร้าง object สามเหลี่ยม
โปรดสังเกตว่าแต่ละคลาสมีความเป็นเอกเทศ คือ ไม่ได้สืบคุณสมบัติร่วมใด ๆ และไม่ได้อ้างอิงถึงกัน
แต่ละคลาสมีแต่ส่วนเก็บข้อมูล ไม่มีโค้ดจัดการข้อมูล ส่วนที่เป็นโค้ดคงมีเพียง method constructorเท่านั้น
รูปที่ 3

วิธีการเดิม (ก่อน C# 7.0 ) เป็นโค้ดเพื่อการแยกแยะคัดกรองว่า object อะไรเป็นobject อะไร
method ComputeArea เป็นตัวอย่าง method ซึ่งทำหน้าที่คำนวณพื้นที่ของรูปทรง
แต่เนื่องจากรูปทรงแต่ละแบบต้องการการคำนวณที่แตกต่างกัน
ยกตัวอย่างเช่น ถ้าเป็นสี่เหลี่ยมก็คำนวณโดยนำด้านมาคูณด้าน แต่ถ้าเป็นวงกลมเราจะคำนวณพื้นที่ได้โดยรัศมีคูณรัศมีคูณค่าพาย
ดังนั้นเราจึงต้องตรวจสอบก่อนว่ามันคืออะไร ซึ่งสามารถทำได้โดยใช้คำสั่ง if ร่วมกับนิพจน์บูลีนที่มีตัวกระทำ is (ดูบรรทัด 65 และ 70)
เมื่อตรวจพบแล้วยังต้องแคส (อันบ็อกซ์) เสียก่อนจึงจะนำไปประมวลผลได้
รูปที่ 4
method ComputeArea เป็นตัวอย่าง method ซึ่งทำหน้าที่คำนวณพื้นที่ของรูปทรง
แต่เนื่องจากรูปทรงแต่ละแบบต้องการการคำนวณที่แตกต่างกัน
ยกตัวอย่างเช่น ถ้าเป็นสี่เหลี่ยมก็คำนวณโดยนำด้านมาคูณด้าน แต่ถ้าเป็นวงกลมเราจะคำนวณพื้นที่ได้โดยรัศมีคูณรัศมีคูณค่าพาย
ดังนั้นเราจึงต้องตรวจสอบก่อนว่ามันคืออะไร ซึ่งสามารถทำได้โดยใช้คำสั่ง if ร่วมกับนิพจน์บูลีนที่มีตัวกระทำ is (ดูบรรทัด 65 และ 70)
เมื่อตรวจพบแล้วยังต้องแคส (อันบ็อกซ์) เสียก่อนจึงจะนำไปประมวลผลได้
รูปที่ 4

ตัวอย่างโค้ดการตรวจสอบเพื่อการจับคู่รูปแบบ แบบใหม่ใน C# 7.0 จะเห็นว่าโค้ดคล้ายเดิม
แต่คราวนี้เราได้ตัวแปร s c และ r โดยไม่ต้องแคส (อันบ็อกซ์) เสียก่อน
โดยตัวแปร s c และ r จะเกิดก็ต่อเมื่อเงื่อนไขของนิพจน์บูลีนเป็นจริง
ตัวแปรแต่ละตัวจะไม่เกิดถ้านิพจน์บูลีนเป็นเท็จ และตัวแปรแต่ละตัวจะมีสโคปอยู่ภายใน if ของมันเอง
ยกเว้น s ที่กินสโคปในระดับเมธอด ComputeAreaModernIs
รูปที่ 5
แต่คราวนี้เราได้ตัวแปร s c และ r โดยไม่ต้องแคส (อันบ็อกซ์) เสียก่อน
โดยตัวแปร s c และ r จะเกิดก็ต่อเมื่อเงื่อนไขของนิพจน์บูลีนเป็นจริง
ตัวแปรแต่ละตัวจะไม่เกิดถ้านิพจน์บูลีนเป็นเท็จ และตัวแปรแต่ละตัวจะมีสโคปอยู่ภายใน if ของมันเอง
ยกเว้น s ที่กินสโคปในระดับเมธอด ComputeAreaModernIs
รูปที่ 5

ในกรณีที่เป็นการคัดกรองจำนวนมาก
หากเขียนโดยใช้คำสั่ง if จะมีโค้ดรุงรังเกินไป เราจึงจะทดแทนด้วยคำสั่ง switch
ยกตัวอย่างใน รูปที่ 5 Method GenerateMessage เป็น Method ที่มี input parameter จำนวนไม่แน่นอน
หากเราต้องการตรวจสอบจำนวนของพารามิเตอร์ เราสามารถทำได้โดยใช้คำสั่ง switch
โดยพิจารณาจาก property Length ของตัวแปร parts
ส่วนคำสั่ง case มีข้อจำกัดว่าจะต้องมีค่าเป็นชนิดข้อมูลแบบ “เลขจำนวนเต็ม” หรือ “string”
และต้องเป็นตัวคงค่าเท่านั้น (เป็นตัวแปรไม่ได้)
รูปที่ 6
หากเขียนโดยใช้คำสั่ง if จะมีโค้ดรุงรังเกินไป เราจึงจะทดแทนด้วยคำสั่ง switch
ยกตัวอย่างใน รูปที่ 5 Method GenerateMessage เป็น Method ที่มี input parameter จำนวนไม่แน่นอน
หากเราต้องการตรวจสอบจำนวนของพารามิเตอร์ เราสามารถทำได้โดยใช้คำสั่ง switch
โดยพิจารณาจาก property Length ของตัวแปร parts
ส่วนคำสั่ง case มีข้อจำกัดว่าจะต้องมีค่าเป็นชนิดข้อมูลแบบ “เลขจำนวนเต็ม” หรือ “string”
และต้องเป็นตัวคงค่าเท่านั้น (เป็นตัวแปรไม่ได้)
รูปที่ 6

case แบบใหม่และ when
ข้อจำกัดของคำสั่ง switch / case ที่กล่าวถึงในย่อหน้าบนถูกยกออกไปใน C# 7.0
นั่นคือต่อแต่นี้เราสามารถใช้ชนิดข้อมูลอะไรก็ได้
เป็นตัวคงค่าหรือตัวแปรก็ได้
จะเป็นชนิดข้อมูลแบบ “Reference Type”
หรือ “Value Type” ก็ได้ ร่วมกับคำสั่ง case
บรรทัดที่ 114, 116 เป็นตัวอย่างการใช้คำสั่ง case แบบใหม่ที่ทำงานกับ Reference Type (Square และ Circle เป็นคลาส) ให้ผลลัพธ์เป็นตัวแปร s และ c
บรรทัดที่ 118 เป็นตัวอย่างการใช้คำสั่ง case แบบใหม่ที่ทำงานกับ Value Type (Rectangle เป็น structure) ให้ผลลัพธ์เป็นตัวแปร r
นั่นคือต่อแต่นี้เราสามารถใช้ชนิดข้อมูลอะไรก็ได้
เป็นตัวคงค่าหรือตัวแปรก็ได้
จะเป็นชนิดข้อมูลแบบ “Reference Type”
หรือ “Value Type” ก็ได้ ร่วมกับคำสั่ง case
บรรทัดที่ 114, 116 เป็นตัวอย่างการใช้คำสั่ง case แบบใหม่ที่ทำงานกับ Reference Type (Square และ Circle เป็นคลาส) ให้ผลลัพธ์เป็นตัวแปร s และ c
บรรทัดที่ 118 เป็นตัวอย่างการใช้คำสั่ง case แบบใหม่ที่ทำงานกับ Value Type (Rectangle เป็น structure) ให้ผลลัพธ์เป็นตัวแปร r
โปรดสังเกตว่า case แบบใหม่ไม่ต้องปิดก้อนด้วยคำสั่ง break อีกต่อไปแล้ว
เพราะการทำงานจะไม่ตกไปยัง case ถัดไปเหมือนแต่ก่อน
คุณสมบัติใหม่นี้ทำให้ลำดับของการเขียน case มีนัยสำคัญ
ไม่เหมือน case แบบเก่าที่เราสามารถสลับลำดับของมันไปมาอย่างไรก็ได้ตามใจชอบ จะไม่ทำให้เกิดความแตกต่างใด ๆ ในการทำงาน
เพราะการทำงานจะไม่ตกไปยัง case ถัดไปเหมือนแต่ก่อน
คุณสมบัติใหม่นี้ทำให้ลำดับของการเขียน case มีนัยสำคัญ
ไม่เหมือน case แบบเก่าที่เราสามารถสลับลำดับของมันไปมาอย่างไรก็ได้ตามใจชอบ จะไม่ทำให้เกิดความแตกต่างใด ๆ ในการทำงาน
รูปที่ 7

ความวิเศษของ case แบบใหม่ไม่ได้หมดเพียงแค่นั้น
เรายังสามารถใช้คำสั่ง when ร่วมกับคำสั่ง case ได้อีกด้วย ซึ่งช่วยให้การทำ PM มีโค้ดที่กระชับยิ่งไปขึ้นอีก
ยกตัวอย่างเช่น รูปที่ 7 บรรทัด 131
ในกรณีที่พบว่า shape เป็น Square จะเกิดตัวแปร s และ คำสั่ง when
จะตรวจสอบสืบไปได้อีกว่า property Side มีค่าเป็น 0 หรือไม่
หากใช่ ตัว run time จะทำงานแบบทัดที่ 133
เรายังสามารถใช้คำสั่ง when ร่วมกับคำสั่ง case ได้อีกด้วย ซึ่งช่วยให้การทำ PM มีโค้ดที่กระชับยิ่งไปขึ้นอีก
ยกตัวอย่างเช่น รูปที่ 7 บรรทัด 131
ในกรณีที่พบว่า shape เป็น Square จะเกิดตัวแปร s และ คำสั่ง when
จะตรวจสอบสืบไปได้อีกว่า property Side มีค่าเป็น 0 หรือไม่
หากใช่ ตัว run time จะทำงานแบบทัดที่ 133
โปรดสังเกตว่าโค้ดบรรทัด 131 กับ 135 เป็น case ที่ซ้ำกัน (s) ซึ่ง C# 7.0 อนุญาตให้ทำได้
ท่านอาจสงสัยว่าแล้วบรรทัดไหนจะทำงานก่อน คำตอบคือบรรทัด 131
นั่นคือการทำงานของ case จะไล่ไปตามลำดับของซอร์สโค้ด
และถ้าเงื่อนไขของบรรทัด 131 เป็นจริง case อื่น ๆ ที่เหลือจะไม่มีโอกาสได้ทำงานเลย
เราจึงไม่ต้องกังวลว่าจะเกิดการทำงานซ้ำซ้อน
รูปที่ 8
ท่านอาจสงสัยว่าแล้วบรรทัดไหนจะทำงานก่อน คำตอบคือบรรทัด 131
นั่นคือการทำงานของ case จะไล่ไปตามลำดับของซอร์สโค้ด
และถ้าเงื่อนไขของบรรทัด 131 เป็นจริง case อื่น ๆ ที่เหลือจะไม่มีโอกาสได้ทำงานเลย
เราจึงไม่ต้องกังวลว่าจะเกิดการทำงานซ้ำซ้อน
รูปที่ 8

เพิ่มการตรวจสอบ object อีกสองแบบ คือ Triangle และ Rectangle
โดย object สองแบบนี้มีนิพจน์เงื่อนไขของ when ที่ซับซ้อนขึ้น
คือใช้ตัวกระทำ || (or) ทำตรรกบูลีนกับนิพจน์ย่อยสองนิพจน์ (บรรทัด 152,153) ด้วย
รูปที่ 9
โดย object สองแบบนี้มีนิพจน์เงื่อนไขของ when ที่ซับซ้อนขึ้น
คือใช้ตัวกระทำ || (or) ทำตรรกบูลีนกับนิพจน์ย่อยสองนิพจน์ (บรรทัด 152,153) ด้วย
รูปที่ 9

ตัวอย่างการใช้คำสั่ง var เพื่อประกาศตัวแปรภายในคำสั่ง case
ในกรณีนี้เราใช้คำสั่ง switch กับตัวแปรแบบ string
บรรทัด 203, 206, 209 เป็นการเทียบกับตัวคงค่าที่กำหนดไว้กับ case ซึ่งเป็นซินแท็กซ์ที่มีมาแต่เดิม
บรรทัด 212 เป็นคุณสมบัติใหม่ของ C# 7.0 ที่เราสามารถใส่คำสั่ง var เพื่อ ประกาศตัวแปร o (ใช้ชื่ออื่นได้)
เพื่อทำหน้าที่เก็บค่าของตัวแปร shapeDescription จากนั้นสามารถใช้ o เพื่อตรวจสอบได้ว่าค่าของมันเป็น “” ( string ว่าง) หรือไม่
ในกรณีนี้เราใช้คำสั่ง switch กับตัวแปรแบบ string
บรรทัด 203, 206, 209 เป็นการเทียบกับตัวคงค่าที่กำหนดไว้กับ case ซึ่งเป็นซินแท็กซ์ที่มีมาแต่เดิม
บรรทัด 212 เป็นคุณสมบัติใหม่ของ C# 7.0 ที่เราสามารถใส่คำสั่ง var เพื่อ ประกาศตัวแปร o (ใช้ชื่ออื่นได้)
เพื่อทำหน้าที่เก็บค่าของตัวแปร shapeDescription จากนั้นสามารถใช้ o เพื่อตรวจสอบได้ว่าค่าของมันเป็น “” ( string ว่าง) หรือไม่
โดยสรุปแล้ว Pattern Matching ใน C# 7 นั้น จะช่วยให้การเขียน Code นั้นกระชับขึ้น
และจากข้อจำกัดเดิมของคำสั่ง switch/case ที่ไม่สามารถใช้ตัวแปรได้
แต่ใน C# 7 ทำให้เราสามารถใช้ชนิดข้อมูลอะไรก็ได้ เป็นตัวคงค่าหรือตัวแปรก็ได้ จะเป็นชนิดข้อมูลแบบ “Reference Type” หรือ “Value Type” ก็ได้ ร่วมกับคำสั่ง case
รวมถึงไม่ต้องกังวลว่าจะเกิดการทำงานซ้ำซ้อน เนื่องจากการทำงานของ case จะไล่ไปตามลำดับของซอร์สโค้ด หากเข้าเงื่อนไขใดแล้ว จะไม่ไปที่ case อื่น ๆ ถัดไป เหมือน case แบบเดิม
ซึ่งทำให้ case แบบใหม่ไม่ต้องปิดก้อนด้วยคำสั่ง break อีกต่อไปแล้ว เพราะการทำงานจะไม่ตกไปยัง case ถัดไปเหมือนแต่ก่อน
อย่างไร ทดลองใช้งาน Pattern Matching ด้วย switch/case แบบใหม่ ใน C# 7 ดูกันนะครับ