Skip to content

บทที่ 10 — Modern C#

← บทที่ 9 | สารบัญ | บทที่ 11: Async + Tasks →

C# ออก version ใหม่ทุกปี — บทนี้รวม feature สำคัญตั้งแต่ C# 9 ถึง C# 14 (.NET 10) ที่ทำให้ code modern อ่านง่ายขึ้นมาก

📌 C# version กับ .NET version ออกพร้อมกัน — C# 9 มากับ .NET 5, C# 12 มากับ .NET 8, C# 13 มากับ .NET 9, C# 14 มากับ .NET 10

สถานะ ณ เวลาเขียน: C# 13 (.NET 9) = GA (General Availability — ออกใช้งานจริงแล้ว); C# 14 (.NET 10) = RC (Release Candidate — รุ่นทดสอบก่อนออกจริง) — feature C# 14 เช่น field keyword และ partial property อาจเปลี่ยนก่อน release จริง ควรตรวจสอบเวอร์ชันล่าสุดก่อนใช้งานจริง

หัวข้อหลัก: record, pattern matching, nullable reference, primary constructor, collection expression, raw string, Span<T>, source generator และ feature C# 14 ใหม่ ๆ เช่น field keyword และ partial property


1. Record (C# 9+) — value-equality + immutable by default

record (C# 9+) คือ type สำหรับเก็บข้อมูลที่เขียนสั้น — ประกาศบรรทัดเดียวได้ property, constructor และ ToString ครบ

มาพร้อม value-equality (เทียบด้วยค่า ไม่ใช่ที่อยู่หน่วยความจำ) และเป็น immutable (แก้ไม่ได้) โดย default

เหมาะกับงานพวก DTO (Data Transfer Object = object ที่ใช้แค่ขนข้อมูลส่งไปมา ไม่มี logic) และ model (แบบจำลองข้อมูลในระบบ เช่น User, Product, Order):

csharp
public record Person(string Name, int Age);

var a = new Person("Alice", 30);
var b = new Person("Alice", 30);
Console.WriteLine(a == b);    // True — value equality

Console.WriteLine(a);
// > Person { Name = Alice, Age = 30 }  — auto ToString

// with expression — non-destructive mutation
var older = a with { Age = 31 };
Console.WriteLine(a.Age);    // 30 — a ไม่เปลี่ยน
Console.WriteLine(older.Age); // 31

// deconstruction
var (name, age) = a;

Record vs class:

classrecord (class)record struct
Equalityreferencevaluevalue
ToStringtype nameautoauto
with-expression
Immutable by default
Mutable allowed✅ (declare set)
Storageheap (ref)heap (ref)stack (value)

หมายเหตุตาราง: record เป็น immutable by default เฉพาะแบบ positional record (ที่ประกาศบรรทัดเดียวแบบ Person(string Name, int Age)) — properties ที่ได้เป็น init-only แก้ไม่ได้หลังสร้าง แต่ถ้า record ที่เขียน body เอง (ดูหัวข้อถัดไป) แล้วเปลี่ยน property ให้มี set แทน init ก็จะกลายเป็น mutable ได้เหมือนกัน

Record with body

csharp
public record Person(string Name, int Age)
{
    public bool IsAdult => Age >= 18;
    public string Greet() => $"Hi {Name}";

    // override init-only by adding set:
    // public int Age { get; set; } = Age;
}

Record struct (value type record)

csharp
public record struct Point(double X, double Y);
public readonly record struct Vec3(double X, double Y, double Z);

ใช้สำหรับ data ขนาดเล็ก ไม่ allocate heap


2. Pattern Matching (advanced — review บทที่ 3)

C# 9-11 ขยาย pattern อย่างมาก — ดูตัวอย่างใช้ใน real code:

csharp
// type ที่ใช้ในตัวอย่างด้านล่าง
record OrderItem(string Sku);
record Order(string Status, decimal Total, List<OrderItem> Items);
csharp
public string Categorize(Order o) => o switch
{
    // ⚠️ ลำดับสำคัญ! switch จะลองทีละ arm จากบนลงล่าง — เจอ match แรกก็หยุด
    // ถ้าวาง { Status: "pending" } ก่อน { Items: [] }
    // → pending order ที่ items ว่างจะถูกจัดเป็น "pending" ไม่ใช่ "empty"
    // จัด arm เฉพาะเจาะจง (specific) ไว้บน, arm กว้าง ๆ ไว้ล่าง
    { Items: [] }                            => "empty",
    { Status: "pending", Total: > 1000 }     => "high-value pending",
    { Status: "pending" }                    => "pending",
    { Items: [{ Sku: "GOLD" }, ..] }         => "starts with gold",
    { Items.Count: > 10 }                    => "bulk order",
    _                                        => "regular"
};

{ Items.Count: > 10 } = property pattern เข้าถึง nested property ตรง ๆ

⚠️ ถ้า Items เป็น array ใช้ .Length แทน .Count (List ใช้ .Count, array ใช้ .Length)


3. Nullable Reference Types (NRT) — recap + advanced

Nullable Reference Types (NRT = ชนิดอ้างอิงที่บอกได้ว่าเป็น null ได้หรือไม่) ใช้กับ reference type เช่น string, class (ไม่ใช่ value type เช่น int/bool) — ให้ compiler ช่วยจับ null ก่อน runtime ลดบั๊ก NullReferenceException

หลักการเบื้องต้น:

  • string = ห้ามเป็น null
  • string? = อนุญาตให้เป็น null
  • มี attribute (ป้ายกำกับโค้ดที่เขียนใน [วงเล็บเหลี่ยม] เพื่อบอกข้อมูลพิเศษให้ compiler/runtime) เช่น [NotNullWhen], [MaybeNull] ไว้บอก compiler ถึงเงื่อนไข null ที่ซับซ้อน (เช่น "ถ้า method นี้ return true แปลว่า out param ไม่ null")
  • runtime (ตอนรันโปรแกรม) — NRT ตรวจช่วงคอมไพล์เท่านั้น ตอนรันไม่ตรวจ:

Attribute สำหรับ nullable analysis

csharp
using System.Diagnostics.CodeAnalysis;

private static readonly Dictionary<string, string> _cache = new();

// NotNullWhen — บอก compiler ว่าถ้า return true → out param ไม่ null
public bool TryGet(string key, [NotNullWhen(true)] out string? value)
{
    return _cache.TryGetValue(key, out value);
}

if (TryGet("greeting", out var v))
    Console.WriteLine(v.Length);  // ไม่มี warning เพราะ NotNullWhen

อื่น ๆ (เป็น reference list — ไม่ต้องจำทั้งหมด แค่รู้ว่ามีไว้ใช้ตอนเจอ):

  • [NotNull] — บอก compiler ว่า out/return ค่านี้ไม่มีทาง null แม้ type จะเป็น ?
  • [MaybeNull] — บอกว่าค่านี้ "อาจจะ" null ได้ แม้ type จะไม่มี ?
  • [NotNullIfNotNull(nameof(x))] — บอกว่าถ้า parameter x ไม่ null ผลลัพธ์ก็จะไม่ null ตามไปด้วย
  • [MemberNotNull(nameof(_field))] — บอกว่าหลังเรียก method นี้แล้ว field _field จะไม่ null แน่นอน
  • [DisallowNull], [AllowNull] — บังคับ/อนุญาตให้ parameter รับ null ได้ ต่างจาก type ที่ประกาศไว้

NRT migration tip

csharp
// pragma เพื่อ silence warning ในส่วนที่ migrate ยังไม่เสร็จ
#nullable disable
// old code
#nullable enable

4. Primary Constructor (C# 12+)

💡 ตัวอย่างถัดไปใช้ IRepository<User> (จากบท 7) และ ILogger<UserService> (Microsoft.Extensions.Logging) ซึ่งได้รับผ่าน DI (Dependency Injection — สอนในส่วน dotnet/) — ตอนนี้แค่ดูโครงสร้าง primary constructor ได้เลย ไม่ต้องรันก็ได้

csharp
// class
public class UserService(IRepository<User> repo, ILogger<UserService> logger)
{
    public async Task<User?> Get(string id)
    {
        logger.LogInformation("get {Id}", id);
        return await repo.FindById(id);
    }
}

// struct — ตั้งชื่อหลีกเลี่ยง System.Range
public readonly struct IntRange(int Start, int End)
{
    public int Length => End - Start;
}

repo, logger, Start, End = ตัวแปร "primary ctor" — ใช้ได้ทุก method ใน class

⚠️ Pitfall: primary ctor parameter ไม่ใช่ field ปกติ — ทุก method ที่ใช้คือกำลัง capture (ยืมใช้) → ระวัง mutability (ความสามารถในการแก้ค่า)

ความจริงที่หลายคนไม่รู้: compiler สร้าง hidden field (ตัวแปร instance ที่ซ่อนอยู่เบื้องหลัง) ไว้เก็บค่าพวกนี้จริง ๆ ดังนั้นถ้า assign ทับใน method ค่าจะคงอยู่ (ไม่ใช่ "แค่ parameter" แบบ method ปกติ):

csharp
public class Counter(int start)
{
    public int Current => start;
    public void Inc() => start++;   // ✅ ทำได้! และค่าจะคงอยู่
}

var c = new Counter(0);
c.Inc(); c.Inc();
Console.WriteLine(c.Current);   // 2

ดังนั้นถ้าอยากให้ "อ่านอย่างเดียว" ให้ assign เข้า readonly field เอง:

csharp
public class Service(IRepository<User> repo)
{
    private readonly IRepository<User> _repo = repo;   // ตอนนี้ห้ามแก้
}

5. Collection Expression (C# 12+)

C# 12 เพิ่มไวยากรณ์สร้าง collection แบบสั้นด้วย [...] — ใช้ได้กับ array, List, Span ฯลฯ โดย compiler เลือกวิธีสร้างที่เหมาะที่สุดให้ และรองรับ spread (..) รวม collection:

csharp
// แทน
int[] a = new int[] { 1, 2, 3 };
List<int> b = new List<int> { 1, 2, 3 };

// ใช้
int[] a2 = [1, 2, 3];
List<int> b2 = [1, 2, 3];
ReadOnlySpan<int> c = [1, 2, 3];

// spread
int[] combined = [..a2, 4, 5, ..b2];

Compiler เลือกวิธีสร้างที่ดีที่สุดตามชนิดเป้าหมาย — เร็วกว่าเขียนเอง (เป็นมิตรกับ AOT = Ahead-Of-Time compile คือการแปลงโปรแกรมเป็น native code ที่รันได้ทันทีตั้งแต่ตอน compile โดยไม่ต้องรอ JIT แปลตอนเปิดโปรแกรม — รายละเอียดเพิ่มเป็นเรื่องขั้นสูงอยู่ในหัวข้อ 17 ข้ามได้ถ้ายังไม่สนใจ)

ที่น่าสนใจ: backing storage (พื้นที่จริงที่เก็บข้อมูล) ขึ้นอยู่กับ target type ที่เรากำหนดไว้ ไม่ใช่ syntax [...] — เช่น List<int> จะไปอยู่บน heap เหมือนเดิม

ส่วน ReadOnlySpan<int> compiler อาจเลือกใช้ stackalloc (จองหน่วยความจำบน stack โดยตรง ไม่ใช่ heap) หรือสร้าง inline array (array ขนาดคงที่ที่ฝังอยู่ใน stack เลย ไม่ต้อง allocate แยก) ให้อยู่บน stack แทน — เพื่อหลบการ allocation (จองหน่วยความจำ) บน heap ที่ช้ากว่า


6. Required Members (C# 11+) — recap

ทบทวน required (C# 11+) — บังคับให้ผู้สร้าง object กำหนดค่า property นั้นตอน initialize ไม่งั้น compile ไม่ผ่าน ทำให้มั่นใจว่าข้อมูลสำคัญถูกตั้งครบโดยไม่ต้องเขียน constructor:

csharp
public class User
{
    public required string Email { get; init; }
    public required string Name { get; init; }
}

var u = new User { Email = "a@b", Name = "A" };
// var u2 = new User();   // ❌ compile error

7. File-scoped namespace (C# 10+)

C# 10 ให้เขียน namespace แบบ "ทั้งไฟล์" (namespace X; จบด้วย ;) แทนการครอบทั้งไฟล์ด้วยปีกกา — ลด indent หนึ่งระดับ โค้ดสะอาดขึ้น (1 ไฟล์มักมี namespace เดียวอยู่แล้ว):

csharp
// แทน
namespace MyApp.Models
{
    public class User { }
}

// ใช้ (ลด indent 1 ระดับ)
namespace MyApp.Models;

public class User { }

ใช้กับทุก file ใหม่ตั้งแต่ C# 10+


8. Global Using (C# 10+)

csharp
// ไฟล์ GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
global using MyApp.Common;

ทุกไฟล์ใน project เห็น using พวกนี้โดยไม่ต้องเขียน

💡 ตั้งแต่ .NET 6, ImplicitUsings=enable ให้ default พอแล้ว — ใช้ global using สำหรับ custom


9. Init-only setter + Required (recap)

init (แทน set) ทำให้ property กำหนดค่าได้แค่ตอนสร้าง object แล้วล็อกไม่ให้แก้อีก — ได้ immutability แต่ยังใช้ object initializer ได้ คู่กับ required ก็บังคับให้ตั้งครบ:

csharp
public record Config
{
    public required string ConnectionString { get; init; }
    public int Timeout { get; init; } = 30;
}

immutable หลัง construct + บังคับว่าต้อง set


10. Raw String Literal (C# 11+)

"Raw string literal" (C# 11+) ใช้ triple quote """...""" เขียนข้อความหลายบรรทัดโดยไม่ต้อง escape เครื่องหมายคำพูดหรือ backslash — ดีมากสำหรับ SQL, JSON, regex ที่มีอักขระพิเศษเยอะ:

csharp
// triple quote — ไม่ต้อง escape
string sql = """
    SELECT * FROM Users
    WHERE Status = 'active'
""";

// interpolation ที่ raw (ใช้ $$ และ {{ }})
string name = "Alice";
string json = $$"""
{
    "name": "{{name}}",
    "tags": ["a", "b"]
}
""";

ดีมากสำหรับ SQL, JSON, regex

🚨 อย่าใช้ string interpolation ต่อ SQL กับค่าจากผู้ใช้ — เปิดช่อง SQL injection ทันที

csharp
// ❌ อันตราย — input ของผู้ใช้ถูกฝังเป็นข้อความใน SQL ตรง ๆ
string sql = $"""
    SELECT * FROM Users WHERE Name = '{userInput}'
""";

// ✅ ใช้ parameterized query — ค่าถูกส่งแยกจาก SQL
string sql = """
    SELECT * FROM Users WHERE Name = @name
""";
using var cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@name", userInput);

ตัวอย่างถัดไปใช้ EF Core (Entity Framework Core — library จัดการฐานข้อมูลที่จะสอนใน dotnet/) ยังไม่ต้องเข้าใจ syntax ทั้งหมดตอนนี้ — อ่านเพื่อจำหลักการ "ห้ามต่อ string ใส่ SQL" ไว้ก่อนพอ:

EF Core ก็เช่นเดียวกัน — ใช้ FromSql (EF Core 7+, จัดการ parameterize ให้อัตโนมัติ) ไม่ใช่ FromSqlRaw กับ string interpolation:

csharp
// ✅ ปลอดภัย — FromSql (EF Core 7+) รับ interpolated string literal ตรง ๆ แล้วแยกค่าออกมา parameterize ให้อัตโนมัติ
db.Users.FromSql($"SELECT * FROM Users WHERE Name = {userInput}")

(FromSqlInterpolated ยังใช้ได้แต่เป็น API เก่า — FromSql คือ modern API)

⚠️ ข้อควรระวัง: ที่ปลอดภัยเพราะเราส่ง interpolated string literal (ตัว $"..." ตรง ๆ) เข้า FromSql โดยตรง — compiler จะแปลงมันเป็น type พิเศษที่แยกค่าออกมา parameterize ให้ ถ้าเผลอไปประกาศเก็บใน string variable ก่อนแล้วค่อยส่งเข้า FromSql (เช่น string sql = $"...{userInput}..."; db.Users.FromSql(sql)) จะกลายเป็น string ธรรมดา สูญเสียการ parameterize และกลับมาเสี่ยง SQL injection เหมือนเดิม


11. UTF-8 String Literal (C# 11+)

ต่อท้าย string ด้วย u8 (C# 11+) จะได้ byte ของ UTF-8 ตรง ๆ

ปกติเขียน "Hello" จะได้ string ก่อน แล้วต้องแปลงเป็น byte ตอนรัน (runtime) ซึ่งช้า

ใช้ u8 compiler ฝัง byte ลงไปตั้งแต่ตอน compile — เหมาะกับงาน network/protocol ที่ทำงานกับ byte ล้วน และต้องการ performance:

csharp
ReadOnlySpan<byte> bytes = "Hello"u8;
// → byte[] ของ UTF-8 — ไม่ allocate string ตอน runtime

ใช้ใน network protocol / file format


12. List Pattern (C# 11+) — recap

"List pattern" (C# 11+) ให้ match รูปร่างของ array/list ใน switch ได้ — เช่นเช็คว่าว่าง, มีกี่ตัว, หรือขึ้นต้น/ลงท้ายด้วยค่าไหน (.. แทนส่วนที่เหลือ) เขียน logic ตามโครงสร้างข้อมูลได้กระชับ:

csharp
int[] arr = [1, 2, 3];

string desc = arr switch
{
    []                       => "empty",
    [var x]                  => $"one: {x}",
    [_, _]                   => "two",
    [1, .., 3]               => "starts 1 ends 3",
    [var first, .. var rest] => $"first={first}, rest len={rest.Length}"
};

13. Generic Math (C# 11+)

C# 11 เพิ่ม "static abstract member" ใน interface ทำให้เขียน generic ที่ใช้ตัวดำเนินการคณิตศาสตร์ได้ — เช่น Sum<T> ที่ทำงานกับทุกชนิดตัวเลข (int, double, decimal) ผ่าน constraint INumber<T> ไม่ต้องเขียนซ้ำ:

csharp
public static T Sum<T>(IEnumerable<T> source) where T : INumber<T>
{
    T total = T.Zero;
    foreach (var x in source) total += x;
    return total;
}

Sum(new[] { 1, 2, 3 });         // ok
Sum(new[] { 1.0, 2.0, 3.0 });   // ok
Sum(new[] { 1m, 2m, 3m });      // ok decimal

INumber<T> มี static abstract Zero, operator +, -, etc. — ทำให้เขียน generic math ได้


14. Default Lambda Parameter (C# 12+)

C# 12 ให้ lambda มีค่า default ของพารามิเตอร์ได้ (เหมือน method) — เรียกโดยไม่ส่งค่าก็ใช้ default:

csharp
var greet = (string name = "World") => $"Hello, {name}!";
greet();          // Hello, World!
greet("Alice");   // Hello, Alice!

15. Params Collections (C# 13+)

เดิม params ต้องเป็น array (ซึ่ง allocate ทุกครั้งที่เรียก) — C# 13 ให้ params เป็น Span/collection อื่นได้ ทำให้รับจำนวน argument ไม่จำกัดโดยไม่ต้องจองหน่วยความจำ heap:

csharp
void Print(params ReadOnlySpan<int> nums)
{
    foreach (var n in nums) Console.WriteLine(n);
}

Print(1, 2, 3);   // no allocation (Span บน stack)

ก่อนหน้านี้ params ต้องเป็น array → allocate

💡 C# 13 ยังเพิ่ม params IEnumerable<T>, params IReadOnlyList<T>, params List<T> ด้วย — ทำให้รับ collection ที่มีอยู่แล้วได้โดยไม่ต้องสร้าง array ใหม่


16. ref struct + Span<T>

⏭️ โซนขั้นสูง — มือใหม่ข้ามได้

Span<T> และ ref struct เป็นเครื่องมือเพิ่ม performance ที่มีข้อจำกัดเยอะ (ห้าม async, ห้ามเก็บใน field) ใช้เฉพาะโค้ดที่ต้องเร็วเป็นพิเศษ มือใหม่อ่านผ่าน ๆ พอ ยังไม่ต้องใช้

Span<T> = "มุมมอง" (view) ของหน่วยความจำที่เรียงต่อกัน (array, stack, native) โดยไม่ก๊อปข้อมูล

ref struct = struct ที่บังคับให้อยู่บน stack เท่านั้น (อยู่บน heap ไม่ได้)

csharp
Span<int> stackSpan = stackalloc int[100];
ReadOnlySpan<char> sub = "Hello, World".AsSpan(7, 5);   // "World"

// ใช้สำหรับ parse efficiently
int Parse(ReadOnlySpan<char> input)
{
    int result = 0;
    foreach (char c in input)
    {
        if (!char.IsDigit(c)) throw new FormatException();
        result = result * 10 + (c - '0');
    }
    return result;
}

Parse("123".AsSpan());

Span<T> เป็น ref struct → ห้ามอยู่บน heap → ห้ามเก็บใน field, ห้ามใช้ใน async — แต่เร็วมากเพราะไม่ต้อง allocate (จองหน่วยความจำ)


17. Source Generators (preview overview)

⏭️ โซนขั้นสูง — มือใหม่ข้ามได้

Source generator เป็นเรื่องของการสร้างโค้ดอัตโนมัติตอน compile ซึ่งมือใหม่แทบไม่ต้องเขียนเอง (แค่ใช้ของที่มากับ library) อ่านผ่าน ๆ พอ

C# 9+ มี source generator = ตัวสร้างโค้ดให้อัตโนมัติตอน compile (ไม่ต้องเขียนเองตอนรัน)

ตัวอย่างที่เจอบ่อย:

  • [JsonSerializable] ของ System.Text.Json → สร้าง serializer (ตัวแปลง object ↔ JSON) ให้
    • ปกติ JSON serializer ใช้ reflection (การส่องดูโครงสร้าง type ตอนรัน) ซึ่งช้าและ AOT ไม่รองรับ
    • source generator แทนด้วยโค้ดที่สร้างไว้แล้ว → เร็วขึ้น + เข้ากับ AOT (Ahead-Of-Time compile = compile เป็น native ก่อนรัน)
  • [GeneratedRegex] → compile regex (รูปแบบค้นหาข้อความ) ตั้งแต่ compile
  • [LoggerMessage] → log แบบไม่ต้อง allocate string
  • [LibraryImport] (C# 12+) → แทน [DllImport] เดิม สำหรับเรียก native lib (library ที่เขียนด้วย C/C++) แบบ AOT-friendly — งานทั่วไปไม่ต้องใช้
csharp
// ต้องอยู่ใน partial class — source generator จะ generate body ของ DateRegex() ให้
public partial class Validators
{
    [GeneratedRegex(@"^\d{4}-\d{2}-\d{2}$")]
    private static partial Regex DateRegex();

    public static bool IsIsoDate(string s) => DateRegex().IsMatch(s);
}

Validators.IsIsoDate("2024-05-23");   // true

attribute พวกนี้คือ pattern ที่ทำให้โค้ดใช้กับ AOT compile ได้ — ถ้าเล็งทำ Native AOT (เช่น minimal API ที่บูตเร็ว) ใช้ทั้งหมดที่กล่าวมา


18. Modern C# Style Checklist

โค้ดที่ "ดู modern" จะมี:

  • file-scoped namespace
  • record สำหรับ DTO/data
  • top-level statement สำหรับ entry point
  • primary constructor สำหรับ service
  • required แทน big constructor
  • init setter
  • collection expression [1, 2, 3]
  • pattern matching แทน if/else chain
  • raw string literal สำหรับ SQL/JSON
  • LINQ chain แทน manual loop
  • static lambda เพื่อ no closure
  • Span<T> ใน hot path

🛠️ Checkpoint 10 — ลงมือทำ

  1. แปลง class User (มี constructor + override Equals/GetHashCode) ให้เป็น record
  2. ใช้ with expression สร้าง user ใหม่ที่อายุ +1
  3. ใช้ primary ctor + collection expression สร้าง OrderService(IRepository<Order> repo) กับ method Recent() => [..repo.FindAll().Take(10)]
  4. เขียน method bool TryParseEmail(string input, [NotNullWhen(true)] out string? email) ใช้ NRT attribute
  5. ใช้ raw string + interpolation สร้าง SQL ที่ inject parameter (ผ่าน @param)
  6. เปลี่ยน method ที่รับ string แล้ว split → รับ ReadOnlySpan<char> แทน

สรุปบทที่ 10

  • record = value equality + immutable + with expression
  • pattern matching ขยายไปถึง list, property nested, relational
  • NRT + attribute = หลีกเลี่ยง NRE ได้เกือบสมบูรณ์
  • primary ctor ลดโค้ดของ class ที่ DI dependency
  • collection expression [1, 2, 3] + spread [..a, ..b]
  • raw string ทำให้ SQL/JSON literal สวย
  • Span<T> เร็วใน hot path (parse, slice) — ห้าม async
  • Source generator = code-gen ที่ compile-time (AOT-friendly)

→ บทที่ 11: Async + Tasks