diff --git a/.skills/csharp-best-practices/SKILL.md b/.skills/csharp-best-practices/SKILL.md index 1d99bf30..9753a2ed 100644 --- a/.skills/csharp-best-practices/SKILL.md +++ b/.skills/csharp-best-practices/SKILL.md @@ -1,15 +1,15 @@ --- name: csharp-best-practices description: > - C# 12 / .NET 8 coding conventions, idiomatic patterns, and performance best practices + C# 14 / .NET 10 coding conventions, idiomatic patterns, and performance best practices for the Minecraft Console Client codebase. Use when writing, reviewing, or modifying C# code. -version: 0.3.0 +version: 0.4.0 --- -# C# 12 / .NET 8 Best Practices +# C# 14 / .NET 10 Best Practices -Target: **.NET 8**, **C# 12**, nullable enabled. -Sources: [MS C# Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) · [.NET Runtime Style](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md) · [C# 12 Docs](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12) · [.NET 8 Perf](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/) +Target: **.NET 10**, **C# 14**, nullable enabled. +Sources: [MS C# Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) · [.NET Runtime Style](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md) · [C# 14 Proposals](https://github.com/dotnet/csharplang/blob/main/Language-Version-History.md) · [C# 13 Docs](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13) ## Naming @@ -41,6 +41,188 @@ public int packet_count { get; set; } // snake_case public async Task Connect(CancellationToken ct) { } // missing Async suffix ``` +## C# 14 Features + +### Extension Members (C# 14) + +Declare extension methods, properties, and operators inside `extension(...)` blocks. Replaces `this`-parameter pattern for new extensions. + +```csharp +// CORRECT: extension property + method (C# 14) +public static class EntityExtensions +{ + extension(Entity entity) + { + public bool IsAlive => entity.Health > 0; + public void Heal(int amount) => entity.Health = Math.Min(entity.Health + amount, 20); + } + extension(IEnumerable items) + { + public bool IsEmpty => !items.GetEnumerator().MoveNext(); + } +} +``` + +```csharp +// WRONG: classic extension method when C# 14 extension block is available +public static bool IsAlive(this Entity entity) => entity.Health > 0; +``` + +### `field` Keyword in Properties (C# 14) + +Access the auto-generated backing field without declaring it. Mix auto and full accessors. + +```csharp +// CORRECT: lazy init with field keyword +public string DisplayName => field ??= ComputeDisplayName(); + +// CORRECT: INotifyPropertyChanged pattern +public bool IsConnected +{ + get; + set + { + if (field == value) return; + field = value; + OnPropertyChanged(); + } +} +``` + +```csharp +// WRONG: manual backing field when field keyword suffices +private string? _displayName; +public string DisplayName => _displayName ??= ComputeDisplayName(); +``` + +### Null-Conditional Assignment (C# 14) + +Assign through `?.` — RHS is only evaluated when receiver is non-null. + +```csharp +// CORRECT: null-conditional assignment +player?.Health = 20; +connection?.OnDisconnect += HandleDisconnect; +inventory?[slot] = newItem; +``` + +```csharp +// WRONG: manual null check for simple assignment +if (player is not null) + player.Health = 20; +``` + +### Simple Lambda Parameters with Modifiers (C# 14) + +Omit types on lambda parameters while still applying modifiers. + +```csharp +// CORRECT: modifiers without explicit types +TryParse parse = (text, out result) => int.TryParse(text, out result); +ReadOnlySpan data = [1, 2, 3]; +ProcessSpan((scoped span) => span.Length); +``` + +```csharp +// WRONG: fully explicit types just for a modifier +TryParse parse = (string text, out int result) => int.TryParse(text, out result); +``` + +### First-Class Span Types (C# 14) + +Implicit conversions between `T[]`, `Span`, and `ReadOnlySpan` — no explicit cast needed. Extension methods on `ReadOnlySpan` apply to arrays and spans automatically. + +```csharp +// CORRECT: pass array where ReadOnlySpan is expected (C# 14) +int[] data = [1, 2, 3]; +bool found = data.StartsWith(1); // ReadOnlySpan extension resolved +ReadOnlySpan span = stackalloc byte[4]; +``` + +### Unbound Generics in `nameof` (C# 14) + +```csharp +// CORRECT: no need to pick a dummy type argument +string name = nameof(Dictionary<,>); // "Dictionary" +string prop = nameof(List<>.Count); // "Count" +``` + +```csharp +// WRONG: arbitrary type argument just to satisfy nameof +string name = nameof(Dictionary); +``` + +### Partial Events and Constructors (C# 14) + +Separate declaration from implementation for source-generator scenarios. + +```csharp +// CORRECT: partial constructor for source-gen interop +partial class ServerConnection +{ + partial ServerConnection(string host, int port); +} +partial class ServerConnection +{ + partial ServerConnection(string host, int port) { /* generated */ } +} +``` + +### `#:` Ignored Directives (C# 14) + +For file-based `dotnet run app.cs` programs — ignored by the compiler. + +```csharp +#!/usr/bin/dotnet run +#:package System.CommandLine@2.0.0-* +Console.WriteLine("Hello"); +``` + +## C# 13 Features + +### `Lock` Object (C# 13) + +Use `System.Threading.Lock` instead of `lock(obj)` on arbitrary objects. + +```csharp +// CORRECT: dedicated Lock type +private readonly Lock _lock = new(); +public void Enqueue(ChatMessage msg) { lock (_lock) _queue.Add(msg); } +``` + +```csharp +// WRONG: locking on an object reference +private readonly object _syncRoot = new(); +lock (_syncRoot) { } +``` + +### `params` Collections (C# 13) + +`params` now works with `ReadOnlySpan`, `Span`, `IEnumerable`, and other collection types. + +```csharp +// CORRECT: params span avoids array allocation +public void Log(params ReadOnlySpan messages) +{ + foreach (var msg in messages) Console.WriteLine(msg); +} +``` + +### Partial Properties (C# 13) + +```csharp +// CORRECT: partial property for source generators +partial class Config +{ + public partial string Host { get; set; } +} +partial class Config +{ + public partial string Host { get => _host; set => _host = value; } + private string _host = ""; +} +``` + ## C# 12 Features ### Primary Constructors @@ -518,7 +700,7 @@ for (int i = 0; i < data.Length; i++) int found = data.ToArray().Count(b => b == target); ``` -## Performance (.NET 8) +## Performance (.NET 8+) ### Span\ / Memory\ @@ -617,11 +799,12 @@ foreach (var s in items) combined += s + ", "; | Scenario | Type | Notes | |---|---|---| | General key-value | `Dictionary` | O(1) lookup | -| Build once, read many | `FrozenDictionary` | .NET 8; faster reads | +| Build once, read many | `FrozenDictionary` | .NET 8+; faster reads | | Thread-safe | `ConcurrentDictionary` | Lock-free reads | | Immutable snapshots | `ImmutableDictionary` | Persistent structure | | Membership test | `HashSet` / `FrozenSet` | FrozenSet for static | | Priority queue | `PriorityQueue` | .NET 6+ | +| Synchronisation | `System.Threading.Lock` | C# 13; prefer over `lock(obj)` | | Producer-consumer | `Channel` | Over `BlockingCollection` | | Temp buffer | `ArrayPool` / `stackalloc` | Zero/low alloc |