續上篇,繼續來實作不同情境的 Prefix 與 Postfix。
解決原始方法多載的問題
擴充前述範例的 OriginalClass ,多載 DisplayMessage 方法:
public class OriginalClass
{
public static string DisplayMessage(string message)
{
Console.WriteLine("Executing Original Method...");
return $"Original Message : {message}";
}
public static string DisplayMessage(string message, int number)
{
Console.WriteLine("Executing Original Method with Number...");
return $"Original Message : {message}, Number: {number}";
}
}這種情況下,HarmonyPatch attribute 必須要定義補丁對象的參數,如果沒有定義會在呼叫 PathAll 方法的時候發生例外。假設要補丁的對象是帶有兩個參數 (string,int) 的多載,那補丁類別就要宣告如下,HarmonyPatch attribute 的第三個參數就是對應 DisplayMessage(string message, int number) 方法:
[HarmonyPatch(typeof(OriginalClass), nameof(OriginalClass.DisplayMessage), new Type[] {typeof(string), typeof(int)})]
public static class PatchOriginal
{
public static void Prefix()
{
Console.WriteLine("Prefix: Before the original method.");
}
public static void Postfix()
{
Console.WriteLine("Postfix: After the original method.");
}
}主程式內容如下:
static void Main(string[] args)
{
var harmony = new Harmony("com.example.sampleApp002");
harmony.PatchAll(Assembly.GetExecutingAssembly());
string result = OriginalClass.DisplayMessage("Hello, Harmony!", 100);
Console.WriteLine(result);
string result2 = OriginalClass.DisplayMessage("Hello, Harmony!");
Console.WriteLine(result2);
}從執行結果可以看出來只在執行 DisplayMessage(string message, int number) 前後有執行補丁;但 DisplayMessage(string message) 並沒有。[範例連結Sample002]
Prefix: Before the original method.
Executing Original Method with Number...
Postfix: After the original method.
Original Message : Hello, Harmony!, Number: 100
Executing Original Method...
Original Message : Hello, Harmony!Prefix 處理參數與回傳值
這個例子模擬了一個深度嵌套運算的挑戰:某核心私有方法負責最終的除法運算,但在特定邊界條件下,傳入的除數可能為 0。由於該方法位於無法修改的第三方套件深處,且呼叫鏈過於複雜,從源頭過濾數值變成一個艱深的挑戰。
傳統做法多採用 try-catch 進行補救,但 try-catch 向來有增加額外的例外處理開銷的惡名,且程式碼侵入性較高。透過 Harmony,藉由 Prefix 攔截機制,我們能在除法執行前預先檢查所傳入的參數內容,若除數為 0 則直接返回 0 並跳過原方法執行。這不僅解決了例外崩潰問題,也維持了呼叫端邏輯的純粹。
假設函式庫內有這麼一個類別,從外部呼叫公開的 DisplayMessage 方法時會傳入兩個 double 的引數,而DisplayMessage 方法內部 (可能會是深層嵌套的呼叫) 有機率讓傳給私有方法 Divide 的 y 值變成 0 (我們的假設情況是 y = a - b,也就是 a==b 時會造成 y = 0),從程式碼可以看出來若 y 為 0 會直接拋出例外:
public class OriginalClass
{
public static string DisplayMessage(string message, double a, double b)
{
double x = a;
/* 假設在程式碼內呼叫許多層的方法後計算後會讓 y=0 (if a==b) , 導致 Divide 方法拋出例外
這裡的計算只是為了模擬這種情況,實際上可能會有更複雜的邏輯導致 y=0 */
double y = a - b;
double result = Divide(x, y);
return $"Original Message : {message}, Result: {result}";
}
private static double Divide(double x, double y)
{
Console.WriteLine("Executing Original Method...");
if (y == 0)
{
throw new DivideByZeroException("Cannot divide by zero.");
}
return x / y;
}
}這個寫法當然是正確的,除以 0 拋出例外符合一般的概念。但外部呼叫者可能有個特定的邏輯是「當除以 0 的時候,結果就是 0」,此時採用 Prefix patch 來預先處理這個問題,俾使邏輯符合需求並且不耗費太多校能。
[HarmonyPatch(typeof(OriginalClass), "Divide")]
public static class PatchOriginal
{
public static bool Prefix(double x, double y, ref double __result)
{
if (y == 0)
{
Console.WriteLine("Prefix: Detected potential divide by zero. Modifying y to prevent exception.");
__result = 0;
return false; // Skip the original method
}
return true;
}
}- 要存取或更改原始方法的特定參數,只需在補丁方法中重複使用相同的參數名稱即可。
型別: 必須是可從原始參數型別賦值的(或直接使用 object)。
名稱: 必須與原有名稱相同,或使用 __n 形式(n 為從 0 開始的參數索引)。 - 補丁可以使用 __result 來存取回傳值。型別必須與原始回傳型別匹配或可被賦值。
在 Prefix 中: 因為原始方法尚未執行,__result 為該型別的預設值(參考類型通常為 null)。若要修改回傳值,需使用 ref。 - 回傳值型別為 bool,決定是否呼叫原始方法。Prefix 內部直接判斷 y 值是否為 0,若 y 值為 0 則直接設定回傳值為 0,並跳過原始方法呼叫。
註:__result 屬於 Harmony 注入機制的一部分,欲詳細了解可以參考 [Common injected values]
這個範例主程式與執行結果如下 [範例連結Sample003]:
static void Main(string[] args)
{
var harmony = new Harmony("com.example.sampleApp003");
harmony.PatchAll(Assembly.GetExecutingAssembly());
string message = "default message";
message = OriginalClass.DisplayMessage("Hello, Harmony!", 10, 5);
Console.WriteLine(message);
Console.WriteLine("-----------------");
message = OriginalClass.DisplayMessage("Hello, Harmony!", 10, 10);
Console.WriteLine(message);
}Executing Original Method...
Original Message : Hello, Harmony!, Result: 2
-----------------
Prefix: Detected potential divide by zero. Modifying y to prevent exception.
Original Message : Hello, Harmony!, Result: 0Prefix 與 Postfix 間傳遞狀態
讓我們暫時撇開 Benchmark.NET,假設我們想手動使用 Stopwatch 來量測特定方法的執行時間。在邏輯上,我們必須在 Prefix 中建立並啟動 Stopwatch 執行個體,接著在 Postfix 中停止它以取得耗費時間。(如果框架和C#版本允許,這種需求我會比較傾向用 Interceptor 來實作)
那麼,如何將 Prefix 中建立的執行個體傳遞給 Postfix 呢?這正是注入機制 __state 大顯身手的時候。透過在兩個補丁方法中定義同名的 __state 參數,Harmony 會自動幫我們完成執行個體的跨方法傳遞。
先建立一個跑起來夠久的 OriginalClass
public class OriginalClass
{
public static void LongTimeMethod()
{
Console.WriteLine("executing LongTimeMethod.....");
Enumerable.Range(0, 100000).Select(x => BigInteger.Pow(x, 10)).ToArray();
}
}建立補丁,使用 ref Stopwatch __state 作為 Prefix 與 Postfix 之間傳遞的橋樑。
[HarmonyPatch(typeof(OriginalClass), nameof(OriginalClass.LongTimeMethod))]
public static class PatchOriginal
{
public static void Prefix(ref Stopwatch __state)
{
Console.WriteLine("Stopwatch start.....");
__state = new Stopwatch();
__state.Start();
}
public static void Postfix(ref Stopwatch __state)
{
__state.Stop();
Console.WriteLine($"Stopwatch stop, elapsed {__state.ElapsedMilliseconds} ms");
}
} 主程式與其執行結果 [範例連結Sample004]:
static void Main(string[] args)
{
var harmony = new Harmony("com.example.sampleApp004");
harmony.PatchAll(Assembly.GetExecutingAssembly());
OriginalClass.LongTimeMethod();
}Stopwatch start.....
executing LongTimeMethod.....
Stopwatch stop, elapsed 42 ms到此暫時打住,下一篇繼續探討 Prefix 與 Postfix 的其他議題。