(Martin Fowler's Example)
1. 積極使用 Guard Clause(保護語句)
"如果條件不滿足,立即返回。將核心邏輯放在最少縮進的地方。"
概念定義
Guard Clause(保護語句) 是一種在函數開頭檢查特定條件是否滿足,如果不滿足則立即退出(return) 的方法。
它的目的是減少不必要的 if
嵌套,使代碼更加線性和平坦(flat) 。
Before(不好的示例)
if (user != null)
{if (user.IsActive){Process(user);}
}
-
代碼嵌套過多,核心邏輯
Process()
被隱藏在最內層。 -
隨著條件的增加,代碼會形成“金字塔代碼(Pyramid of Doom)”,變得越來越復雜。
-
當測試條件增多時,代碼覆蓋率和調試的可讀性都會變差。
After(好的示例)
if (user == null) { return; }
if (!user.IsActive) { return; }Process(user);
-
在條件不滿足的情況下,通過提前返回 快速整理流程。
-
核心邏輯
Process()
位于最少縮進的中央部分 ,可讀性更高。 -
在測試/重構時,可以輕松追蹤條件與結果 。
反對意見:“否定條件違背直覺”
一些開發者可能會提出以下觀點:
“比起
if (!condition) return;
,if (condition) { ... }
更加直觀。
否定條件會讓代碼難以理解。”
回應反對意見:
-
關鍵在于條件的正/負,而不是‘意圖’和‘重要性’的排序。
-
如果“在這種情況下不應該執行操作”是明確的,那么否定條件更為清晰 。
指南:
-
不重要的條件 = 否定形式 + 提前返回
-
重要的條件 = 肯定形式 + 執行核心邏輯
補充提示:Guard 子句不僅可以使用 return
,還可以使用 throw
或 continue
if (value == null) { throw new ArgumentNullException(nameof(value)); }foreach (var item in collection)
{if (item.IsEmpty) { continue; }Process(item);
}
什么時候應該積極使用 Guard 子句?
情況 | 描述 |
---|---|
包含大量驗證的函數 | 通過提前返回來清理條件 |
包含許多狀態分支的循環 | 使用 |
存在未檢查的異常風險 | 清理 |
方法開始變長時 | 使用 Guard 子句整理條件過濾,簡化代碼 |
實戰技巧
-
Guard Clause 通過去除不必要的嵌套 + 提前退出 ,使代碼變得更加平坦(flat) 。
-
這是一種設計策略,旨在將核心邏輯放在縮進最少、最顯眼的位置。
-
條件越多 、失敗案例越明確 、測試越復雜 ,Guard Clause 的優勢就越明顯。
Dictionary<TKey, TValue> 在內部使用哈希表(Hash Table) 實現。
因此,鍵查找的平均時間復雜度為 O(1) ,這在很多情況下比 switch-case 更快
2. 戰略性地將 Switch 轉換為 Dictionary 映射
"條件分支越復雜,代碼的責任應被分解得越清晰。"
概念定義
在 C# 中,switch-case
語句對簡單的分支處理非常有用,但當分支數量增加時,在維護性和擴展性方面會暴露出局限性 。
在這種情況下,使用 Dictionary<Enum, Action>
或 Dictionary<Enum, Func<T>>
構建顯式映射 ,可以顯著提升可讀性和功能擴展性 。
Before(不好的示例)
switch (state)
{case UserState.Idle:HandleIdle();break;case UserState.Running:HandleRunning();break;case UserState.Dead:HandleDead();break;default:throw new InvalidOperationException();
}
-
隨著分支增多,代碼變得冗長。
-
如果在
switch
內部處理過多邏輯,容易違反單一職責原則(SRP) 。 -
當新增狀態時,需要找到對應的分支、添加邏輯并測試,修改分散且繁瑣。
After(好的示例)
private static readonly Dictionary<UserState, Action> stateHandlers = new()
{{ UserState.Idle, HandleIdle },{ UserState.Running, HandleRunning },{ UserState.Dead, HandleDead }
};public void Handle(UserState state)
{if (stateHandlers.TryGetValue(state, out Action action)){action.Invoke();}else{throw new InvalidOperationException($"No handler for {state}");}
}
-
狀態與操作的關系通過映射明確管理 。
-
新增狀態時只需在字典中注冊即可完成。
-
測試時可以獨立驗證每個處理器。
使用場景示例
UI 狀態機(State Machine)
Dictionary<GameState, Action> renderState = new()
{{ GameState.MainMenu, DrawMainMenu },{ GameState.InGame, DrawGame },{ GameState.Paused, DrawPauseScreen },
};
什么時候適合使用?
情況 | 描述 |
---|---|
基于 Enum 的分支較多時 |
|
命令/輸入處理分支 | 鍵/按鈕輸入 → 動作映射 |
狀態機 | 狀態 → 行為對應結構 |
需要分離出可測試的邏輯時 |
|
缺點及注意事項
缺點 | 應對措施 |
---|---|
未注冊的鍵沒有對應處理 |
|
不保證順序 | 使用 |
復雜條件分支難以處理 | 僅適用于簡單條件邏輯,復雜邏輯仍需使用 |
實戰技巧
-
命令模式(Command Pattern)的簡易實現
可以像Dictionary<string, ICommand>
一樣,將命令和執行對象進行映射。 -
向函數式編程(FP)靠攏的信號
基于字典的映射實際上是將代碼轉換為數據驅動的表格式結構 ,這與 FP(函數式風格命令調度)的理念更為接近。
(MSDN Magazine Issues Volume 32 Number 3)
(Read only, frozen, and immutable collections - Developer Support)
3. 不可變數據(Immutable Data)習慣
“調試地獄從何開始?——正是從那些意外的值變更開始。”
概念定義
不可變(Immutable)數據 是指一旦定義后,其值不會改變的狀態 。
在 C# 中,可以通過 readonly
、const
、record
等方式實現有意圖的不可變設計 。
Before(不好的示例)
public class Player
{public int health;public void TakeDamage(int amount){health -= amount;}
}
- 在這種結構中,很難追蹤
health
是在哪里被修改的。 - 特別是在多線程或事件驅動系統中,副作用 的累積會使調試難度急劇上升。
After(好的示例)
public class Player
{private readonly int maxHealth = 100;private int currentHealth;public int GetHealth() => currentHealth;public void SetHealth(int value){currentHealth = Math.Clamp(value, 0, maxHealth);}
}
maxHealth
是一個永遠不會改變的常量 → 使用readonly
聲明。- 狀態修改僅通過
SetHealth()
方法完成 → 訪問控制與不可變性分離 。
為什么在游戲開發中很重要?
如果狀態(State)變更以不透明的方式擴散 :
- UI 更新延遲
- Bug 不規則發生
- 多人環境中同步問題
解決方法:將狀態本身建模為不可變對象 進行管理。
public readonly struct PlayerState
{public readonly int health;public readonly bool isDead;public PlayerState(int health, bool isDead){this.health = health;this.isDead = isDead;}
}
狀態不是用來修改的,而是‘重新創建’的。→ 函數式編程模式
Unity 開發者可以這樣使用
- 使用
ScriptableObject
存儲配置數據時 → 只讀結構化 - 避免使用
public int value;
形式,改為沒有 setter 的SerializedField
- 推薦將狀態對象存儲在不可變的
struct + Copy-on-Write
模式中,而不是放在可變的 MonoBehaviour 中。
[SerializeField] private int initialHealth = 100;public int InitialHealth => initialHealth; // 只允許讀取
從 C# 9 開始引入了 record
record
默認是不可變對象 。- 使用
with
表達式進行修改時會創建新對象(immutable-safe) 。
var newStatus = status with { Health = status.Health - 10 };
高級技巧:并行處理與架構設計建議
readonly
+ volatile
組合
private volatile bool isGameOver;
private readonly object lock = new();public void SetGameOver()
{lock (lock){isGameOver = true;}
}
- 將看似不可變的值在多線程環境中保持一致性保護 。
- 這種組合也常用于雙重檢查模式。
注意:所有內容都必須不可變嗎?
- 如果在游戲循環中性能至關重要的結構 ,需要考慮
struct
不可變對象的創建成本。 - 對于需要頻繁狀態變更的對象,可以采用“內部不可變性(internal immutability)”作為折衷方案。
什么時候適合使用不可變模式?
場景 | 描述 |
---|---|
需要跟蹤狀態時 | 難以追蹤狀態變更的結構會導致調試噩夢 |
線程間共享數據 | 不可變性設計可以在無鎖的情況下保證穩定性 |
可測試的狀態建模 | 基于對象復制的測試和時間點比較更加容易 |
UI 狀態更新 | 在 ViewModel 中便于變更跟蹤和綁定 |
實戰技巧
- 不可變數據是降低調試成本的最佳結構
- 狀態不應修改,而應替換 :覆蓋對象的方式對追蹤和恢復更有利
- 在 C# 中,可以通過
readonly
、record
和ScriptableObject + Getter
設計來實現。
結論:
我們了解了在 C# 中經常使用的基礎重構方法。
實際上,詳細撰寫這種入門級別的文章對我也有幫助,因此我重新整理了一遍。
除此之外,還有很多其他模式,但我選出了三個我認為重要的基礎概念。