實體方法和靜態方法有區別嗎?

2023-07-05 09:00:16

實體方法和靜態方法有區別嗎?對於很多人來說,這是一個愚蠢的問題。因為我們都知道它們的區別,實體方法作用於某個具體的上下文物件,該上下文物件可以利用this關鍵字獲得;靜態方法則是定義在某個型別中,不存在上下文物件的概念。但是如果我們從函數的角度來看的話,不論是靜態方法還是實體方法都是一個用於處理輸入引數的操作,貌似又沒有什麼區別。

以如下這個用於封裝一個整數的IntValue型別為例,它具有兩個AsInt32方法,實體方法返回當前InValue物件的_value欄位;靜態方法將IntValue物件作為引數,返回該物件的_value欄位。我們的問題是:這兩個AsInt32方法有分別嗎?

var target = new IntValue(123);
target.AsInt32();
IntValue.AsInt32(target);

public class IntValue
{
    private readonly int _value;
    public IntValue(int value) => _value = value;

    public int AsInt32() => _value;
    public static int AsInt32(IntValue value) => value._value;
}

我們從IL的視角來看這兩個方法的宣告和實現。如下面的程式碼片段所示,從方法宣告來看,實體方法AsInt32和靜態方法AsInt32確實不同,但是它們的實現卻完全一致。方法涉及三個IL指令:ldarg.0提取第1個引數壓入棧中,具體入棧的是指向IntValue物件的地址;目標IntValue物件的_value欄位通過ldfld指令被載入,最終通過ret指令作為結果返回。實體方法也好,靜態方法也罷,它們都被視為的普通函數。函數只有輸入和輸出,並不存在所謂的上下文物件(this)。

.method public hidebysig
	instance int32 AsInt32 () cil managed
{
	// Method begins at RVA 0x2178
	// Header size: 1
	// Code size: 7 (0x7)
	.maxstack 8

	// return _value;
	IL_0000: ldarg.0
	IL_0001: ldfld int32 IntValue::_value
	IL_0006: ret
} // end of method IntValue::AsInt32
.method public hidebysig static
	int32 AsInt32 (
		class IntValue 'value'
	) cil managed
{
	.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
		01 00 01 00 00
	)
	// Method begins at RVA 0x2180
	// Header size: 1
	// Code size: 7 (0x7)
	.maxstack 8

	// return value._value;
	IL_0000: ldarg.0
	IL_0001: ldfld int32 IntValue::_value
	IL_0006: ret
} // end of method IntValue::AsInt32

實體方法實際上將目標物件作為它的第一個引數,這與顯式將目標物件作為第一個引數的靜態方法並沒有本質的區別,所以呼叫它們的IL程式碼也一樣。如下所示的就是上面C#針對這兩個方法的呼叫轉換生成的IL程式碼。

.method private hidebysig static
	void '<Main>$' (
		string[] args
	) cil managed
{
	// Method begins at RVA 0x213c
	// Header size: 12
	// Code size: 23 (0x17)
	.maxstack 1
	.entrypoint
	.locals init (
		[0] class IntValue target
	)

	// IntValue intValue = new IntValue(123);
	IL_0000: ldc.i4.s 123
	IL_0002: newobj instance void IntValue::.ctor(int32)
	IL_0007: stloc.0
	// intValue.AsInt32();
	IL_0008: ldloc.0
	IL_0009: callvirt instance int32 IntValue::AsInt32()
	IL_000e: pop
	// IntValue.AsInt32(intValue);
	IL_000f: ldloc.0
	IL_0010: call int32 IntValue::AsInt32(class IntValue)
	IL_0015: pop
	// }
	IL_0016: ret
} // end of method Program::'<Main>$'

由於實體方法和靜態方法的「無差異性」,我們可以使用一些Hijack的方式「篡改」現有某個型別的實體方法。比如我們在IntValue型別(可以定義任意型別中)中定義了一個總是返回int.MaxValue的AlwaysMaxValue方法。在演示程式中,我們通過呼叫Hijack方法將IntValue的實體方法AsInt32「替換」這個AlwaysMaxValue方法。

var target = new IntValue(123);
Hijack(()=>target.AsInt32(), () => IntValue.AlwaysMaxValue(null!));
Debug.Assert(target.AsInt32() == int.MaxValue);

public class IntValue
{
    private readonly int _value;
    public IntValue(int value) => _value = value;
    public int AsInt32() => _value;
    public static int AsInt32(IntValue value) => value._value;

    public static int AlwaysMaxValue(IntValue _) => int.MaxValue;
}

如下所示的就是這個Hijack方法的定義。它的兩個方法表示呼叫原始方法和篡改方法的表示式,我們利用它們得到對應的MethodInfo物件。我們利用MethodHandle得到方法控制程式碼,並進一步利用GetFunctionPointer方法得到具體的指標地址。有了這兩個地址,我們就可以計算出它們之間的偏移量,然後利用Marshal.Copy方法「篡改」了原始方法的指令。具體來說,我們將原始方法的初始指令改為跳轉指令JUMP,通過設定的偏移量跳轉到新的方法。

static void Hijack(Expression<Action> originalCall, Expression<Action> targetCall)
{
    var originalMethod = ((MethodCallExpression)originalCall.Body).Method;
    var targetMethod = ((MethodCallExpression)targetCall.Body).Method;

    RuntimeHelpers.PrepareMethod(originalMethod.MethodHandle);
    RuntimeHelpers.PrepareMethod(targetMethod.MethodHandle);

    var sourceAddress = originalMethod.MethodHandle.GetFunctionPointer();
    var targetAddress = (long)targetMethod.MethodHandle.GetFunctionPointer();

    int offset = (int)(targetAddress - (long)sourceAddress - 5);

    byte[] instruction = {
        0xE9, // JUMP
        (byte)(offset & 0xFF),
        (byte)((offset >> 8) & 0xFF),
        (byte)((offset >> 16) & 0xFF),
        (byte)((offset >> 24) & 0xFF)
    };

    Marshal.Copy(instruction, 0, sourceAddress, instruction.Length);
}