天天看點

執行個體方法和靜态方法有差別嗎?

作者:opendotnet

執行個體方法和靜态方法有差別嗎?對于很多人來說,這是一個愚蠢的問題。因為我們都知道它們的差別,執行個體方法作用于某個具體的上下文對象,該上下文對象可以利用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.ableContextAttribute::.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(!));
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);
}           

繼續閱讀