Đây là bài viết thứ 9 trong series: Khám phá bản preview .NET 10.
Trong bài viết này, tôi sẽ mô tả một số cải tiến của cơ chế [UnsafeAccessor] được giới thiệu trong .NET 8. [UnsafeAccessor] cho phép bạn dễ dàng truy cập các trường riêng tư và gọi các phương thức riêng tư của các kiểu, mà không cần sử dụng các API reflection. Trong .NET 9 có một số hạn chế về các phương thức và kiểu mà cơ chế này hoạt động. Trong .NET 10, một số khoảng trống đó đã được lấp đầy bằng việc giới thiệu [UnsafeAccessorType], và chúng ta sẽ xem xét những cải tiến này trong bài viết.
Mục lục
Gọi các thành viên riêng tư với [UnsafeAccessor] trong .NET 8 và 9
Cơ chế [UnsafeAccessor] được giới thiệu trong .NET 8, với hỗ trợ cho generics được thêm vào .NET 9.
Ví dụ, giả sử bạn muốn truy cập trường riêng tư _items trong List<T>:
public class List<T>
{
T[]? _items;
// .. other members
}
Bạn có thể làm điều này bằng reflection với code như sau:
// Get a FieldInfo object for accessing the value
var itemsFieldInfo = typeof(List<int>)
.GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance);
// Create an instance of the list
var list = new List<int>(16);
// Retreive the list using reflection
var items = (int[])itemsFieldInfo.GetValue(list);
Console.WriteLine($"{items.Length} items"); // Prints "16 items"
Để sử dụng [UnsafeAccessor], bạn phải tạo một phương thức extern đặc biệt, được trang trí với attribute, có chữ ký chính xác để truy cập thành viên bạn muốn. Trong trường hợp của một trường, điều đó có nghĩa là một phương thức nhận một tham số duy nhất của kiểu đích và trả về một thể hiện của kiểu đích của trường dưới dạng ref. Tên của phương thức tự nó không quan trọng.
// Create an instance of the list
var list = new List<int>(16);
// Invoke the method to retrieve the list
int[] items = Accessors<int>.GetItems(list);
Console.WriteLine($"{items.Length} items"); // Prints "16 items"
static class Accessors<T>
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
public static extern ref T[] GetItems(List<T> list);
}
Cũng cần lưu ý rằng chữ ký GetItems() trả về một ref (và nó phải như vậy khi bạn đang truy cập một trường), có nghĩa là bạn cũng có thể thay đổi trường. Ví dụ dưới đây sử dụng cùng phương thức truy cập nhưng lần này thay thế trường:
// Create an instance of the list
var list = new List<int>(16);
Console.WriteLine($"Capacity: {list.Capacity}"); // Prints "Capacity: 16"
// Invoke the method to retrieve the field ref and set the value of the field to an empty array
Accessors<int>.GetItems(list) = Array.Empty<int>();
Console.WriteLine($"Capacity: {list.Capacity}"); // Prints "Capacity: 0"
Tất nhiên, không chỉ các trường mà bạn có thể gọi, bạn cũng có thể gọi các phương thức (và do đó các thuộc tính) và các hàm tạo; bất cứ thứ gì được định nghĩa trên UnsafeAccesorType:
public enum UnsafeAccessorKind
{
Constructor,
Method,
StaticMethod,
Field,
StaticField,
}
Ví dụ sau đây cho thấy một ví dụ về việc gọi một phương thức tĩnh được định nghĩa trên List<T>:
// Invoking private static methods
// We can pass `null` as the instance argument because these are static methods
bool isCompat1 = Accessors<int?>.IsCompatibleObject(null, 123); // true
bool isCompat2 = Accessors<int?>.IsCompatibleObject(null, null); // true
bool isCompat3 = Accessors<int?>.IsCompatibleObject(null, 1.23); // false
static class Accessors<T>
{
// The method we're invoking has this signature:
// private static bool IsCompatibleObject(object? value)
//
// Our extern signature must include the target type as the first method parameter
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "IsCompatibleObject")]
public static extern bool CheckObject(List<T> instance, object? value);
}
Như bạn có thể thấy ở trên, đối với cả phương thức thể hiện và phương thức tĩnh, bạn bao gồm thể hiện “đích” làm tham số đầu tiên trên phương thức (và truyền null làm đối số khi gọi phương thức tĩnh). Bằng cách đó, runtime biết nó đang làm việc với Type nào: đó là bất cứ kiểu nào của đối số đầu tiên. Nhưng nếu bạn không thể chỉ định kiểu này, vì bản thân kiểu đó là riêng tư thì sao?
Hạn chế của [UnsafeAccessor] trong .NET 9
Hạn chế lớn xung quanh việc sử dụng [UnsafeAccessor] trong .NET 9 là bạn phải có thể tham chiếu trực tiếp tất cả các kiểu là một phần của chữ ký đích. Như một ví dụ cụ thể, hãy tưởng tượng một thư viện bạn đang sử dụng có một kiểu trông giống như thế này.
public class PublicClass
{
private readonly PrivateClass _private = new("Hello world!");
internal PrivateClass GetPrivate() => _private;
}
internal class PrivateClass(string someValue)
{
internal string SomeValue { get; } = someValue;
}
Hãy tưởng tượng bạn có một thể hiện của PublicClass nhưng thứ bạn thực sự cần là SomeValue được giữ trên trường _private. Tuy nhiên, PrivateClass được đánh dấu internal nên bạn không thể tham chiếu trực tiếp đến nó. Điều đó có nghĩa là không có cách truy cập nào trong số này sẽ hoạt động, vì không có cách nào để sử dụng PrivateClass trong chữ ký:
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_private")]
static extern ref readonly PrivateClass GetByField(PublicClass instance);
// 👆 ❌ Can't reference PrivateClass
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetPrivate")]
static extern PrivateClass GetByMethod(PublicClass instance);
// 👆 ❌ Can't reference PrivateClass
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_SomeValue")]
static extern string GetSomeValue(PrivateClass instance);
// 👆 ❌ Can't reference PrivateClass
Trong .NET 9, giải pháp duy nhất cho những vấn đề này là quay lại sử dụng reflection “thông thường”, sử dụng System.Reflection.Emit, hoặc sử dụng System.Linq.Expressions, tất cả đều chậm hơn đáng kể so với [UnsafeAccessor] và phức tạp hơn nhiều.
May mắn thay, trong .NET 10, chúng ta có cách để có được cả hai thế giới tốt nhất: chúng ta có thể sử dụng [UnsafeAccessor] ngay cả với các kiểu mà chúng ta không thể tham chiếu!
Truy cập các kiểu không được tham chiếu với [UnsafeAccessorType] trong .NET 10
Trong .NET 9, bạn phải có thể tham chiếu trực tiếp tất cả các kiểu được sử dụng trong chữ ký phương thức của một phương thức [UnsafeAccessor]. .NET 10 giới thiệu attribute [UnsafeAccessorType], cho phép chúng ta sử dụng [UnsafeAccessor] ngay cả với các kiểu mà chúng ta không thể tham chiếu.
Sử dụng [UnsafeAccessorType] với [UnsafeAccessor]
.NET 10 giới thiệu một attribute mới, [UnsafeAccessorType], cho phép bạn chỉ định kiểu mong đợi cho một tham số [UnsafeAccessor] dưới dạng string, giải quyết cả hai kịch bản được mô tả trong phần trước. Dễ dàng nhất là xem điều này trong hành động, vì vậy hãy xem xét cùng một ví dụ như trước. Giả sử chúng ta có hệ thống phân cấp này.
public class PublicClass
{
private readonly PrivateClass _private = new("Hello world!");
internal PrivateClass GetPrivate() => _private;
}
internal class PrivateClass(string someValue)
{
internal string SomeValue { get; } = someValue;
}
Và chúng ta sẽ tạo một số phương thức unsafe accessor để truy xuất SomeValue đó:
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetPrivate")]
[return: UnsafeAccessorType("PrivateClass")] // 👈 Specify target return type as a string
static extern object GetByMethod(PublicClass instance);
// 👆 use object as return type
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_SomeValue")]
static extern string GetSomeValue([UnsafeAccessorType("PrivateClass")] object instance);
// Specify target type in attribute 👆 and use object as instance type 👆
Như bạn có thể thấy trong các ví dụ trên, thay vì tham chiếu trực tiếp đến kiểu, bạn sử dụng object và thêm một [UnsafeAccessorType] với kiểu “thực” (thêm về điều đó sau).
Khi chúng ta có các định nghĩa trên, chúng ta có thể kết hợp hai phương thức này để truy xuất giá trị được lưu trữ trong một thể hiện PrivateClass, ngay cả khi chúng ta không thể tham chiếu trực tiếp đến nó:
// Create the target instance
var publicClass = new PublicClass();
// Invoke GetPrivate(), and return the result as an object
object privateClass = GetByMethod(publicClass);
// Pass the object and invoke the SomeValue getter method
string value = GetSomeValue(privateClass);
Console.WriteLine(value); // Hello world!
Chỉ định tên kiểu với [UnsafeAccessorType]
Tên kiểu tôi sử dụng trong ví dụ trước rất đơn giản, chỉ là PrivateClass, nhưng điều này che giấu thực tế rằng đây thực sự là một tên kiểu đầy đủ, giống như bạn có thể sử dụng trong Type.GetType(name) chẳng hạn. Điều này cần được định nghĩa đầy đủ mặc dù nó không phải là Assembly qualified, mặc dù làm như vậy thường mạnh mẽ hơn.
Cũng lưu ý rằng đối với các kiểu và phương thức tổng quát, các chuỗi này phải ở định dạng tổng quát mở hoặc đóng, tùy thuộc vào cách sử dụng của chúng, ví dụ: List``1[[!0]]. Tương tự, bạn cần ký tự + để xử lý các lớp lồng nhau.
Để làm cho tất cả cụ thể hơn một chút, tôi đã bao gồm một số ví dụ được lấy từ các bài kiểm tra đơn vị của runtime cho [UnsafeAccessor], minh họa việc sử dụng UnsafeAccessorType] để tham chiếu các kiểu trong các phương thức truy cập. Lưu ý rằng tất cả các kiểu được định nghĩa trong một assembly có tên PrivateLib, và tất cả các lớp và thành viên đều là internal, vì vậy không thể tham chiếu trực tiếp.
namespace PrivateLib;
internal class Class1
{
static int StaticField = 123;
int InstanceField = 456;
Class1() { }
static Class1 GetClass() => new Class1();
private Class1[] GetArray(ref Class1 a) => new[] { a };
}
internal class GenericClass<T>
{
List<Class1> ClosedGeneric() => new List<Class1>();
List<U> GenericMethod<U>() => new List<U>();
bool GenericWithConstraints<V, W>(List<T> a, List<V> b, List<W> c, List<Class1> d)
where W : T
=> true;
}
Sau đây là một loạt các ví dụ đại diện về các phương thức truy cập, với độ phức tạp ngày càng tăng. Mỗi ví dụ cho thấy một trường hợp sử dụng khác nhau của [UnsafeAccessorType]. Điều này cũng minh họa các loại phương thức truy cập khác nhau, chẳng hạn như các hàm tạo.
// 1. Calling the Class1 constructor, returned type name is assembly qualified
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("PrivateLib.Class1, PrivateLib")]
extern static object CreateClass();
// 2. Calling a static method. Both the return type and the "target" parameter are assembly qualified
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "GetClass")]
[return: UnsafeAccessorType("PrivateLib.Class1, PrivateLib")]
extern static object CallGetClass([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);
// 3. Returning a ref to the static field
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "StaticField")]
extern static ref int GetStaticField([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);
// 4. Returning a ref to an instance field
// Note that we cannot use [UnsafeAccessorType] on the return type. More on that later.
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "InstanceField")]
extern static ref int GetInstanceField([UnsafeAccessorType("PrivateLib.Class1, PrivateLib")] object a);
// 5. Passing an object by reference and returning an array
// Note the `&` in the signature when passing by reference.
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetArray")]
[return: UnsafeAccessorType("PrivateLib.Class1[], PrivateLib")]
extern static object CallM_RC1(TargetClass tgt, [UnsafeAccessorType("PrivateLib.Class1&, PrivateLib")] ref object a);
// 6. Invoking a method on a generic type, and returning a closed-generic type
// The return type uses a mix of fully qualified (for BCL types) and assembly qualified types
// Note that the open generic definition uses !0 to indicate an unspecified type parameter
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ClosedGeneric")]
[return: UnsafeAccessorType("System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]")]
extern static object CallGenericClassClosedGeneric([UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object a);
// 7. Invoking a generic method on a generic type.
// This is similar to the above, but we use !!0 to indicate an unspecified generic method type parameter.
// Note that the accessor method itself must be generic.
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GenericMethod")]
[return: UnsafeAccessorType("System.Collections.Generic.List`1[[!!0]]")]
extern static object CallGenericClassGenericMethod<U>([UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object a);
// 8. Invoking a generic method, on a generic type, with type constraints
// This is a more complex version of the above, but additionally specifies
// type constraints which match the target method's constraints.
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GenericWithConstraints")]
public extern static bool CallGenericClassGenericWithConstraints<V, W>(
[UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object tgt,
[UnsafeAccessorType("System.Collections.Generic.List`1[[!0]]")]
object a,
[UnsafeAccessorType("System.Collections.Generic.List`1[[!!0]]")]
object b,
List<W> c,
[UnsafeAccessorType("System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]")]
object d) where W : T;
Những ví dụ này cho thấy sự đa dạng rộng rãi của các lời gọi mà bạn có thể thực hiện bằng cách sử dụng [UnsafeAccessorType], điều này làm cho phương pháp này trở nên rất mạnh mẽ. Và hơn thế nữa, nó nhanh. Sử dụng [UnsafeAccessor] nói chung nhanh hơn nhiều so với reflection truyền thống.
Thật không may, ngay cả trong .NET 10, vẫn còn một vài khoảng trống với những gì chúng ta có thể làm.
Hạn chế của [UnsafeAccessorType]
Như tôi đã chỉ ra trong phần trước, bạn có thể sử dụng [UnsafeAccessorType] kết hợp với [UnsafeAccessor] để truy cập các thành viên riêng tư của các kiểu mà bạn không thể tham chiếu tại thời điểm chạy, nhưng vẫn còn một vài khoảng trống nơi bạn không thể thay thế việc sử dụng reflection truyền thống. Tóm lại, chúng là:
- Bạn không thể gọi một phương thức truy cập trên một kiểu tổng quát nếu bạn không thể biểu diễn đối số kiểu tổng quát.
- Bạn không thể gọi các trường mà trường cần được đánh dấu bằng
[UnsafeAccessorType]. - Bạn không thể gọi các phương thức trả về một
refnơi giá trị trả về cần được đánh dấu bằng[UnsafeAccessorType].
1. Không thể biểu diễn đối số kiểu
Trường hợp đầu tiên khá đơn giản. Hãy tưởng tượng chúng ta có kiểu Generic<T>, cộng với một lớp trợ giúp, Class1:
internal class Generic<T> { }
internal class Class1 { }
Chúng ta cần tạo các thể hiện của kiểu Generic<T> mặc dù nó được đánh dấu internal, vì vậy chúng ta tạo một phương thức truy cập cho nó:
static class Accessors<T>
{
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("Generic`1[[!0]]")]
public static extern object Create();
}
Điều này hoạt động tốt khi T chúng ta cần có thể tham chiếu:
object instance = Accessors<int>.Create();
Console.WriteLine(generic.GetType()); // Generic`1[System.Int32]
nhưng điều gì xảy ra nếu chúng ta cần tạo một thể hiện của Generic<Class1>? Câu trả lời đơn giản là chúng ta không thể. Chúng ta cần gọi Accessors<Class1>.Create(), nhưng điều đó sẽ không biên dịch được vì chúng ta không thể tham chiếu Class1. Vì vậy, nếu chúng ta gặp mẫu này thì chúng ta phải quay lại sử dụng reflection truyền thống.
2. Không thể biểu diễn kiểu trả về của trường
Chúng ta sẽ mở rộng ví dụ trước để thêm một kiểu mới, Class2, có một vài trường tham chiếu đến kiểu Class1:
internal class Class1 { }
internal class Class2
{
private Class1 _field1 = new();
private readonly Class1 _field2 = new();
}
Nếu _field1 và _field2 tham chiếu đến các kiểu đã biết, thì chúng ta có thể tạo [UnsafeAccessor] cho chúng mà không gặp vấn đề gì. Bạn có thể nghĩ rằng các phương thức truy cập như sau sẽ hoạt động:
// Helper for creating a C2 instance
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("Class2")]
static extern object Create();
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field1")]
[return: UnsafeAccessorType("Class1")]
static extern ref object CallField1([UnsafeAccessorType("Class2")] object instance);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field2")]
[return: UnsafeAccessorType("Class1")]
static extern ref readonly object CallField2([UnsafeAccessorType("Class2")] object instance);
Tuy nhiên, nếu bạn thử sử dụng CallField1() hoặc CallField2(), bạn sẽ gặp lỗi tại thời điểm chạy:
object class2 = Create();
object field1 = CallField1(class2); // throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute
object field2 = CallField2(class2); // throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute
Trong cả hai trường hợp, bạn nhận được một System.NotSupportedException nêu rõ Invalid usage of UnsafeAccessorTypeAttribute: bạn đơn giản là không thể truy cập các trường trừ khi bạn có thể biểu diễn các kiểu.
3. Không thể biểu diễn giá trị trả về ref của phương thức
Tương tự như vấn đề trước, nếu bạn có một phương thức trả về ref, và bạn không thể biểu diễn kiểu, thì bạn không thể sử dụng [UnsafeAccessor]. Ví dụ, hãy thêm một phương thức GetField1(), trả về một ref đến _field1:
internal class Class1 { }
internal class Class2
{
private Class1 _field1 = new();
private ref Class1 GetField1(Class2 a) => ref _field1;
}
Một phương thức truy cập cho điều này có thể trông như sau:
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetField1")]
[return: UnsafeAccessorType("Class1&")] // ref return
static extern ref object CallGetField1([UnsafeAccessorType("Class2")] object instance);
Nhưng việc cố gắng gọi phương thức truy cập này sẽ thất bại tại thời điểm chạy theo cùng cách như cố gắng truy cập trường trực tiếp:
object class2 = Create();
object field1 = CallGetField1(class2); // throws System.NotSupportedException: Invalid usage of UnsafeAccessorTypeAttribute
Đó là tất cả các hạn chế tôi tìm thấy, vì vậy miễn là những gì bạn đang cố gắng làm không rơi vào một trong những trường hợp này, thì hy vọng bạn sẽ ổn!
Tóm tắt
Trong bài viết này, tôi đã mô tả một số cải tiến của cơ chế [UnsafeAccessor] được giới thiệu trong .NET 8. Tôi đã chỉ ra cách [UnsafeAccessor] cho phép bạn dễ dàng truy cập các trường riêng tư và gọi các thành viên riêng tư của các kiểu, mà không cần sử dụng các API reflection trong .NET 8 và .NET 9. Sau đó, tôi đã mô tả một số hạn chế, cụ thể là bạn cần phải có thể tham chiếu các kiểu được sử dụng bởi các thành viên bạn đang truy cập.
Tiếp theo, tôi đã giới thiệu attribute [UnsafeAccessorType] và chỉ ra cách bạn có thể sử dụng nó để gọi các phương thức trên các kiểu mà bạn không thể tham chiếu tại thời điểm biên dịch. Tôi đã chỉ ra cách bạn có thể sử dụng điều này để gọi các phương thức, hàm tạo và trường, và cách làm việc với các kiểu tổng quát và phương thức tổng quát. Cuối cùng, tôi đã mô tả các hạn chế của [UnsafeAccessorType], cụ thể là bạn không thể sử dụng nó để làm việc với các thể hiện của các kiểu tổng quát nơi bạn không thể tham chiếu tham số kiểu, và bạn không thể sử dụng [UnsafeAccessorType] với các trường hoặc các phương thức trả về ref.



