C# coding standards and code review. Use when the user mentions coding standards or conventions, writing code to spec, defining standards for a project, cohesion and coupling, design pattern selection, or naming conventions, or wants a code review on the design and standards dimension.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add tianzhiying/Unity-perf --skill code-standards --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Code Standards?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/tianzhiying-code-standards)More formats (shields.io, HTML) on the badges page.
---
name: code-standards
description: C# coding standards and code review. Use when the user mentions coding standards or conventions, writing code to spec, defining standards for a project, cohesion and coupling, design pattern selection, or naming conventions, or wants a code review on the design and standards dimension.
---
# C# coding standards (Unity client)
This skill has two uses: setting the standard while writing code (sections 1–3), and running the review protocol during a review (section 4).
Its scope is C# and Unity. The reference files also carry the rules for Go, C++, Java, and Lua — consult those only when you are explicitly reviewing one of those languages. The protocol structure carries over, but perspective C in step 1 (asset and configuration evidence) is Unity-specific and must be swapped for that language's equivalent evidence dimension.
The core of the review protocol is its structure — parallel multi-perspective passes plus an independent final review — not the checklist. It is more precise and covers more than a single agent reading straight through, so don't skip the protocol and just read the checklist.
## 0. Precedence and applicability
A project's existing conventions take precedence over this skill's defaults. When you pick up a repository, first look for `.editorconfig` and analyzer configuration, `gofmt` and lint configuration, `.clang-format`, checkstyle, and the project's own documentation (`CLAUDE.md`, README, `.cursor/rules/`, and so on). Standards are often written in non-`.md` extensions like `.mdc`, so read the repository index before asserting "there is no standard." Once found, it governs, and citing its clauses requires the verbatim line number.
Rules come in two tiers: violating one marked [HARD RULE] fails the review; ones marked [DEFAULT] apply when the project has no convention of its own.
Hot paths (frame loop, combat, network send/receive, high QPS) enforce all performance clauses, and any path that every user interaction must traverse falls under the same clauses. The cold-path exemption covers only editor tooling, one-off scripts, and initialization code.
## 1. Goals for every line you write
The goals must be met; the approach is yours.
1. **Peak algorithmic efficiency**: think about the optimal order of magnitude before you start. What can be precomputed isn't computed at runtime, what can be incremental isn't done in full, what can be event-driven isn't polled.
2. **Space discipline**: choose by access pattern, allocate by budget. Predictable capacities must be pre-set; caches must have a cap and an invalidation policy.
3. **Zero GC on hot paths**: zero allocation within a frame in C# — eliminate boxing, closures, LINQ, string concatenation, and temporary collections. For per-language specifics see [references/language-hard-rules.md](references/language-hard-rules.md).
4. **High cohesion, low coupling**: single responsibility, one-directional dependencies. For the criteria see [references/cohesion-coupling.md](references/cohesion-coupling.md).
5. **Consistent naming and style**: names must read as semantics, carrying units and direction. For C# specifics see [references/naming-conventions.md](references/naming-conventions.md).
## 2. Universal hard rules
Writing code and reviewing code share these criteria.
1. [HARD RULE] **Hot-path complexity declaration**: hot-path functions with two or more levels of nesting, or that process collections, state their complexity and the upper bound of N.
2. [HARD RULE] **No unexpected allocation within a frame or within a request.**
3. [HARD RULE] **One-directional dependencies**: lower layers don't reference upper layers; reverse dependencies are decoupled by pushing an event or interface downward.
4. [HARD RULE] **Single responsibility, expressible in one sentence.**
5. [HARD RULE] **Errors are never silent**: no empty catch, no ignored error, no logging-and-continuing into a bad state. Failure paths must produce UI or state feedback, or carry an explicit comment on why they're ignorable.
6. [HARD RULE] **Magic numbers get names**: when you see a magic number, search for its authoritative definition (protocol, constant table) first — using the wrong carrier is far worse than not naming it at all.
7. [HARD RULE] **Minimize mutable shared state**: globally writable singletons, public mutable fields, and bare cross-thread sharing are rejected outright.
8. [DEFAULT] Functions no longer than 50 lines, no more than 4 parameters, no more than 3 levels of nesting; comments explain *why*; public APIs carry doc comments.
9. [HARD RULE] **Lifecycle and async ordering**: for every combination of an async process (coroutine, tween, callback, timer) and a state flag, check the abort paths one by one. A callback that outlives its host needs a liveness guard on its first line. `?.` is banned on UnityEngine.Object. Any entry point that fires a request on tap needs in-flight protection that resets on both success and failure. Cleanup order must be reviewed — base before self, `Clear()` before iterating, and similar patterns are "done but ineffective."
10. [HARD RULE] **Signal source trustworthiness**: event-driven progression requires a bidirectional audit for false negatives and false positives (under what conditions is it not fired, under what conditions is it fired wrongly). Global broadcasts are checked subscriber by subscriber.
11. [DEFAULT] **Consistent handling of the same state across multiple sites**: when you find twin implementations, list the feature points first and compare them item by item — don't report only the difference you happened to bump into.
For design judgments (pattern selection, collection selection, module boundaries) see [references/design-patterns.md](references/design-patterns.md) and [references/algorithm-efficiency.md](references/algorithm-efficiency.md).
## 3. Output when writing code or defining standards
**When writing code**, state the applicable scope before you start (language, hot/cold determination, project conventions being followed). Hot-path deliverables carry a complexity declaration and their allocation profile. New modules carry their responsibility boundary, dependency direction, and interface surface.
**When defining standards**, first survey the repository's current state and produce three columns: keep / add / correct. The deliverable must include toolchain configuration — documentation alone counts as incomplete.
## 4. Review execution protocol
Don't read it all yourself from top to bottom. A single read-through has limited coverage and high variance. Execute the five steps below; none may be skipped.
### Step 0: Recon (do this yourself)
Get three things: the file list with line counts; how many call sites the module's entry point has repo-wide (zero callers means the module is unreachable — put this at the top of the report, it determines severity); and the real paths of the project's own standards files.
### Step 1: Four perspective subagents, dispatched in one wave, running in parallel
Each perspective handles only its own scope. The prompt gives the target path, the read-only constraint, that perspective's checklist, and the uniform output format `P-level | one-sentence conclusion | file:line | trigger path / evidence`. Better to report less than to report anything whose source line you did not see with your own eyes.
Every perspective must **enumerate the full set first, then check each member**: exhaustively list this perspective's review objects (all request sites / all async entry points / all serialized fields / all external callers) in the simplest format at the top of the report — names and line numbers only, no descriptions, kept to a dozen or so lines — then give a conclusion per item. **Failing to list is worse than failing to judge**: a wrong judgment at least leaves a trace, while an omission from the list is silent. This step costs almost no tokens yet converts "whatever I bumped into" into "enumerate, then eliminate one by one."
**Perspective A — data flow and protocol contracts**: do the endpoint and the semantics match (compare against the correct usage in a twin elsewhere in the repo)? Response failure paths (what does the network layer call back on failure, does the first line handle it)? Three-state inventory of DTO and config fields (defined, written, read — zero reads means that business shape doesn't exist)? Check every referenced config-table ID against the actual table, and write the "currently effective value" into the conclusion wherever you can find it. Trace parameters to their source (no assignment site repo-wide means it is permanently the default). Client-side local rewriting of authoritative data is high-risk and gets its own entry.
**Perspective B — lifecycle and async ordering**: work through hard rules 9 and 10 item by item; what does each of the three reset moments (switching tabs, reopening the panel, switching accounts) actually reset; do in-flight flags reset on the failure path; cleanup ordering; the rapid-tap window.
**Perspective C — asset and configuration evidence**: you must actually open the serialized files — do prefab references contain unassigned `{fileID:0}` or dangling GUIDs; do asset paths referenced in code exist on disk; do localization keys exist with non-empty values; recompute layout numbers (derive the real rect from anchor and pivot, look for overlap and overflow); read the *current measured* values of compile macros and project settings, noting that editing values inside a platform override does nothing when that override isn't enabled. Also walk up the call chain once and read the shared base classes and framework implementations this module stands on — defects often live there, and one fix multiplies across the whole project.
**Perspective D — cross-module consistency and project standards**: for twin modules, list the feature points first and compare item by item; explicit standards violations carry the verbatim line number; cross-view and cross-account contamination via global and static state; dead-code inventory (zero callers, zero reads, commented-out call sites, constants pointing at nonexistent assets); bidirectional checking of event publishers against subscribers.
### Step 2: Merge and deduplicate (do this yourself)
Deduplicate by "file:line + substance of the problem," keeping the phrasing with the most complete evidence chain, and mark entries that two or more perspectives hit independently — those are higher-confidence and sort first.
### Step 3: Adversarial final review
Dispatch an independent subagent that took no part in the earlier work. Don't do it yourself, and don't skip it. It does four things: cross-reconciliation; evidence checks on P0 and P1 (does each carry "trigger path + current-value evidence (file:line)"; downgrade to P2 if not); the seven false-positive checks plus opposing-case argument for P0/P1; and a severity re-review item by item (judge over- or under-rating against the three anchors at the end of this section and correct it directly). The final review's conclusions must be enforced — killed entries may not appear in the final report.
**Cross-reconciliation compares entries against each other; it is not a per-entry pass.** The seven checks and the opposing-case argument are both per-entry, and a per-entry pass cannot see the following three classes, so this must be done as its own sweep.
1. **Gate swallowing**: does a gate, early-out, switch, or always-false guard reported by one entry make the paths described by other entries structurally unreachable? A report that surfaced the gate itself yet failed to notice the gate kills several of its own other entries is the single largest source of false positives.
2. **Mutually exclusive symptoms**: two entries describing visible symptoms of the same object or widget at the same moment — can both hold? The classic shape is one entry saying a widget will display something and another saying that widget's renderer is disabled and it doesn't render at all; both cannot be true of the same widget in the same frame. The method is to group the list by the object or widget involved and check symptom pairs within each group for contradictions.
3. **Cluster by repair action**: group all entries by "which single code or asset change resolves it." Multiple entries in one group are one root cause split apart — merge them into one (listing all affected locations in the description). This step must be an actual grouping pass; asking per entry "is this a duplicate of an earlier one?" has a high miss rate.
**The seven false-positive checks** are the fixed sources of false positives; run every entry through them, and delete or downgrade on a hit. The first six are systematic blind spots of the model rather than random noise; the seventh's failure mechanism is task-independent and is included for that reason.
1. **Same root cause split apart**: two entries are the same missing thing said two ways or framed from two angles — merge them into one and don't count them separately. The test is whether the repair is the same single change.
2. **Self-admitted unreachability**: the entry's own reasoning says "structurally impossible in the current design," "never reached," "this component has zero mounts," or "no actual consequence," yet it is still listed as a defect. Either delete it or downgrade to P3 with an explicit "only holds once X is wired up." This one can be caught by plain text scanning — sweep for it first, then go entry by entry.
3. **Untriggerable exceptions**: asserting "this will throw X" when that branch is unreachable given the actual arguments at this call site (for example, asserting that formatting will throw a format exception when the interpolated argument contains no format specifiers at all). The method is to substitute the real argument values and walk it through; delete the unreachable ones.
4. **Fabricated authority**: citing a project standards clause, a compiler diagnostic ID, or a framework behavior contract requires opening the original or confirming by measurement. Common failures are asserting "not registered in registry X" when the standard registers it in black and white, or misremembering a compiler diagnostic ID. The test: every citation must be able to quote the actual line; if you can't quote it, withdraw the citation. If the conclusion still stands on its own it may remain, but it may not lean on false authority.
5. **State reachability not walked end to end**: asserting "when object A is in state S it causes X" requires listing which line produces S, which line consumes it, and which branches intercept in between. For containers and pooled objects you must explicitly distinguish the "registry collection" from the "buffer collection" — mistaking a temporary buffer pool for the registry list you iterate produces a null reference that is entirely unreachable. The test is that you give line numbers for all three of the null-out point, the recycle point, and the reuse point; missing any one counts as unproven.
6. **Missing downstream self-healing**: when asserting "this bad value will persist" or "this object will become invalid," you must search every subsequent write site for that value and the destruction ordering of that object. Common misses: a cache miss auto-fetches and broadcasts; the cleared listener and its host are destroyed together; the refresh entry point synchronously initializes before updating the view. In one sentence: "it will stay wrong" requires proving that nobody sets it back — not that it went wrong.
7. **Wrongly declaring something dead** (the opposite direction from check 2 — don't confuse them): check 2 covers "reported despite saying it's unreachable"; this one covers declaring live code dead. Declaring something dead grows an irreversible action ("recommend deletion"), so **the evidence bar must be higher than for declaring it alive.** Before declaring anything dead, enumerate and state which carriers you checked: GUID references in `.prefab` / `.unity` / `.asset` (including the module's own sibling files), `AddComponent` and reflection and string-name lookup in code, config tables and registries (including binary tables — don't search only text), hot-update and scripted calls, and indirect dispatch at the framework layer (events bubbling upward, base classes auto-adding components during initialization, message dispatch). If any one of these went unchecked you may not write "zero references" or "never executes"; rewrite it as "no reference found among the X classes of carrier I checked." The two most common wrecks: limiting the search scope outside the target directory and thereby missing the nearest sibling files; and forgetting that a UGUI click bubbles up from child nodes to the nearest handler, which makes a normally-triggered entry point look like it never fires.
**File-level coverage inventory** is a near-zero-cost way to catch omissions: take the complete file list from step 0, compare it against the findings list, and see which files have no findings at all. Open and read each such file yourself and decide whether it is "genuinely clean (with the basis)" or "nobody checked it" — for the latter, check it on the spot. Turning up a real defect in a zero-finding file is routine, and the cost is reading a few usually-short files.
**P0/P1 opposing-case argument** inverts the burden of proof: for every P0 or P1, the final review is not "check whether there's a problem" but actively constructing an argument that the entry does *not* hold — go find the guard, the unreachability, the downstream correction, the framework fallback. The entry passes only when that construction fails; when it succeeds, delete or downgrade it and record the counter-evidence line number. The difference from a checklist is the motive: a checklist says "I found no problem, so it passes"; an opposing-case argument says "I tried hard to refute it and couldn't, so it passes." Do this only for P0/P1 — P2 and P3 go through the seven checks, which keeps the volume manageable. Don't overcorrect: refutations must rest on facts in the source, and killing entries on evidence-free suspicion ("theoretically there might be another guard") is forbidden, because the cost of killing a real finding exceeds the cost of letting a false positive through.
### Step 4: Output
**No cap on the number of entries**: report every finding that survived the final review, with no top-N selection and no "appendix" or "the rest omitted." Removing the cap catches significantly more real issues at nearly identical cost — provided the final review's four anti-padding checks are actually run. Skipping them and removing the cap loses precision; those four checks exist precisely to counter those four losses.
Sort by severity descending. P0 and P1 must carry `trigger path: ...; current-value evidence: file:line`; anything that can't produce it is automatically downgraded to P2.
Three closing sections: **shared root-cause consolidation** (fixing A also resolves B and C, so fixing B alone is insufficient; where there is a dependency chain, state the repair order); **negative results** (directions you checked and confirmed clean, with the evidence standard used, so nobody redoes the elimination); and **verification data notes** (defects masked by a coincidence in the current data — call out "the current data shows no difference; only data X exposes it").
Finally append one line with the final review's disposition counts: `cross-reconciliation merged/deleted a; seven checks deleted b; opposing-case deleted or downgraded c; severity corrected d; file inventory added e`. This line is the basis for deciding which stage can be skipped next time, and may not be omitted.
### Severity definitions and the three rating anchors
- **P0** = happening in production right now, or unconditionally reproducible on the code path; with real user loss, or financial or account-security risk.
- **P1** = reproducible under specific conditions with severe consequences (feature unusable, hang, data corruption), but those conditions may not currently hold.
- **P2** = a real defect, but narrowly triggered or with manageable consequences (experience blemish, performance cost, latent hazard).
- **P3** = a standards or maintainability issue with no direct functional consequence.
Three anchors target the most common rating mistakes; everyone applies them uniformly.
1. **Unreachable modules**: "unreachable" gets its own single entry; the remaining entries are still rated by "is it unconditional once the condition holds + how severe is the consequence," and may not be downgraded across the board. The reverse holds too: consequences that depend on "a config or widget that doesn't currently exist being added later" are rated P3 and may not be lifted to P1 on the grounds that "it'll be added eventually."
2. **Mock and placeholder data domains**: something happening right now but losing only mock currency or fake data has manageable consequences — rate P2. "Unconditionally reproducible with severe consequences once the switch is flipped to real" is P1. P0 requires both "right now" and "real loss."
3. **No downgrading for dependency chains**: "it's blocked by another bug's gate and only surfaces after that one is fixed" is not grounds for a downgrade. P1's definition already permits the condition not currently holding, and one more layer of precondition doesn't change the "unconditional once the condition holds" nature — unless that precondition makes the consequence itself disappear. The correct handling is to keep it at P1 and state the repair dependency order in the root-cause consolidation.
### Evidence discipline
Violating any one of these means the item is incomplete. Confirm the line with your own eyes before citing it. "Not verified" must state where you got blocked, and anything readable inside the repo doesn't qualify as unverifiable. When you can find the "current actual value" (a switch, a config, a season), don't stop at "under some condition it would." Self-produced data (binary decoding, counts) carries its self-validation basis. Before asserting "this will crash," trace where the exception lands (a bare stack is a crash; being caught is a silent failure — the two symptoms lead in completely different directions). When a conclusion is refuted or mitigated, check the same spot again from another angle before moving on — you zeroed in on it usually because a layer really is missing; you just guessed the wrong layer.
## 5. Division of labor with sibling skills
- Performance localization, Profiler workflow, engine-layer optimization → unity-performance
- The limits of C# micro-optimization (SIMD, unsafe, memory layout) → the L0–L5 ladder in section 5.2 of unity-performance
- Dedicated zero-GC remediation → csharp-zero-gc
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!