ref struct ใน .NET Core 3 และ C# 8

จากบทความ มีอะไรใหม่ใน .NET Core 3 และ C# 8 : Stackalloc ซ้อนนิพจน์ ได้พูดถึง คุณสมบัติ Span<T> ไว้ และได้บอกว่ามันคือ ref struct ในหัวข้อนี้จะขออธิบายรายละเอียดเกี่ยวกับ ref struct โดยย่อพอเข้าใจ
หน้าปกบทความ ref struct ใน .NET Core 3 และ C# 8
ทักษะ (ระบุได้หลายทักษะ)

ref struct ใน .NET Core 3 และ C# 8 

สำหรับบทความนี้ จะอธิบายรายละเอียดเกี่ยวกับ ref struct โดยย่อพอเข้าใจ ซึ่งเป็น คุณสมบัติหนึ่งที่ถูกปรับปรุงใหม่ของภาษา C#  8  และ .NET Core 3   

ref struct

จากบทความ มีอะไรใหม่ใน .NET Core 3 และ C# 8 : Stackalloc ซ้อนนิพจน์  ได้พูดถึง คุณสมบัติ Span<T> ไว้ และได้บอกว่ามันคือ ref struct ในหัวข้อนี้จะขออธิบายรายละเอียดเกี่ยวกับ ref struct โดยย่อพอเข้าใจ
แต่ก่อนที่จะพูดถึง ref struct ขอทบทวนเรื่องการจัดการเรื่องหน่วยความจำในการเก็บค่าต่าง ๆ จะมี Value Type และ Reference Type
 

  • Value Type จะมีการจัดการแบบ LIFO (Last In – First Out) หรือการจัดการแบบ Stack เรียงเป็นชั้น ๆ สิ่งที่มาก่อนจะอยู่ล่างสุด จึงจะต้องออกที่หลัง จะใช้เก็บค่าที่มีขนาดตายตัว เช่น int, double, char, struct เป็นต้น
  • Reference Type จะมีการจัดเก็บค่าแบบอ้างอิง ซึ่งค่าจะเก็บไว้ใน Heap โดยไม่ได้ใช้พื้นที่ติดต่อกัน จะใช้เก็บค่าที่เป็น object เช่น class, interface, delegate, object, string

คนโค้ดบางคนเข้าใจว่าเนื่องจาก struct เป็น Value Type  ดังนั้นอย่างไรก็ต้องอยู่ในstackไม่มีทางที่จะอยู่ในHeapได้
แต่ในความเป็นจริงไม่ใช่เช่นนั้น สมมุติเรามี object  foo ซึ่งเป็น object ธรรมดาที่อยู่ใน Heap หาก foo มีสมาชิกหนึ่งตัวเป็น struct สมาชิกตัวนี้ก็จะอยู่ใน Heap ด้วยเช่นกัน
ในสถานะการณ์ทั่ว ๆ ไป struct จะอยู่ใน stack แต่ในบางสถานะการณ์ struct จะอยู่ใน Heap อาทิ
 

  • เมื่อถูก box
  • เมื่อเป็น field ของ class
  • เมื่อเป็นหน่วยของ array
  • เมื่อเป็นค่าตัวแปรแบบ value type

ถ้าเราต้องการกำหนดให้ struct อยู่ใน stack เราจะต้องใส่ตัวขยาย ref ไว้หน้านิยาม struct  object
อะไรก็ตามที่มาจาก ref struct จะถูกจัดสรรหน่วยความจำไว้ภายใน stack ไม่ใช่ใน Heap ที่ถูกดูแลจัดการ  

object ที่เป็น ref struct มีข้อจำกัดหลายอย่างที่ป้องกันไม่ให้มันถูกเลื่อนขั้นกลายไปเป็น Heap อาทิ

  • เราไม่อาจ box มันได้ (boxing การแปลงชนิดข้อมูลจาก object ให้เป็น Type ที่เฉพาะเจาะจงหรือกลับกัน) เราจะนำมันไปกำหนดให้แก่ตัวแปรที่มี Type เป็น  object  dynamic หรือ interphaseใด ๆ ไม่ได้ มันจะมี field แบบ Reference Type ไม่ได้ และจะใช้งานข้ามขอบเขตระหว่าง await กับ yield ไม่ได้ ยิ่งไปกว่านั้นการแปลงชนิดข้อมูลระหว่าง  Equals(Object) กับ GetHashCode ก็ไม่ได้และจะทำให้เกิด exception แบบ NotSupportedException

