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
รูปที่ 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 ไว้หน้านิยาม 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
ภาษา 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>
เมื่อมี 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 ได้อย่างเข้าใจมากยิ่งขึ้น