一键导入
csharp-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.
// 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.
Build, run, and debug Minecraft Console Client (MCC) against a real local Minecraft Java server on Linux, macOS, or WSL. Use this whenever the user wants to compile MCC, start or inspect a local test server, connect MCC to a server, debug protocol or login issues, validate a code change end-to-end, or run MCC commands on a real server instead of guessing from static code.
Use when proving MCC behavior on a real local Minecraft server, validating runtime or protocol changes end-to-end, exercising movement, physics, inventory, entity, chat, or terrain behavior, or running a single-version or cross-version regression sweep.
create, repair, compress, and optimize prompts, system messages, tool instructions, schemas, and eval rubrics for general tasks across writing, research, coding, analysis, planning, tutoring, automation, and agent workflows. use when the user wants a new prompt, wants an existing prompt improved, wants prompt failures debugged, or needs better structure for grounding, tool use, output format, or reliability.
Adapt MCC palettes and protocol handling for a new Minecraft version. Use when the user wants to add support for a new MC version, compare version registries, update item/entity/block/metadata palettes, or fix protocol mismatches between MC versions.
Use when diagnosing or optimizing generic C#/.NET performance, GC pressure, allocations, heap or stack usage, LINQ overhead, boxing, Span/Memory, stackalloc, pooling, or hot-path code with CLI-first tools such as dotnet-counters, dotnet-trace, dotnet-stack, dotnet-gcdump, dotnet-dump, or BenchmarkDotNet.
Use when optimizing C# code in MCC, reducing GC pressure, profiling hot paths, fixing latency spikes, or reviewing code for allocation or throughput issues.
| name | csharp-best-practices |
| description | 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. |
Target: .NET 10, C# 14, nullable enabled. Sources: MS C# Conventions · .NET Runtime Style · C# 14 Proposals · C# 13 Docs
| Element | Style | Example |
|---|---|---|
| Type, method, property, const, enum member | PascalCase | PacketHandler, MaxRetries, GameMode.Survival |
| Interface | I + PascalCase | IChatBot |
| Private instance field | _camelCase | _handler |
| Private static field | s_camelCase | s_defaultTimeout |
| Thread-static field | t_camelCase | t_cachedBuffer |
| Local, parameter | camelCase | packetId |
| Type parameter | T + PascalCase | TResult |
| Namespace | PascalCase | MinecraftClient.Protocol |
| Async methods | Suffix Async | ConnectAsync(), ReadPacketAsync() |
// CORRECT: naming conventions
private readonly Dictionary<int, Entity> _entities = new();
private static readonly TimeSpan s_reconnectDelay = TimeSpan.FromSeconds(5);
public int PacketCount { get; private set; }
public async Task<bool> ConnectAsync(CancellationToken ct) { }
// WRONG: naming violations
private Dictionary<int, Entity> entities = new(); // missing _
private static TimeSpan reconnectDelay; // missing s_
public int packet_count { get; set; } // snake_case
public async Task<bool> Connect(CancellationToken ct) { } // missing Async suffix
Declare extension methods, properties, and operators inside extension(...) blocks. Replaces this-parameter pattern for new extensions.
// 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<T>(IEnumerable<T> items)
{
public bool IsEmpty => !items.GetEnumerator().MoveNext();
}
}
// 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.
// 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();
}
}
// WRONG: manual backing field when field keyword suffices
private string? _displayName;
public string DisplayName => _displayName ??= ComputeDisplayName();
Assign through ?. — RHS is only evaluated when receiver is non-null.
// CORRECT: null-conditional assignment
player?.Health = 20;
connection?.OnDisconnect += HandleDisconnect;
inventory?[slot] = newItem;
// WRONG: manual null check for simple assignment
if (player is not null)
player.Health = 20;
Omit types on lambda parameters while still applying modifiers.
// CORRECT: modifiers without explicit types
TryParse<int> parse = (text, out result) => int.TryParse(text, out result);
ReadOnlySpan<int> data = [1, 2, 3];
ProcessSpan((scoped span) => span.Length);
// WRONG: fully explicit types just for a modifier
TryParse<int> parse = (string text, out int result) => int.TryParse(text, out result);
Implicit conversions between T[], Span<T>, and ReadOnlySpan<T> — no explicit cast needed. Extension methods on ReadOnlySpan<T> apply to arrays and spans automatically.
// CORRECT: pass array where ReadOnlySpan<T> is expected (C# 14)
int[] data = [1, 2, 3];
bool found = data.StartsWith(1); // ReadOnlySpan<int> extension resolved
ReadOnlySpan<byte> span = stackalloc byte[4];
nameof (C# 14)// CORRECT: no need to pick a dummy type argument
string name = nameof(Dictionary<,>); // "Dictionary"
string prop = nameof(List<>.Count); // "Count"
// WRONG: arbitrary type argument just to satisfy nameof
string name = nameof(Dictionary<object, object>);
Separate declaration from implementation for source-generator scenarios.
// 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.
#!/usr/bin/dotnet run
#:package System.CommandLine@2.0.0-*
Console.WriteLine("Hello");
Lock Object (C# 13)Use System.Threading.Lock instead of lock(obj) on arbitrary objects.
// CORRECT: dedicated Lock type
private readonly Lock _lock = new();
public void Enqueue(ChatMessage msg) { lock (_lock) _queue.Add(msg); }
// WRONG: locking on an object reference
private readonly object _syncRoot = new();
lock (_syncRoot) { }
params Collections (C# 13)params now works with ReadOnlySpan<T>, Span<T>, IEnumerable<T>, and other collection types.
// CORRECT: params span avoids array allocation
public void Log(params ReadOnlySpan<string> messages)
{
foreach (var msg in messages) Console.WriteLine(msg);
}
// 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 = "";
}
Use for simple parameter capture. Parameters are camelCase, mutable — assign to readonly fields when immutability matters.
// CORRECT: primary constructor captures dependencies
public class ChatLogger(string logFilePath, bool appendMode) : ChatBot
{
private readonly StreamWriter _writer = new(logFilePath, appendMode);
public override void GetText(string text) => _writer.WriteLine(text);
}
// WRONG: verbose constructor boilerplate for simple capture
public class ChatLogger : ChatBot
{
private readonly StreamWriter _writer;
public ChatLogger(string logFilePath, bool appendMode)
{
_writer = new StreamWriter(logFilePath, appendMode);
}
public override void GetText(string text) => _writer.WriteLine(text);
}
Use [...] and .. spread for arrays, lists, spans.
// CORRECT: collection expressions (C# 12)
int[] ids = [1, 2, 3];
List<string> names = ["Steve", "Alex"];
ReadOnlySpan<byte> header = [0xFE, 0x01]; // no heap alloc
int[] combined = [..firstArray, ..secondArray, 42];
IReadOnlyList<string> empty = [];
// WRONG: verbose initialization
int[] ids = new int[] { 1, 2, 3 };
var names = new List<string> { "Steve", "Alex" };
ReadOnlySpan<byte> header = new byte[] { 0xFE, 0x01 }; // allocates
var combined = firstArray.Concat(secondArray).Append(42).ToArray();
// CORRECT: alias complex types for readability
using Coordinate = (int X, int Y, int Z);
using PacketMap = System.Collections.Generic.Dictionary<int, System.Action<byte[]>>;
// CORRECT: C# 12
var greet = (string name, string prefix = "Player") => $"{prefix} {name}";
// CORRECT: file-scoped namespace — one per file, less nesting
namespace MinecraftClient.ChatBots;
public class MyBot : ChatBot { }
// WRONG: block-scoped namespace adds unnecessary nesting
namespace MinecraftClient.ChatBots
{
public class MyBot : ChatBot { }
}
newUse when the type is obvious from the left-hand side.
// CORRECT: target-typed new
private readonly Dictionary<string, int> _scores = new();
List<Entity> entities = new(capacity: 256);
// WRONG: redundant type name
private readonly Dictionary<string, int> _scores = new Dictionary<string, int>();
Prefer patterns over type-casting chains and complex boolean logic.
// CORRECT: is-pattern with declaration and property patterns
if (entity is Player { Health: > 0 } player)
SendMessage($"{player.Name} is alive");
// WRONG: manual cast and multi-step check
if (entity is Player)
{
var player = (Player)entity;
if (player.Health > 0)
SendMessage($"{player.Name} is alive");
}
// CORRECT: switch expression
public string GetStatusLabel(GameMode mode) => mode switch
{
GameMode.Survival => "Survival",
GameMode.Creative => "Creative",
GameMode.Adventure => "Adventure",
GameMode.Spectator => "Spectator",
_ => throw new ArgumentOutOfRangeException(nameof(mode))
};
// WRONG: switch statement with returns
public string GetStatusLabel(GameMode mode)
{
switch (mode)
{
case GameMode.Survival: return "Survival";
case GameMode.Creative: return "Creative";
default: throw new ArgumentOutOfRangeException(nameof(mode));
}
}
// CORRECT: property patterns for compound conditions
if (response is { StatusCode: >= 200 and < 300, Content.Length: > 0 })
ProcessResponse(response);
// WRONG: multiple chained conditions
if (response != null && response.StatusCode >= 200
&& response.StatusCode < 300 && response.Content != null
&& response.Content.Length > 0)
ProcessResponse(response);
// CORRECT: relational, logical, and list patterns
if (health is > 0 and <= 6) LogToConsole("Low health!");
if (args is [var command, var target, ..]) ProcessCommand(command, target);
Use for JSON, regex, multi-line strings.
// CORRECT: raw string literal
string json = """
{ "username": "Steve", "action": "connect" }
""";
string pattern = """<\w+>""";
// WRONG: escaped quotes
string json = "{ \"username\": \"Steve\", \"action\": \"connect\" }";
Use record for immutable data carriers and DTOs. Use record struct for small value types.
// CORRECT: record for data carrier
public record PlayerInfo(string Name, Guid Uuid, GameMode Mode);
public record struct ChunkCoord(int X, int Z);
var updated = info with { Mode = GameMode.Creative };
// WRONG: full class for a simple data carrier
public class PlayerInfo
{
public string Name { get; set; } = string.Empty;
public Guid Uuid { get; set; }
public GameMode Mode { get; set; }
}
// CORRECT: compact constructor for record validation
public record OrderItem(string ProductId, int Quantity, decimal UnitPrice)
{
public OrderItem
{
ArgumentException.ThrowIfNullOrWhiteSpace(ProductId);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(Quantity);
ArgumentOutOfRangeException.ThrowIfNegative(UnitPrice);
}
}
// CORRECT: required + init enforces initialization without constructor boilerplate
public class ServerConfig
{
public required string Host { get; init; }
public required int Port { get; init; }
public string? Password { get; init; }
}
var config = new ServerConfig { Host = "mc.example.com", Port = 25565 };
Project has nullable enabled. Follow these rules:
// CORRECT: guard at API boundaries with .NET 8 throw helpers
public void Connect(string host, IProtocolHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
ArgumentException.ThrowIfNullOrEmpty(host);
}
// WRONG: manual null checks
if (handler == null) throw new ArgumentNullException(nameof(handler));
if (string.IsNullOrWhiteSpace(host))
throw new ArgumentException("Host is required", nameof(host));
// CORRECT: 'is not null' pattern
if (currentPlayer is not null)
currentPlayer.Update();
// WRONG: comparison operator for null check
if (currentPlayer != null)
currentPlayer.Update();
// CORRECT: explicit nullable handling
public Entity? FindEntity(int id)
{
return _entities.TryGetValue(id, out var entity) ? entity : null;
}
// CORRECT: null-coalescing / null-conditional
string name = player?.CustomName ?? player?.Name ?? "Unknown";
// CORRECT: null-forgiving only when proven safe (after ThrowIfNull or equivalent)
string val = GetRequiredValue()!;
// CORRECT: annotate return values
[return: MaybeNull]
public T Find<T>(Predicate<T> match) { }
[MemberNotNull(nameof(_connection))]
private void EnsureConnected() { }
// WRONG: hiding nullability with null-forgiving
public string GetName(Player? player)
{
return player!.Name; // hides potential NullReferenceException
}
// CORRECT: propagate CancellationToken through every async I/O call
public async Task<string> FetchDataAsync(Uri uri, CancellationToken ct = default)
{
using var response = await _httpClient.GetAsync(uri, ct);
return await response.Content.ReadAsStringAsync(ct);
}
// WRONG: CancellationToken not passed downstream
public async Task<string> FetchDataAsync(Uri uri)
{
using var response = await _httpClient.GetAsync(uri, default);
return await response.Content.ReadAsStringAsync(default);
}
// CORRECT: ValueTask when result is often available synchronously
public ValueTask<int> GetCachedCountAsync()
{
if (_cache.TryGetValue("count", out int count))
return ValueTask.FromResult(count);
return new ValueTask<int>(LoadCountFromDbAsync());
}
// WRONG: Task allocates unnecessarily when result is cached
public async Task<int> GetCachedCountAsync()
{
if (_cache.TryGetValue("count", out int count))
return count; // allocates a Task
return await LoadCountFromDbAsync();
}
// CORRECT: async Task for async event handlers
public async Task HandleEventAsync(GameEvent e, CancellationToken ct)
{
await notificationService.SendAsync(e.PlayerId, ct);
}
// WRONG: async void — exceptions are unobservable, cannot be awaited
public async void HandleEvent(GameEvent e)
{
await notificationService.SendAsync(e.PlayerId, default);
}
// CORRECT: await the result
var packet = await reader.ReadPacketAsync(ct);
// WRONG: .Result / .Wait() causes deadlocks
var packet = reader.ReadPacketAsync(ct).Result;
var packet2 = reader.ReadPacketAsync(ct).GetAwaiter().GetResult();
// CORRECT: ConfigureAwait(false) in library code
var data = await stream.ReadAsync(buffer, ct).ConfigureAwait(false);
// CORRECT: IAsyncEnumerable for streaming
public async IAsyncEnumerable<ChatMessage> ReadChatStreamAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
while (!ct.IsCancellationRequested)
yield return await _reader.ReadNextAsync(ct);
}
// CORRECT: await using for async disposal
await using var conn = new McConnection(host, port);
// CORRECT: method syntax for common operations
var onlinePlayers = players
.Where(p => p.IsOnline)
.OrderBy(p => p.Name)
.Select(p => new PlayerListItem(p.Id, p.Name))
.ToList();
// AVOID: query syntax for simple operations
var onlinePlayers = (
from p in players
where p.IsOnline
orderby p.Name
select new PlayerListItem(p.Id, p.Name)
).ToList();
// CORRECT: query syntax makes joins readable
var results =
from entity in entities
join player in players on entity.OwnerId equals player.Id
where entity.Health > 0
select new { entity.Name, player.Name };
// AVOID: method syntax for complex joins is hard to read
var results = entities
.Join(players,
e => e.OwnerId,
p => p.Id,
(e, p) => new { e, p })
.Where(x => x.e.Health > 0)
.Select(x => new { x.e.Name, PlayerName = x.p.Name });
// CORRECT: materialize once, iterate many times
var online = players.Where(p => p.IsOnline).ToList();
Console.WriteLine(online.Count);
foreach (var p in online) { }
// WRONG: enumerates the query twice
var filtered = players.Where(p => p.IsOnline);
Console.WriteLine(filtered.Count()); // first enumeration
foreach (var p in filtered) { } // second enumeration
// CORRECT: short-circuits on first match
if (entities.Any(e => e.IsHostile))
TriggerAlert();
// WRONG: counts the entire collection
if (entities.Count(e => e.IsHostile) > 0)
TriggerAlert();
// CORRECT: explicit null handling
var target = players.FirstOrDefault(p => p.Name == name)
?? throw new InvalidOperationException($"Player '{name}' not found");
// CORRECT: avoid full enumeration just to get count (.NET 6+)
if (source.TryGetNonEnumeratedCount(out int count))
buffer = new Entity[count];
// CORRECT: manual loop with Span in performance-critical code
Span<byte> data = stackalloc byte[256];
int found = 0;
for (int i = 0; i < data.Length; i++)
if (data[i] == target) found++;
// AVOID: LINQ allocates enumerators and delegates on hot paths
int found = data.ToArray().Count(b => b == target);
// CORRECT: zero-allocation slicing
ReadOnlySpan<char> command = input.AsSpan()[1..]; // skip '/'
// CORRECT: stack-allocated parsing
public static int ParseVarInt(ReadOnlySpan<byte> data, out int bytesRead)
{
int result = 0; bytesRead = 0; byte cur;
do { cur = data[bytesRead]; result |= (cur & 0x7F) << (bytesRead * 7); bytesRead++; }
while ((cur & 0x80) != 0);
return result;
}
Build once, read many — ~50% faster lookups than Dictionary.
// CORRECT: FrozenDictionary for read-heavy lookup tables (palettes, protocol maps)
using System.Collections.Frozen;
private static readonly FrozenDictionary<int, string> s_blockNames =
new Dictionary<int, string> { [0] = "air", [1] = "stone" }.ToFrozenDictionary();
Hardware-accelerated set search.
// CORRECT: precompute once, scan with SIMD
private static readonly SearchValues<char> s_separators = SearchValues.Create(" \t\n\r,;");
int idx = input.AsSpan().IndexOfAny(s_separators);
Parse format string once, reuse.
// CORRECT: avoids re-parsing the format string each call
private static readonly CompositeFormat s_logFmt = CompositeFormat.Parse("[{0:HH:mm:ss}] {1}: {2}");
string msg = string.Format(CultureInfo.InvariantCulture, s_logFmt, DateTime.Now, player, text);
// CORRECT: rent from pool for temporary buffers
byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
try { int n = stream.Read(buf.AsSpan(0, 4096)); ProcessPacket(buf.AsSpan(0, n)); }
finally { ArrayPool<byte>.Shared.Return(buf); }
// CORRECT: stackalloc for small, fixed-size buffers (< 512 bytes)
Span<byte> header = stackalloc byte[5];
// CORRECT: explicit StringComparison — always
bool match = name.Equals("Steve", StringComparison.OrdinalIgnoreCase);
int idx = text.IndexOf("hello", StringComparison.Ordinal);
// WRONG: allocates a lowered copy
bool match = name.ToLower() == "steve";
// CORRECT: string.Create for perf-critical formatting
string hex = string.Create(data.Length * 2, data, static (span, bytes) =>
{
for (int i = 0; i < bytes.Length; i++)
bytes[i].TryFormat(span[(i * 2)..], out _, "X2");
});
// CORRECT: StringBuilder for loops
var sb = new StringBuilder(256);
foreach (var item in inventory)
sb.Append(item.Name).Append(" x").Append(item.Count).AppendLine();
// WRONG: O(n²) string concatenation in loop
string combined = "";
foreach (var s in items) combined += s + ", ";
| Scenario | Type | Notes |
|---|---|---|
| General key-value | Dictionary<K,V> | O(1) lookup |
| Build once, read many | FrozenDictionary<K,V> | .NET 8+; faster reads |
| Thread-safe | ConcurrentDictionary<K,V> | Lock-free reads |
| Immutable snapshots | ImmutableDictionary<K,V> | Persistent structure |
| Membership test | HashSet<T> / FrozenSet<T> | FrozenSet for static |
| Priority queue | PriorityQueue<E,P> | .NET 6+ |
| Synchronization | System.Threading.Lock | C# 13; prefer over lock(obj) |
| Producer-consumer | Channel<T> | Over BlockingCollection<T> |
| Temp buffer | ArrayPool<T> / stackalloc | Zero/low alloc |
// CORRECT: Try* pattern for expected failures
if (int.TryParse(input, out int value)) ProcessValue(value);
if (_registry.TryGetValue(packetId, out var handler)) handler.Invoke(data);
// WRONG: using exceptions for control flow
try { return dict[key]; }
catch (KeyNotFoundException) { return null; } // use TryGetValue
// CORRECT: exception filters (catch-when)
try { await ConnectAsync(ct); }
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
LogToConsole("Connection refused, retrying...");
}
// CORRECT: throw helpers (smaller IL, better inlining)
ArgumentNullException.ThrowIfNull(handler);
ArgumentOutOfRangeException.ThrowIfNegative(timeout);
ArgumentOutOfRangeException.ThrowIfGreaterThan(timeout, MaxTimeout);
ObjectDisposedException.ThrowIf(_disposed, this);
// WRONG: generic exceptions
throw new Exception($"Entity {id} not found");
// CORRECT: specific, meaningful exception types
throw new EntityNotFoundException(id);
// or use null-coalescing with throw
return await FindEntityAsync(id, ct)
?? throw new EntityNotFoundException(id);
// AVOID: catching Exception without filtering
try { DoWork(); }
catch (Exception) { /* swallowed */ }
// CORRECT: fix the warning by handling null properly
public string GetDisplayName(Player? player)
{
return player?.DisplayName ?? "Unknown";
}
// WRONG: suppressing nullable warning with pragma
#pragma warning disable CS8602
public string GetDisplayName(Player? player)
{
return player.DisplayName; // NullReferenceException at runtime
}
#pragma warning restore CS8602
// WRONG: suppressing with attribute
[SuppressMessage("Usage", "CA1062:Validate arguments of public methods")]
public void Process(Packet packet)
{
// missing null check
}
Project-wide .editorconfig is the only acceptable place for warning policy:
# .editorconfig - project-wide policy decisions only
dotnet_diagnostic.CA2007.severity = none
// CORRECT: using declaration — disposed at end of scope
using var stream = new FileStream(path, FileMode.Open);
using var reader = new StreamReader(stream);
// CORRECT: IAsyncDisposable
await using var conn = await CreateConnectionAsync();
// CORRECT: Dispose pattern
public class PacketReader : IDisposable
{
private Stream? _stream;
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_stream?.Dispose(); _stream = null; _disposed = true;
}
}
// CORRECT: secure random for tokens
byte[] token = RandomNumberGenerator.GetBytes(32);
// CORRECT: constant-time comparison for secrets
bool valid = CryptographicOperations.FixedTimeEquals(expected, actual);
// CORRECT: validate external input
if (Uri.TryCreate(userInput, UriKind.Absolute, out var uri)
&& uri.Scheme is "http" or "https")
await FetchAsync(uri);
// WRONG: predictable random for security-sensitive values
var rng = new Random();
// WRONG: timing side-channel on secret comparison
bool eq = secret1.SequenceEqual(secret2);
// CORRECT: var when type is obvious from RHS
var entities = new Dictionary<int, Entity>();
var timer = Stopwatch.StartNew();
// CORRECT: explicit type when var would be unclear
Stream responseStream = GetResponse();
int count = items.Count;
// CORRECT: expression-bodied members for one-liners
public override string ToString() => $"[{X}, {Y}, {Z}]";
public bool IsAlive => Health > 0;
// CORRECT: discards for unused values
_ = int.TryParse(s, out int result);
(_, int y, _) = GetCoordinates();
// CORRECT: nameof for resilient refactoring (unbound generics in C# 14)
throw new ArgumentException("Invalid value", nameof(packetId));
LogToConsole($"{nameof(AutoEat)}: eating {item.Name}");
string typeName = nameof(Dictionary<,>); // "Dictionary"
// CORRECT: static lambdas prevent accidental closure allocations
list.Sort(static (a, b) => a.Id.CompareTo(b.Id));
// CORRECT: index/range operators
var last = items[^1];
var slice = data[3..^1];
// CORRECT: tuple deconstruction
var (x, y, z) = GetPosition();
// CORRECT: string interpolation with alignment and format specifiers
LogToConsole($"Health: {health,6:F1} | Hunger: {hunger,6:F1}");