ref struct ใน .NET Core 3 และ C# 8
ref struct ใน .NET Core 3 และ C# 8
สำหรับบทความนี้ จะอธิบายรายละเอียดเกี่ยวกับ ref struct โดยย่อพอเข้าใจ ซึ่งเป็น คุณสมบัติหนึ่งที่ถูกปรับปรุงใหม่ของภาษา C# 8 และ .NET Core 3ref 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
แต่ในความเป็นจริงไม่ใช่เช่นนั้น สมมุติเรามี object foo ซึ่งเป็น object ธรรมดาที่อยู่ใน Heap หาก foo มีสมาชิกหนึ่งตัวเป็น struct สมาชิกตัวนี้ก็จะอยู่ใน Heap ด้วยเช่นกัน
ในสถานะการณ์ทั่ว ๆ ไป struct จะอยู่ใน stack แต่ในบางสถานะการณ์ struct จะอยู่ใน Heap อาทิ
- เมื่อถูก box
- เมื่อเป็น field ของ class
- เมื่อเป็นหน่วยของ array
- เมื่อเป็นค่าตัวแปรแบบ value type
อะไรก็ตามที่มาจาก 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
ยกตัวอย่างเช่นเมื่อเป็นงานประจำที่เรียก 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 และฟังก์ชันท้องถิ่นไม่ได้
นิยาม 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 ไม่ได้
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 ไม่ได้เช่นเดียวกันกับโค้ดตัวอย่างก่อนหน้านี้
ภาษา 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 นี้
การใช้งาน 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
จากนิยาม ref struct, ref struct ที่เป็น readonly, ภาษา IL ของ ref struct, และการใช้งาน ReadOnlySpan<T> ในบทความนี้ น่าจะทำให้ท่านผู้อ่านนำไปประยุกต์ใช้งาน ref struct ได้อย่างเข้าใจมากยิ่งขึ้น
Tags: