【.NET8】訪問私有成員新姿勢UnsafeAccessor(上)

前言

前幾天在.NET性能優化羣裏面,有羣友聊到了.NET8新增的一個特性,這個類叫UnsafeAccessor,有很多羣友都不知道這個特性是幹嘛的,所以我就想寫一篇文章來帶大家瞭解一下這個特性。

其實在很早之前我就有關注到這個特殊的特性,但是當時.NET8還沒有正式發佈,所以我也沒有寫文章,現在.NET8已經RC了,很快就會發布正式版,而且剛好有了一些時間,所以也可以帶大家瞭解一下這個新的特性。

由於篇幅原因,這篇文章會分爲上下兩篇,其中上篇會帶大家瞭解UnsafeAccessor是幹嘛的,有哪些用法,下篇會帶大家瞭解UnsafeAccessor的性能比較以及它的實現原理。

首先在我們瞭解這個類之前要假設一個場景,在很多時候我們都會遇到這樣的場景,就是我們需要在一個類中訪問另外一個類的私有成員,比如有如下代碼:


var a = new A();
Console.WriteLine(a._value);

public class A
{
    private int _value = 10;
}

在上面的代碼中,我們在類B中訪問了類A的私有成員_value,這種情況在我們的實際開發中是很常見的,但是在.NET中是不允許的,因爲私有成員是不允許被外部訪問的,所以我們在類B中是不能訪問類A的私有成員_value的,但是在實際的開發中,我們有時候確實需要訪問類A的私有成員_value,這個時候我們該怎麼辦呢?下面我們來看一下如何訪問私有成員。

.NET8以前的解決方案

在.NET8之前,我們可以通過如下的幾種方法來訪問私有成員,分別是反射、Emit、Expression,下面我們分別來看一下這幾種方法。

反射

在.NET中,有一種叫反射的技術,這個對於任何一個.NET開發工程師應該都不陌生,我們可以通過反射來訪問程序集的元數據信息,調用方法,訪問字段等等,所以可以通過反射來訪問私有成員,比如下面的代碼我們可以通過反射來訪問私有成員,如下所示:


var a = new A();
// 反射訪問私有成員
var value = typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(a);
Console.WriteLine(value);

public class A
{
    private int _value = 10;
}

在上面的代碼中,我們通過反射來訪問了類A的私有成員_value,這樣就可以訪問到了,但是這種方式有一個缺點,就是性能比較差,因爲反射是通過查找元數據和臨時調用來實現的,所以性能比較差。在實際的開發中,我們一般不會使用反射來訪問私有成員。

Emit

Emit 是 .NET 提供的一種動態生成和編譯代碼的技術。通過 Emit,我們可以動態生成一個新的方法,這個方法可以直接訪問私有成員,這樣就避免了反射的性能問題。以下是一個使用 Emit 訪問私有成員的例子:


var a = new A();

// 創建一個動態方法,簽名爲 int GetValue(A a)
var method = new DynamicMethod("GetValue", typeof(int), new Type[] { typeof(A) }, typeof(A));

// 獲取方法的 ILGenerator,通過 Emit 生成方法體
var il = method.GetILGenerator();

// 將參數 a 的私有成員 _value 壓入堆棧
il.Emit(OpCodes.Ldarg_0);

// 將私有成員 _value 壓入堆棧
il.Emit(OpCodes.Ldfld, typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance));

// 返回棧頂的值
il.Emit(OpCodes.Ret);

// 通過 Emit 創建的方法,可以直接訪問私有成員 _value
var func = (Func<A, int>)method.CreateDelegate(typeof(Func<A, int>));

// 調用方法
var value = func(a);
Console.WriteLine(value);

public class A
{
    private int _value = 10;
}

在上面的代碼中,我們通過 Emit 創建了一個新的方法,這個方法可以直接訪問類 A 的私有成員 _value。這種方法的性能比反射好很多,但是代碼比較複雜,不易於維護。

Expression

Expression 是 .NET 提供的一種表達式樹的技術。通過 Expression,我們可以創建一個表達式樹,然後編譯這個表達式樹,生成一個可以訪問私有成員的方法。以下是一個使用 Expression 訪問私有成員的例子:


var a = new A();

// 創建一個表達式樹,訪問私有成員 _value
var parameter = Expression.Parameter(typeof(A), "x");

// 訪問私有成員 _value
var field = Expression.Field(parameter, typeof(A).GetField("_value", BindingFlags.NonPublic | BindingFlags.Instance));

