Отримати властивості в порядку оголошення за допомогою відображення


81

Мені потрібно отримати всі властивості, використовуючи відображення в тому порядку, в якому вони оголошені в класі. Згідно з MSDN, замовлення не може бути гарантоване під час використанняGetProperties()

Метод GetProperties не повертає властивості в певному порядку, наприклад алфавітному порядку чи порядку декларування.

Але я вже читав, що є обхідний шлях шляхом упорядкування властивостей за MetadataToken. Тож моє запитання: чи це безпечно? Здається, я не можу знайти про це інформацію на MSDN. Або існує інший спосіб вирішення цієї проблеми?

Моя поточна реалізація виглядає так:

var props = typeof(T)
   .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
   .OrderBy(x => x.MetadataToken);

12
У всякому разі, це погана ідея. Створіть власний атрибут із значенням замовлення або будь-якими іншими метаданими та позначте поля класу цим атрибутом.
Кирило Поліщук

1
Можливо, ви можете додати новий атрибут, який містить int замовлення. Потім отримати властивості, отримати DisplayOrderAttribute кожного властивості та відсортувати за цим?
BlueChippy

1
З цікавості, для чого ви це робите, чого намагаєтесь досягти?
Sam Greenhalgh,

6
@Magnus Однак питання все ще цікаве, оскільки деякі частини фреймворку в значній мірі покладаються на це. Наприклад, серіалізація з атрибутом Serializable зберігає членів у тому порядку, в якому вони були визначені. Найменше Вагнер заявляє про це у своїй книзі "Ефективний C #"
Павло Воронін

Відповіді:


145

На .net 4.5 (і навіть .net 4.0 у vs2012) ви можете зробити набагато краще за допомогою відображення, використовуючи розумний трюк з [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].


2
Це справді дуже розумно! Цікаво, чи є у нього протилежні аргументи проти? Насправді, мені здається, це досить елегантно. Я використовую Linq2CSV, і я думаю, що буду успадковувати CsvColumnAttributeі використовувати це як FieldIndexзначення за замовчуванням
julealgon

2
@julealgon Я вважаю, що аргумент проти цього полягає в тому, що існує недокументований позиційний API, який хтось може зламати під час рефакторингу, якщо не зрозумів, що атрибут використовується таким чином. Я все ще думаю, що це досить елегантно, просто кажучи на випадок, якщо хтось скопіює / вставить це і шукає мотивацію, щоб залишити коментарі для наступного хлопця.
Джоель Б

2
Це працює, крім випадку, коли 1 клас успадковує інший клас, і мені потрібен порядок усіх властивостей. Чи є для цього теж хитрість?
Пол Бакстер,

Я думаю, це зламається, якщо клас оголошено частково. На відміну від успадкування, API не може це зрозуміти.
Wu Zhenwei

1
Дуже розумний підхід, але він викине нульове виключення посилання, якщо деякі забудуть встановити атрибут [Order]. Краще перевірити нуль перед дзвінком. Наприклад: властивості var = з властивості typeof (Test) .GetProperties () дозвольте orderAttribute = (property.GetCustomAttributes (typeof (OrderAttribute), false) .SingleOrDefault () == null? New OrderAttribute (0): property.GetCustomAttributes (typeof (OrderAttribute), false) .SingleOrDefault ()) як OrderAttribute orderby orderAttribute.Order select property;
Sopan Maiti

15

Якщо ви йдете по шляху атрибутів, ось метод, який я використовував раніше;

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 на пост Марка Гравелла є хороший зразок.


2
Якщо це вимога до відображення, було б доцільно використовувати DataAnnotations.DisplayAttribute , який має поле Order.
Джерф

12

На думку MSDN, MetadataToken він унікальний всередині одного модуля - нічого не говорить про те, що він взагалі гарантує будь-яке замовлення.

НАВІТЬ якби він поводився так, як ти хочеш, це було б специфічним для реалізації та могло змінитися будь-коли без попередження.

Дивіться цей старий запис у блозі MSDN .

Я настійно рекомендую уникати будь-якої залежності від таких деталей реалізації - див. Цю відповідь від Марка Гравелла .

ЯКЩО вам потрібно щось під час компіляції, ви можете поглянути на Roslyn (хоча це на дуже ранній стадії).


5

Те, що я перевірив сортування за допомогою MetadataToken, працює.

Деякі користувачі тут стверджують, що це якось невдалий підхід / не надійний, але я ще не бачив жодних доказів цього - можливо, ви можете розмістити тут якийсь фрагмент коду, коли даний підхід не працює?

Про зворотну сумісність - поки ви зараз працюєте над .net 4 / .net 4.5 - Microsoft робить .net 5 або новішу версію, тому ви майже можете припустити, що цей метод сортування не буде порушено в майбутньому.

Звичайно, можливо, до 2017 року, коли ви перейдете на .net9, ви перервете сумісність, але до того часу хлопці Microsoft, ймовірно, зрозуміють "офіційний механізм сортування". Немає сенсу повертатися назад або ламати речі.

Гра з додатковими атрибутами для впорядкування властивостей також вимагає часу та реалізації - навіщо турбуватися, якщо сортування MetadataToken працює?


1
Це 2019 рік, і навіть .net-4.9 ще не вийшов :-p.
binki

Кожен раз, коли .net випускає основну версію, це чудова можливість для них вносити зміни у такі речі, як MetadataToken або порядок повернення з GetProperties(). Ваш аргумент, чому ви можете покладатися на порядок, точно такий самий, як аргумент, чому ви не можете покладатися на майбутні версії .net, не змінюючи цієї поведінки: кожен новий випуск може змінювати деталі реалізації. Зараз .net насправді дотримується філософії, згідно з якою "помилки - це особливості", тому насправді вони, мабуть, ніколи не змінять порядок GetProperties(). Просто API заявляє, що їм це дозволено.
binki

1

Ви можете використовувати DisplayAttribute у System.Component.DataAnnotations замість спеціального атрибута. Ваша вимога все одно має щось робити з дисплеєм.


0

Якщо вас влаштовує додаткова залежність, для цього можна використовувати протобуф -мережу Марка Гравелла, не турбуючись про найкращий спосіб відображення та кешування тощо. Просто прикрасьте свої поля за допомогою, [ProtoMember]а потім перейдіть до полів у цифровому порядку, використовуючи:

MetaType metaData = ProtoBuf.Meta.RuntimeTypeModel.Default[typeof(YourTypeName)];

metaData.GetFields();

0

Я зробив це так:

 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_; } }

}

Чим це відрізняється чи краще прийнятої відповіді?
Ян Кемп,

0

Спираючись на прийняте вище рішення, щоб отримати точний індекс, ви можете використовувати щось подібне

Дано

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);

Зверніть увагу , немає перевірки помилок або стійкості до несправностей, ви можете додати перець і сіль за смаком


0

Інша можливість полягає у використанні 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; }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.