เนื่องจาก Span<T> เป็น object แบบ ref struct ดังนั้นจึงไม่เหมาะที่จะนำมันไปใช้ในสถานะการณ์ที่ต้องเก็บค่าอ้างอิงไว้ภายในบัฟเฟอร์ที่อยู่ภายใน Heap นี่เป็นเรื่องที่ต้องระวัง
ยกตัวอย่างเช่นเมื่อเป็นงานประจำที่เรียก method แบบไม่ผสานจังหวะ ในกรณนี้ให้ใช้ Type  System.Memory<T> และ System.ReadOnlyMemory<T> เป็นการทดแทน
เมื่อจัดสรรความจำไว้ในstackเท่านั้นจะมีผลให้ ref struct มีข้อจำกัดต่าง ๆ ดังต่อไปนี้
 

  •  box ไม่ได้: เราไม่สามารถนำ ref struct ไปกำหนดค่าให้แก่ตัวแปรแบบ object ได้
  • interphase: เราจะใส่อิมพลีเมนท์ของ interphaseใน ref struct ไม่ได้
  • สมาชิก: เราจะประกาศสมาชิกของคลาสหรือ struct ธรรมดาเป็น ref struct ไม่ได้
  • method ไม่ผสานจังหวะ: เราจะประกาศตัวแปรท้องถิ่นของ method แบบไม่ผสานจังหวะเป็น Type แบบ ref struct ไม่ได้
  • Iterator: เราจะประกาศตัวแปรท้องถิ่นของ iterator เป็น Type แบบ ref struct ไม่ได้
  • Lambda : เราจะประกาศตัวแปรแบบ ref struct ในนิพจน์ Lambda และฟังก์ชันท้องถิ่นไม่ได้

รูปที่ 1 ตัวอย่างโค้ดแสดงนิยาม ref struct

ตัวอย่างโค้ดแสดงนิยาม ref struct

นิยาม ref struct

รูปที่ 1 แสดงโค้ดนิยาม ref struct ชื่อ MyRefStruct โปรดสังเกตว่ามี method  Equals, GetHashCode และ ToString โดยทั้ง 3 method นี้ Override method ชื่อเดียวกันของ Base class ใน Namespaces  System
ต่อไปนี้เป็นคำอธิบายโค้ดแต่ละบรรทัด
 

  • บรรทัดที่  8, 9 ประกาศ field สมาชิกของ struct นี้สองตัว
  • บรรทัดที่ 12, 13 นิยาม method  Equals ที่ overwrite method ชื่อเดียวกันของ Base class ใน Namespaces  System
  • บรรทัดที่ 16, 17 นิยาม method  Equals ที่โอเวอร์ไรด์ method ชื่อเดียวกันของ Base class ใน Namespaces  System
  • บรรทัดที่ 20, 21 นิยาม method  Equals ที่โอเวอร์ไรด์ method ชื่อเดียวกันของ Base class ใน Namespaces  System
  • บรรทัดที่ 27 สร้าง instance ของ MyRefStruct โดยกำหนดค่าเป็นดีฟอลท์
  • บรรทัดที่ 28 ลองกำหนดค่าให้แก่สมาชิกของ instance ของ MyRefStruct
  • บรรทัดที่ 29 แสดงค่าสมาชิก
  • บรรทัดที่ 31 เมื่อลองประกาศ property ให้แก่คลาส Program ให้มี Type เป็น MyRefStruct จะพบว่าเกิด error ขณะ compile พราะเราจะประกาศสมาชิกของคลาสเป็น ref struct ไม่ได้

รูปที่ 2 ตัวอย่างโค้ดแสดงนิยาม ref struct ที่เป็น readonly

ตัวอย่างโค้ดแสดงนิยาม ref struct ที่เป็น readonly

ref struct ที่เป็น readonly