// 編譯表達式樹,生成一個可以訪問私有成員 _value 的方法
var lambda = Expression.Lambda<Func<A, int>>(field, parameter);

// 調用方法
var func = lambda.Compile();
var value = func(a);
Console.WriteLine(value);

public class A
{
    private int _value = 10;
}

在上面的代碼中,我們通過 Expression 創建了一個表達式樹,然後編譯這個表達式樹,生成了一個可以訪問類 A 的私有成員 _value 的方法。這種方法的性能比反射好,代碼也相對簡單,但是仍然比直接訪問複雜。

.NET8的解決方案

我想很多聰明的小夥伴都已經猜到了,在.NET8中新增的UnsafeAccessor就是用來訪問私有成員的,我們可以通過UnsafeAccessor來訪問私有成員,下面我們來看一下如何使用UnsafeAccessor來訪問私有成員。

私有字段

using System.Runtime.CompilerServices;

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
static extern ref int GetValue(A a);

var a = new A();
var value = GetValue(a);
Console.WriteLine(value);

public class A
{
    private int _value = 10;
}

首先我們需要引入System.Runtime.CompilerServices這個命名空間,然後定義一個staic extern ref方法,這個方法的返回值類型是字段的類型,然後它的參數就是對應實例的類型。在方法上面我們需要添加一個UnsafeAccessor特性,這個特性有一個參數UnsafeAccessorKind,這個參數表示我們要訪問的是什麼類型的私有成員,比如字段、屬性、方法等等,這裏我們要訪問的是字段,所以我們傳入的是UnsafeAccessorKind.Field,然後我們還需要指定要訪問的字段的名稱,這裏我們要訪問的是_value字段,所以我們傳入的是Name = "_value",這樣我們就可以通過UnsafeAccessor來訪問私有成員了。

來看一下運行的結果,可以看到和我們預期的一樣輸出了10

因爲它是返回了ref的引用,所以我們可以通過這個引用來修改私有成員的值,比如我們修改一下_value的值,如下所示:

using System.Runtime.CompilerServices;

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
static extern ref int ValueAccessor(A a);

var a = new A();
ref var value = ref ValueAccessor(a);
Console.WriteLine(value);

value = 20;
Console.WriteLine(ValueAccessor(a));

public class A
{
    private int _value = 10;
}

來看一下運行的結果,可以看到我們修改了_value的值,第二次輸出的時候就變成了20

私有構造方法

同樣的,使用UnsafeAccessor我們也可以訪問類中的私有構造方法和私有的方法,我們可以看到UnsafeAccessor有一個參數UnsafeAccessorKind,這個參數表示我們要訪問的是什麼類型的私有成員,比如字段、屬性、方法等等,下方是它的定義:

private enum UnsafeAccessorKind
{
    Constructor,
    Method,
    StaticMethod,
    Field,
    StaticField
};

先來看一下如何訪問私有構造方法,如下所示:

using System.Runtime.CompilerServices;

