我需要按照它们在类中声明的顺序使用反射获取所有属性。根据MSDN的说法,使用GetProperties()
时无法保证订单
方法不返回特定属性 顺序,例如按字母顺序或声明顺序。
但是我已经读到有一种解决方法,即按MetadataToken
对属性进行排序.所以我的问题是,这安全吗?我似乎在MSDN上找不到任何有关它的信息。或者有没有其他方法可以解决这个问题?
我当前的实现如下所示:
var props = typeof(T)
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.OrderBy(x => x.MetadataToken);
在 .net 4.5(甚至是 vs2012 中的 .net 4.0(上,您可以使用带有 [CallerLineNumber]
属性的巧妙技巧在反射方面做得更好,让编译器将顺序插入到属性中:
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class OrderAttribute : Attribute
{
private readonly int order_;
public OrderAttribute([CallerLineNumber]int order = 0)
{
order_ = order;
}
public int Order { get { return order_; } }
}
public class Test
{
//This sets order_ field to current line number
[Order]
public int Property2 { get; set; }
//This sets order_ field to current line number
[Order]
public int Property1 { get; set; }
}
然后使用反射:
var properties = from property in typeof(Test).GetProperties()
where Attribute.IsDefined(property, typeof(OrderAttribute))
orderby ((OrderAttribute)property
.GetCustomAttributes(typeof(OrderAttribute), false)
.Single()).Order
select property;
foreach (var property in properties)
{
//
}
如果必须处理分部类,则可以使用 [CallerFilePath]
对属性进行另外排序。
如果您要使用属性路由,这是我过去使用过的方法;
public static IOrderedEnumerable<PropertyInfo> GetSortedProperties<T>()
{
return typeof(T)
.GetProperties()
.OrderBy(p => ((Order)p.GetCustomAttributes(typeof(Order), false)[0]).Order);
}
然后像这样使用它;
var test = new TestRecord { A = 1, B = 2, C = 3 };
foreach (var prop in GetSortedProperties<TestRecord>())
{
Console.WriteLine(prop.GetValue(test, null));
}
哪里;
class TestRecord
{
[Order(1)]
public int A { get; set; }
[Order(2)]
public int B { get; set; }
[Order(3)]
public int C { get; set; }
}
如果您在所有属性上没有可比属性的类型上运行该方法,则该方法将失败,因此请注意它的使用方式,并且它应该足以满足要求。
我省略了顺序的定义:属性,因为在Yahia链接到Marc Gravell的帖子中有一个很好的样本。
根据MSDN,MetadataToken
在一个模块中是唯一的 - 没有任何说它保证任何顺序。
即使它确实按照您想要的方式运行,那也是特定于实现的,并且可以随时更改,恕不另行通知。
请参阅此旧的 MSDN 博客条目。
我强烈建议远离对此类实现细节的任何依赖 - 请参阅Marc Gravell的答案。
如果你在编译时需要一些东西,你可以看看Roslyn(尽管它处于非常早期的阶段(。
另一种可能性是使用 System.ComponentModel.DataAnnotations.DisplayAttribute
Order
属性。由于它是内置的,因此无需创建新的特定属性。
然后选择如下所示的有序属性
const int defaultOrder = 10000;
var properties = type.GetProperties().OrderBy(p => p.FirstAttribute<DisplayAttribute>()?.GetOrder() ?? defaultOrder).ToArray();
课堂可以这样呈现
public class Toto {
[Display(Name = "Identifier", Order = 2)
public int Id { get; set; }
[Display(Name = "Description", Order = 1)
public string Label {get; set; }
}
我测试过按元数据令牌排序的内容有效。
这里的一些用户声称这在某种程度上不是好方法/不可靠,但我还没有看到任何证据 - 也许当给定的方法不起作用时,您可以在此处发布一些代码狙击?
关于向后兼容性 - 虽然您现在正在处理 .net 4/.net 4.5 - Microsoft正在制作 .net 5 或更高版本,因此您几乎可以假设这种排序方法将来不会被破坏。
当然,也许到2017年升级到.net9时,您将遇到兼容性中断,但到那时Microsoft伙可能会弄清楚"官方排序机制"。回去或破坏东西是没有意义的。
使用额外的属性进行属性排序也需要时间和实现 - 如果元数据令牌排序有效,为什么要打扰?
您可以在 System.Component.DataAnnotations 中使用 DisplayAttribute,而不是自定义属性。无论如何,您的要求必须与显示有关。
如果可以强制类型具有已知的内存布局,则可以依赖StructLayout(LayoutKind.Sequential)
然后按内存中的字段偏移量进行排序。
这样,您就不需要类型中的每个字段上的任何属性。
不过有一些严重的缺点:
- 所有字段类型都必须具有内存表示形式(除了固定长度的数组或字符串之外,实际上没有其他引用类型(。这包括父类型,即使您只想对子类型的字段进行排序。
- 您可以将其用于包括继承在内的类,但所有父类也需要具有顺序布局集。
- 显然,这不会对属性进行排序,但字段对于 POCO 可能很好。
[StructLayout(LayoutKind.Sequential)]
struct TestStruct
{
public int x;
public decimal y;
}
[StructLayout(LayoutKind.Sequential)]
class TestParent
{
public int Base;
public TestStruct TestStruct;
}
[StructLayout(LayoutKind.Sequential)]
class TestRecord : TestParent
{
public bool A;
public string B;
public DateTime C;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 42)] // size doesn't matter
public byte[] D;
}
class Program
{
static void Main(string[] args)
{
var fields = typeof(TestRecord).GetFields()
.OrderBy(field => Marshal.OffsetOf(field.DeclaringType, field.Name));
foreach (var field in fields) {
Console.WriteLine($"{field.Name}: {field.FieldType}");
}
}
}
输出:
Base: System.Int32
TestStruct: TestStruct
A: System.Boolean
B: System.String
C: System.DateTime
D: System.Byte[]
如果您尝试添加任何禁止的字段类型,则会得到System.ArgumentException: Type 'TestRecord' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed.
我是这样做的:
internal static IEnumerable<Tuple<int,Type>> TypeHierarchy(this Type type)
{
var ct = type;
var cl = 0;
while (ct != null)
{
yield return new Tuple<int, Type>(cl,ct);
ct = ct.BaseType;
cl++;
}
}
internal class PropertyInfoComparer : EqualityComparer<PropertyInfo>
{
public override bool Equals(PropertyInfo x, PropertyInfo y)
{
var equals= x.Name.Equals(y.Name);
return equals;
}
public override int GetHashCode(PropertyInfo obj)
{
return obj.Name.GetHashCode();
}
}
internal static IEnumerable<PropertyInfo> GetRLPMembers(this Type type)
{
return type
.TypeHierarchy()
.SelectMany(t =>
t.Item2
.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(prop => Attribute.IsDefined(prop, typeof(RLPAttribute)))
.Select(
pi=>new Tuple<int,PropertyInfo>(t.Item1,pi)
)
)
.OrderByDescending(t => t.Item1)
.ThenBy(t => t.Item2.GetCustomAttribute<RLPAttribute>().Order)
.Select(p=>p.Item2)
.Distinct(new PropertyInfoComparer());
}
属性声明如下:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RLPAttribute : Attribute
{
private readonly int order_;
public RLPAttribute([CallerLineNumber]int order = 0)
{
order_ = order;
}
public int Order { get { return order_; } }
}
基于上述公认的解决方案,要获得确切的索引,您可以使用这样的东西
鉴于
public class MyClass
{
[Order] public string String1 { get; set; }
[Order] public string String2 { get; set; }
[Order] public string String3 { get; set; }
[Order] public string String4 { get; set; }
}
扩展
public static class Extensions
{
public static int GetOrder<T,TProp>(this T Class, Expression<Func<T,TProp>> propertySelector)
{
var body = (MemberExpression)propertySelector.Body;
var propertyInfo = (PropertyInfo)body.Member;
return propertyInfo.Order<T>();
}
public static int Order<T>(this PropertyInfo propertyInfo)
{
return typeof(T).GetProperties()
.Where(property => Attribute.IsDefined(property, typeof(OrderAttribute)))
.OrderBy(property => property.GetCustomAttributes<OrderAttribute>().Single().Order)
.ToList()
.IndexOf(propertyInfo);
}
}
用法
var myClass = new MyClass();
var index = myClass.GetOrder(c => c.String2);
注意,没有错误检查或容错,可以加胡椒粉和盐调味
如果您对额外的依赖性感到满意,Marc Gravell 的 Protobuf-Net 可用于执行此操作,而不必担心实现反射和缓存等的最佳方式。只需使用[ProtoMember]
装饰字段,然后使用以下方法按数字顺序访问字段:
MetaType metaData = ProtoBuf.Meta.RuntimeTypeModel.Default[typeof(YourTypeName)];
metaData.GetFields();
即使这是一个非常古老的线程,这是我基于 @Chris McAtackney 的工作解决方案
var props = rootType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.OrderBy(p =>
(
p.GetCustomAttributes(typeof(AttrOrder), false).Length != 0 ? // if we do have this attribute
((p.GetCustomAttributes(typeof(AttrOrder), false)[0]) as AttrOrder).Order
: int.MaxValue // or just a big value
)
);
属性是这样的
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class AttrOrder : Attribute
{
public int Order { get; }
public AttrOrder(int order)
{
Order = order;
}
}
像这样使用
[AttrOrder(1)]
public string Name { get; set; }