ถ้าต้องการทำ ref struct ให้เป็นแบบอ่านได้เท่านั้นก็สามารถทำได้โดยใส่ตัวเพิ่มขยาย readonly ไว้หน้านิยาม struct ตามที่เห็นในโค้ดตัวอย่างรูปที่ 2 โปรดสังเกตสิ่งต่าง ๆ ดังต่อไปนี้
 

  • บรรทัดที่ 24-43 คือนิยาม struct ชื่อ StudentStruct ซึ่งมีโค้ดคล้าย ๆ ตัวอย่างก่อนหน้านี้ นั่นคือมี method  Equals, GetHashCode และ ToString โดยสาม method นี้โอเวอร์ไรด์ method ชื่อเดียวกันของ Base class ใน Namespaces  System
  • บรรทัดที่ 26, 27 ประกาศ Field โปรดสังเกตว่าเราจำเป็นต้องใส่ตัวเพิ่มขยาย readonly ไว้หน้าการประกาศ field ด้วย หากไม่ใส่จะเกิด error ขณะ compile
  • บรรทัดที่ 29-33 นิยาม method  constructor  ภายในมีโค้ดที่จะนำค่าที่ได้รับจากโค้ดภายนอกมากำหนดให้กับ field ทั้งสอง
  • บรรทัดที่ 48 สร้าง object จาก struct โดยใช้ตัวกระทำ new เพื่ออ้างถึง constructor และส่งค่าไปให้ object  นี่เป็นวิธีเดียวที่เราจะสามารถกำหนดค่าให้แก่ field ของ object ได้
  • บรรทัดที่ 49 หากเราพยายามกำหนดค่าให้แก่ field ของ object จะเกิด error
  • บรรทัดที่ 52 เมื่อลองประกาศ property ให้แก่ class test ให้มี Type เป็น StudentStruct จะพบว่าเกิด error ขณะ compile พราะเราจะประกาศสมาชิกของ class เป็น readonly ref struct ไม่ได้เช่นเดียวกันกับโค้ดตัวอย่างก่อนหน้านี้

รูปที่ 3 ภาษา IL ของ ref struct

รูปที่ 3 ภาษา IL ของ ref struct

ภาษา IL ของ ref struct

หากเราตรวจสอบดูโค้ดภาษา IL ที่เป็นผลจากการ build โปรแกรมตัวอย่างก่อนหน้านี้จะพบว่ามีโค้ดเป็นอย่างที่เห็นในรูปที่ 3 ต่อไปนี้เป็นคำอธิบายโค้ดโดยย่อ (ตัดมาเฉพาะส่วนที่เป็นนิยาม ref struct เพียงบางส่วน)
 

  • บรรทัดที่ 3, 6 compilerใส่ attribute  IsByRefLike และ Obsolete เพื่อให้สามารถเข้ากันได้ย้อนหลังกับ C# เวอร์ชั่นก่อนหน้าที่ ถ้ามีการอ้างอิงไปยัง library เก่าที่มี assembly ใด ๆ ที่ภายในมี ref struct  Attribute  Obsolete จะทำหน้าที่ปิดกั้นไม่ให้โค้ด compile
  • บรรทัดที่ 23, 24 ส่วนประกาศ field ทั้งสองตัว
  • บรรทัดที่  26 ส่วนหัวของนิยาม method
  • บรรทัดที่ 34 กำหนดขนาดของ stack  method นี้เริ่มที่ตำแหน่ง address เสมือน 0x2090 และมีขนาด 16 หน่วย
  • บรรทัดที่ 36 คำสั่ง nop คือไม่ต้องทำอะไรใส่ไว้เพื่อให้ไม่เป็นค่าสุ่มในหน่วยความจำตำแหน่งนั้น
  • บรรทัดที่ 38-40 คำสั่งเพื่อการนำค่าจาก parameter  value1 ไปใช้เพื่อกำหนดให้ค่าให้แก่ field  MyIntValue1
  • บรรทัดที่ 42-44 คำสั่งเพื่อการนำค่าจาก parameter  value2 ไปใช้เพื่อกำหนดให้ค่าให้แก่ field  MyIntValue2
  • บรรทัดที่ 46 จบการทำงาน คำสั่ง ret ทำหน้าที่ย้ายการทำงานกลับไปยังโปรแกรมที่เรียก method นี้