[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
static extern A CreateA(int value);

var a = CreateA(10);
Console.WriteLine(a.Value);

public class A
{
    public readonly int Value;

    private A(int value)
    {
        Value = value;
    }
}

在上面的代碼中,我們通過UnsafeAccessor訪問了類A的私有構造方法,這個私有構造方法的參數是int類型的,我們可以看到我們通過UnsafeAccessor訪問了私有構造方法,然後創建了一個A的實例,然後輸出了Value的值,可以看到輸出的結果是10,這樣我們就可以通過UnsafeAccessor來訪問私有構造方法了。

私有屬性

由於屬性是語法糖,編譯器會自動爲我們生成一個getset方法,比如public int Value {get; set;},就會自動生成一個get_Valueset_Value方法,我這裏就不單獨對訪問私有方法進行演示,直接演示訪問私有屬性,它和訪問私有方法是一樣的,如下所示:

using System.Runtime.CompilerServices;

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Value")]
static extern int GetValue(A a);

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Value")]
static extern void SetValue(A a, int value);

var a = new A();
SetValue(a, 10);
Console.WriteLine(GetValue(a));

public class A
{
    public int Value { get; set; }
}

在上面的代碼中,我們通過UnsafeAccessor訪問了類A的私有屬性Value,這個私有屬性有getset方法,我們通過UnsafeAccessor訪問了getset方法,然後我們就可以訪問私有屬性了,這樣我們就可以通過UnsafeAccessor來訪問私有屬性了。

侷限性

當然,現在使用UnsafeAccessor還有一些侷限性,大家在使用的過程中需要注意一下。

通用泛型

比如現階段它不支持通用泛型,像下面這樣的代碼是不支持的:

[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
static extern ref List<T> Field<T>(MyClass<T> _this);

但是現在可以寫成下方這樣的代碼:

[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
static extern ref List<string> StringField(MyClass<string> _this);

[UnsafeAccessor(UnsafeAccessorKind.Field, Name="_myList")]
static extern ref List<double> DoubleField(MyClass<double> _this);

不過這會在.NET9中解決,有興趣的小夥伴可以關注下方的鏈接:
https://github.com/dotnet/runtime/issues/89439

私有類型

比如有下面這樣的一個示例代碼:

// Assembly A
private class C
{
    private static int Method(int a) { ... }
}

// Assembly B
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
static extern int CallMethod(??? c, int a); 

這裏的問題是,我們不知道如何定義CallMethod的第一個參數,因爲C是私有的,我們無法在CallMethod的入參中定義它。

靜態類

比如有下面這樣的一個示例代碼:

// Assembly A
public static class C
{
    private static int Method(int a) { ... }
}

// Assembly B
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
static extern int CallMethod(??? c, int a); 

這裏的問題是,我們無法在B中定義CallMethod的第一個參數,因爲C是靜態的,我們無法在CallMethod的入參中定義它。

私有類參數

比如有下面這樣的一個示例代碼:

// Assembly A
public class C
{
    private class D { }
    private static int Method(D d) { ... }
}

// Assembly B
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name="Method")]
static extern int CallMethod(??? d); // Unable express D type as a parameter.

這裏的問題是,我們無法在B中定義CallMethod的入參,因爲D是私有的,我們無法在CallMethod的入參中定義它。包括目前還有私有返回值參數也是無法定義的。

但是這些問題在.NET9中也會解決,有興趣的小夥伴可以關注下方的鏈接:

https://github.com/dotnet/runtime/issues/90081

總結

在本文中,首先介紹了在.NET8之前訪問私有成員的幾種方法,包括反射、Emit、和Expression。這些方法雖然可以實現訪問私有成員,但是各有其優缺點,例如反射性能較差,Emit和Expression代碼複雜度較高。

隨後,我們詳細介紹了.NET8新增的特性UnsafeAccessor,這是一種更方便、更高效的訪問私有成員的方法。我們通過實例代碼演示瞭如何使用UnsafeAccessor訪問私有成員,包括私有字段、私有構造方法和私有屬性。並且,UnsafeAccessor還支持修改私有成員的值。

然而,UnsafeAccessor目前還存在一些侷限性,例如不支持通用泛型,無法訪問私有類型、靜態類和私有類參數。但是,這些問題預計在.NET9中將得到解決。

總的來說,UnsafeAccessor爲.NET開發者提供了一個新的工具,使我們能夠更方便、更高效地訪問私有成員。雖然當前還存在一些侷限性,但隨着.NET的不斷髮展和進步,我們有理由相信這些問題將會得到解決。同時,我們也期待.NET在未來能夠提供更多的功能和特性,以滿足我們日益增長的開發需求。

.NET性能優化交流羣

相信大家在開發中經常會遇到一些性能問題,苦於沒有有效的工具去發現性能瓶頸,或者是發現瓶頸以後不知道該如何優化。之前一直有讀者朋友詢問有沒有技術交流羣,但是由於各種原因一直都沒創建,現在很高興的在這裏宣佈,我創建了一個專門交流.NET性能優化經驗的羣組,主題包括但不限於:

  • 如何找到.NET性能瓶頸,如使用APM、dotnet tools等工具
  • .NET框架底層原理的實現,如垃圾回收器、JIT等等
  • 如何編寫高性能的.NET代碼,哪些地方存在性能陷阱

希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET性能問題和寶貴的性能分析優化經驗。目前一羣已滿,現在開放二羣。

如果提示已經達到200人,可以加我微信,我拉你進羣: ls1075

另外也創建了QQ羣,羣號: 687779078,歡迎大家加入。

抽獎送書活動預熱!!!

感謝大家對我公衆號的支持與陪伴!爲慶祝公衆號一週年,抽獎送出一些書籍,請大家關注公衆號後續推文!

image-20230703203249615

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章