เพิ่งได้อ่านบทความสรุป What's new in .NET 11? ของทีม PVS-Studio มาแล้วรู้สึกว่ารอบนี้ Microsoft ทำการบ้านมาดีมาก คือไม่ได้เน้นแค่เพิ่มฟีเจอร์หวือหวา แต่เข้าไปแก้จุดที่คนเขียน C# รำคาญใจในชีวิตประจำวันจริงๆ
เลยหยิบมาสรุปแบบสั้นๆ เหมือนเพื่อนเล่าให้ฟังว่าถ้าจะขยับไป .NET 11 (ซึ่งเป็นรุ่น STS ที่ซัพพอร์ตยาว 2 ปีเต็ม) มีอะไรที่เราจะได้ใช้แน่ๆ และมีเรื่องไหนต้องระวังก่อนอัปเกรดบ้าง
ปกติเวลาเราเขียน async / await คอมไพเลอร์ C# จะแอบไปสร้างโค้ด State Machine ก้อนใหญ่ๆ ไว้เบื้องหลัง ซึ่งบางทีก็ทำให้เปลืองหน่วยความจำ และเวลาไล่ Stack Trace หรือกด Step Over ตอนดีบักก็มักจะกระโดดเข้าไปในโค้ดภายในของระบบจนตาลาย
ใน .NET 11 เขาย้ายหน้าที่การจัดการ async ลงไปให้ Runtime ดูแลโดยตรงแทน (ตัว Core Libraries ของ .NET 11 เองก็คอมไพล์ด้วยโหมดนี้เรียบร้อยแล้ว)
ผลที่ได้แบบเห็นชัดๆ คือ:
เร็วขึ้นและกินแรมน้อยลง: JIT รู้จักโค้ด async โดยตรง ทำให้รีไซเคิลออบเจกต์ได้ และทำ Tiered Compilation กับเมธอดที่ถูกเรียกบ่อยๆ ได้ดีขึ้น
ดีบักง่ายขึ้นมาก: เวลาดู StackTrace จะเห็นเฉพาะลำดับการเรียกฟังก์ชันของเราจริงๆ ไม่มีโค้ดขยะของระบบมาคั่น และกดข้าม await ได้เนียนเหมือนโค้ดปกติ
ถ้าอยากเปิดใช้ในโปรเจกต์ตัวเอง แค่เติมบรรทัดนี้ลงในไฟล์ .csproj (ไม่ต้องเปิด EnablePreviewFeatures แล้ว):
xml copy < PropertyGroup >
< Features >runtime-async=on</ Features >
</ PropertyGroup >
ใครเคยใช้คลาส Process เพื่อสั่งรันคำสั่ง CLI (เช่น git หรือสคริปต์ข้างนอก) จะรู้ว่าเมื่อก่อนต้องเขียนโค้ดยาวเหยียด ต้องคอยผูก OutputDataReceived หรือเปิดอ่าน Stream เอง
ใน .NET 11 มี Process.RunAndCaptureTextAsync มาให้แล้ว จบในบรรทัดเดียวแบบนี้เลย:
csharp copy ProcessTextOutput result = await Process. RunAndCaptureTextAsync (
"git" ,
[ "status" , "--porcelain" ]
);
Console. WriteLine (result.StandardOutput);
Console. WriteLine ( $"Exit code: { result . ExitStatus . ExitCode } " );
นอกจากนี้ยังมี Process.StartAndForget สำหรับสั่งรันทิ้งไว้โดยไม่ต้องรอให้จบอีกด้วย
หลังจากที่เวอร์ชันก่อนเพิ่ม LeftJoin กับ RightJoin มาให้ รอบนี้ LINQ เติม FullJoin เข้ามาจนครบตระกูลแล้ว มันจะดึงข้อมูลทั้งสองฝั่งมาจับคู่กัน ถ้าฝั่งไหนไม่มีคู่ก็จะเก็บไว้โดยใส่ค่าว่างให้อีกฝั่ง
แถมทั้ง Join , GroupJoin , LeftJoin , RightJoin และ FullJoin ตอนนี้เรียกใช้โดยไม่ต้องเขียนฟังก์ชัน resultSelector ปิดท้ายได้แล้ว เพราะมันคืนค่ากลับมาเป็น Tuple ให้แกะใช้ได้เลย:
csharp copy var joined = products. FullJoin (categories, p => p.Category, c => c.Name);
foreach ( var ( product , category ) in joined)
{
Console. WriteLine ( $" { product . Name ?? "(none)" } : { category . Description ?? "(none)" } " );
}
นอกจากสองตัวบน ยังมี API เล็กๆ น้อยๆ ที่เพิ่มเข้ามาแล้วมีประโยชน์มาก:
Async Validation (IAsyncValidatableObject / AsyncValidationAttribute ): เมื่อก่อนเวลาทำ Data Annotations Validation เราเช็กแบบ Async ไม่ได้ (เช่น ต้องยิงไปถาม Database ว่าอีเมลซ้ำไหม) ตอนนี้มี Validator.ValidateObjectAsync และรองรับการเช็ก Options ตอนเปิดแอปผ่าน IAsyncStartupValidator แล้ว
TryParsePartial อ่านตัวเลขเท่าที่อ่านไหว: ปกติถ้าสตริงเป็น "123;789" แล้วใช้ double.TryParse มันจะพังและคืนค่า 0 ทันที แต่ถ้าใช้ double.TryParsePartial มันจะอ่านเลข 123 ออกมาให้ พร้อมบอกว่าอ่านไปกี่ตัวอักษร (consumed = 3 ) เหมาะกับงานแกะไฟล์ข้อมูลดิบมาก
รองรับบีบอัดไฟล์แบบ Zstandard (ZstandardStream ) ในตัว: ไม่ต้องหาไลบรารีแยกแล้ว แถมตัวอ่านไฟล์ ZIP ตอนนี้เช็กค่า CRC32 ให้อัตโนมัติ และเปิดไฟล์ ZIP ที่ติดรหัสผ่านได้ด้วย
Stream แบบไม่ต้องก๊อปปี้ข้อมูลซ้ำ: เพิ่ม ReadOnlyMemoryStream , WritableMemoryStream , ReadOnlySequenceStream และ StringStream ช่วยให้ส่ง Buffer หรือ String เข้า API ที่รับ Stream ได้เลยโดยไม่สร้างขยะให้ GC ตามเก็บ
System.Text.Json ฉลาดขึ้น: รองรับ union type ของ C# 15, เขียนไฟล์แบบ JSON Lines (JSONL) ได้ในตัว, มี JsonNamingPolicy.PascalCase และมี InferClosedTypePolymorphism = true ที่ช่วยเดาคลาสลูกของ closed record ให้อัตโนมัติโดยไม่ต้องนั่งแปะ [JsonDerivedType] ทีละตัว:
csharp copy [ JsonPolymorphic ( InferClosedTypePolymorphism = true )]
closed record Shape ;
record Circle ( double Radius ) : Shape ;
record Square ( double Length ) : Shape ;
ชนิดตัวเลขทศนิยมมาตรฐาน IEEE 754-2019: เพิ่ม Decimal32 , Decimal64 , Decimal128 ใน System.Numerics ที่เก็บค่าฐาน 10 เป๊ะเหมือน decimal (เช่น 0.1 ไม่เพี้ยน) แต่รองรับ NaN และ Infinity สำหรับคุยกับฐานข้อมูลหรือภาษาอื่น
dotnet CLI คอมไพล์เป็น NativeAOT แล้ว: เวลาพิมพ์คำสั่งพื้นฐานอย่าง dotnet --version , dotnet --info หรือเรียกเครื่องมือเสริมอย่าง dotnet ef มันจะทำงานแบบ Native ทันที ไม่ต้องรอโหลดรันไทม์ชุดใหญ่ก่อน
รันไฟล์ .cs เดี่ยวๆ แยกไฟล์ได้แล้ว: ต่อยอดจาก .NET 10 ที่รันไฟล์ .cs เดี่ยวๆ ได้ ตอนนี้มีคำสั่ง #:include ให้ดึงไฟล์ .cs อื่นหรือไฟล์ .dll มาร่วมใช้ได้โดยยังไม่ต้องสร้างไฟล์โปรเจกต์
ส่ง Environment Variable ตอนรันได้ง่ายๆ: ใช้แฟล็ก dotnet run -e KEY=VALUE ได้เลย และ dotnet test มีโหมดทดลองที่เลือกรันเฉพาะเทสต์ที่กระทบจากโค้ดที่เราเพิ่งแก้
เรื่องเดียวที่ต้องเช็กก่อนอัปเกรดคือ .NET 11 ขยับสเปกขั้นต่ำของซีพียูฝั่ง x86/x64 ขึ้นเป็น x86-64-v2 (จากเดิม v1 ):
ถ้าเอาไปรันบนเครื่องหรือ VM รุ่นเก่ามากๆ ที่ไม่รองรับชุดคำสั่ง x86-64-v2 แอปจะ เปิดไม่ติดเลย
ส่วนอิมเมจแบบ ReadyToRun บน Windows และ Linux ขยับไปเล็งที่ x86-64-v3 (AVX2 , BMI2 , FMA ) ถ้าซีพียูไม่ถึง v3 แอปยังรันได้ปกติ แต่ตอนเปิดโปรแกรมจะช้าลงนิดหน่อยเพราะต้องให้ JIT คอมไพล์ใหม่แทน
ถ้าเครื่องเซิร์ฟเวอร์ไม่ได้เก่าเกิน 10 ปี การขยับไป .NET 11 กับ C# 15 รอบนี้คุ้มมาก ได้ทั้งความเร็วจาก JIT และ Runtime Async ฟรีๆ แถมได้ของใช้อย่าง Process.RunAndCaptureTextAsync , FullJoin และ AsyncValidation ที่ช่วยลดโค้ดรกๆ ในโปรเจกต์ลงไปได้เยอะเลย