รูปที่ 4 ตัวอย่างโค้ดแสดงการใช้งาน ReadOnlySpan<T>

ตัวอย่างโค้ดแสดงการใช้งาน ReadOnlySpan<T>

การใช้งาน ReadOnlySpan<T>

เมื่อมี ref struct แล้วก็สามารถจองหน่วยความจำให้เป็นพื้นที่ต่อเนื่องเป็นผืนเดียวกันหมดได้โดยใช้ Span<T> และหรือ Memory<T> ที่แตกต่างกันเล็กน้อย
โดย Span<T> ทำหน้าที่เป็นตัวแทนพื้นที่ต่อเนื่องในหน่วยความจำ ใช้งานได้หลากหลายสารพัดประโยชน์กับ stack และ Heap ทั้งแบบที่จัดการและแบบที่ไม่ได้ถูกจัดการ

ส่วน Memory<T> ไส้ในคือ Span<T> และมีการเพิ่มขยายเปลี่ยนแปลงคุณสมบัติบางอย่างเพื่อให้สามารถทำงานกับ method ที่ไม่ผสานจังหวะ (async) สำหรับการทำงานทั้งแบบที่เน้นการใช้ซีพียูและที่เน้นการใช้ I/O

รูปที่ 4 แสดงตัวอย่างโค้ดแสดงการใช้งาน ReadOnlySpan<T> ซึ่งเหมือน Span<T> แต่อ่านได้เท่านั้น เขียนไม่ได้ ต่อไปนี้เป็นคำอธิบายโค้ดโดยสังเขป
 

  • บรรทัดที่ 7-24 นิยาม method  TrimStart สาทิตการใช้งาน ReadOnlySpan<T>  method นี้ทำหน้าที่เอาช่องว่างหน้า string ออก
  • บรรทัดที่ 8  method นี้รับ parameter หนึ่งตัวมี Type เป็น ReadOnlySpan<char>
  • บรรทัดที่ 10-13 ถ้า text มีแต่ความว่างเปล่าไม่ต้องทำอะไร ให้จบการทำงานของ method
  • บรรทัดที่ 15 ประกาศตัวแปรเพื่อใช้เป็นดรรชนี
  • บรรทัดที่ 16 ประกาศตัวแปรเพื่อให้เก็บตัวอักษรหนึ่งตัว
  • บรรทัดที่ 18-22 วนการทำงานไปเรื่อยตราบเท่าที่ตัวอักษรในตำแหน่งดรรชนีเป็นค่าเคาะวรรค
  • บรรทัดที่ 23  method  Slice เป็น method ของ ReadOnlySpan ทำหน้าที่ตัดส่วนของหน่วยความจำเริ่มที่ตำแหน่งดรรชนี
  • บรรทัดที่ 27  string ที่จะใช้ทดสอบมีค่าเคาะวรรคอยู่ข้างหน้า
  • บรรทัดที่  28 แสดงการเรียกใช้ method  TrimStart

เราอาจนำ Span<T> และ ReadOnlySpan<T> ไปใช้สร้างเป็นบัฟเฟอร์ที่มีพื้นที่ต่อเนื่องกันได้ดี เพราะมีขนาดกะทัดรัดและมีน้ำหนักเบาใช้งานได้ทั้งกับหน่วยความจำแบบที่ถูกจัดการและที่ไม่ถูกจัดการ แต่มีข้อจำกัด คือต้องทำงานอยู่บนstackเท่านั้น จะทำงานในHeapไม่ได้ หากต้องการกลไกแบบ Span<T> และ ReadOnlySpan<T> แต่สามารถงานในHeapได้เราอาจใช้คลาสในกลุ่ม Memory<T> ที่มีอยู่ทั้งหมดสี่ตัวได้แก่ Memory<T>, ReadOnlyMemory<T>, IMemoryOwner<T>, และ MemoryPool<T>

จากนิยาม ref struct, ref struct ที่เป็น readonly, ภาษา IL ของ ref struct, และการใช้งาน ReadOnlySpan<T> ในบทความนี้ น่าจะทำให้ท่านผู้อ่านนำไปประยุกต์ใช้งาน ref struct ได้อย่างเข้าใจมากยิ่งขึ้น