有没有办法通过反射获取一个ref struct的属性?
以 System.Text.RegularExpressions.Match 为例。它有一个属于rex struct的成员 ReadOnlySpan<char> ValueSpan。
常用的反射方式行不通,因为ref struct的成员不能装箱(不能变成 System.Object)。
Match match = new Regex("\\d+").Match("testing 123"); // Create test data
PropertyInfo[] props = match.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
var values = props.Select(s => (s.Name, s.GetValue(match, null))).ToArray();
如果你尝试运行上面的代码,程序会以 System.NotSupportedException 退出。当对 Match.ValueSpan 的PropertyInfo调用 PropertyInfo.GetValue(object? obj, object?[]? index) 时,异常就会被触发。
我想构建的是一个对象检查器,能够查看对象的字段和属性。理想情况下,会有一种调用方法,它接受被检查对象(在本例中是 Match)、属性名的字符串(在本例中是 "ValueSpan")以及返回类型(在本例中是 typeof(ReadOnlySpan<char>))的方法。
像这样的:
Match match = new Regex("\\d+").Match("testing 123"); // Create test data
var result = GetProperty(match, "ValueSpan", typeof(ReadOnlySpan<char>))
不过问题在于,var result 的类型会解析成什么?它不能是object,因为 match.ValueSpan 是一个ref struct。有没有解决这个问题的方法?
我已经尝试过多种解决方式,最有前景的是使用表达式树。下面的方法将被检查的对象和属性名作为参数,但需要知道返回类型:
public static T GetProperty<T>(object target, string name) where T : allows ref struct
{
var arg = Expression.Parameter(typeof(object));
var expr = Expression.Property(Expression.Convert(arg, target.GetType()), name);
var compiled = Expression.Lambda<Func<object, T>>(expr, arg).Compile();
return compiled(target);
}
然后可以这样调用:
Match match = new Regex("\\d+").Match("testing 123"); // Create test data
var result = GetProperty<ReadOnlySpan<char>>(match, "ValueSpan")
进一步完善这个解决方案时,我也想把返回类型作为一个函数参数来声明(而不是泛型参数)。
甚至连hacky的做法也欢迎。我在想,是否可以创建一个伪的“基类型”结构体,并通过指针拷贝数据(包括RuntimeHandle)。总之,我们需要创建一种可以充当任何类型(包括ref struct)的替身的类型。对此有何想法?
解决方案
这 may 不一定完全符合你的用例,但我猜它会。与其 返回 值,不如在 ref struct 规则的精神之内,通过 消费 值而不让它离开栈来实现。你可以用一个接受该值并将其格式化为常规类型(例如字符串)的委托来完成。你已经使用的表达式树方法随后就可以调用该委托,返回一个字符串。
All of this can be wrapped in a class which you can use to register formatters of different types. Here's a simple example, with a bunch of error checking and other things omitted (for example, it looks for a formatter of the exact type instead of using inheritance). It should be enough to get you going though.
internal sealed class PropertyFormatter
{
private readonly Dictionary<Type, Delegate> formatters = new();
public void RegisterFormatter<T>(Func<T, string> formatter) where T : allows ref struct
{
formatters.Add(typeof(T), formatter);
}
public string FormatProperty(object target, string name)
{
var property = target.GetType().GetProperty(name);
if (property is null)
{
throw new ArgumentException("No such property");
}
var formatter = formatters[property.PropertyType];
var arg = Expression.Parameter(typeof(object));
var expr = Expression.Property(Expression.Convert(arg, target.GetType()), name);
var call = Expression.Call(Expression.Constant(formatter.Target), formatter.Method, expr);
var compiled = Expression.Lambda<Func<object, string>>(call, arg).Compile();
return compiled(target);
}
}
你可以这样使用它:
using System.Text.RegularExpressions;
// Create a formatter which can be used multiple times.
var propertyFormatter = new PropertyFormatter();
// You'd normally register a bunch of formatters here.
propertyFormatter.RegisterFormatter<ReadOnlySpan<char>>(span => new string(span));
Match match = new Regex("\\d+").Match("testing 123"); // Create test data
var result = propertyFormatter.FormatProperty(match, "ValueSpan");
Console.WriteLine(result); // Prints 123