Pobierz właściwości w kolejności deklaracji za pomocą odbicia

81

Muszę pobrać wszystkie właściwości za pomocą odbicia w kolejności, w jakiej są zadeklarowane w klasie. Według MSDN nie można zagwarantować zamówienia podczas korzystaniaGetProperties()

Metoda GetProperties nie zwraca właściwości w określonej kolejności, takiej jak kolejność alfabetyczna lub deklaracja.

Ale przeczytałem, że istnieje obejście polegające na porządkowaniu właściwości według MetadataToken. Więc moje pytanie brzmi: czy to jest bezpieczne? Nie mogę znaleźć żadnych informacji na ten temat w witrynie MSDN. Czy jest jakiś inny sposób rozwiązania tego problemu?

Moja obecna realizacja wygląda następująco:

var props = typeof(T)
   .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
   .OrderBy(x => x.MetadataToken);
Magnus
źródło
12
Zresztą to zły pomysł. Utwórz własny atrybut z wartością zamówienia lub innymi metadanymi i oznacz pola klasy tym atrybutem.
Kirill Polishchuk
1
Być może mógłbyś dodać nowy atrybut, który zawiera int zamówienia. Następnie pobierz właściwości, pobierz DisplayOrderAttribute każdej właściwości i posortuj według tego?
BlueChippy
1
Z ciekawości, dlaczego to robisz, co próbujesz osiągnąć?
Sam Greenhalgh
6
@Magnus Jednak pytanie jest nadal interesujące, ponieważ niektóre elementy samego frameworka w dużym stopniu na tym polegają. Na przykład serializacja z atrybutem Serializable przechowuje elementy członkowskie w kolejności, w jakiej zostały zdefiniowane. Co najmniej Wagner stwierdza to w swojej książce „Efektywny C #”
Pavel Voronin

Odpowiedzi:

145

Na .net 4.5 (a nawet .net 4.0 w vs2012) możesz zrobić dużo lepiej z odbiciem używając sprytnej sztuczki z [CallerLineNumber]atrybutem, pozwalając kompilatorowi wstawić porządek do twoich właściwości za Ciebie:

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

A potem użyj refleksji:

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)
{
   //
}

Jeśli masz do czynienia z klasami częściowymi, możesz dodatkowo posortować właściwości za pomocą [CallerFilePath].

ghord
źródło
2
To naprawdę bardzo sprytne! Zastanawiam się, czy ma przeciwko temu jakieś kontrargumenty? Wydaje mi się, że wydaje mi się całkiem elegancki. Używam Linq2CSV i myślę, że odziedziczę CsvColumnAttributei FieldIndex
użyję
2
@julealgon Przypuszczam, że argumentem przeciwko temu jest to, że istnieje nieudokumentowane, pozycyjne API, które ktoś mógłby złamać podczas refaktoryzacji, gdyby nie rozumiał, że atrybut był używany w ten sposób. Nadal uważam, że to całkiem eleganckie, po prostu mówię na wypadek, gdyby ktoś to skopiował / wkleił i szukał motywacji, aby zostawić komentarz dla następnego gościa.
Joel B
2
Działa to z wyjątkiem przypadku, gdy klasa 1 dziedziczy inną klasę i potrzebuję kolejności wszystkich właściwości. Czy na to też jest jakaś sztuczka?
Paul Baxter
Myślę, że to się zepsuje, jeśli klasa zostanie zadeklarowana częściowo. W przeciwieństwie do dziedziczenia, API nie może tego rozgryźć.
Wu Zhenwei
1
Bardzo sprytne podejście, ale jeśli ktoś zapomni ustawić atrybut [Order], wyrzuci wyjątek odniesienia zerowego. Lepiej sprawdzić przed wywołaniem. Np .: var properties = from property in typeof (Test) .GetProperties () let orderAttribute = (property.GetCustomAttributes (typeof (OrderAttribute), false) .SingleOrDefault () == null? New OrderAttribute (0): property.GetCustomAttributes (typeof (OrderAttribute), false) .SingleOrDefault ()) jako OrderAttribute orderby orderAttribute.Order wybierz właściwość;
Sopan Maiti
15

Jeśli wybierasz trasę atrybutów, oto metoda, której używałem w przeszłości;

public static IOrderedEnumerable<PropertyInfo> GetSortedProperties<T>()
{
  return typeof(T)
    .GetProperties()
    .OrderBy(p => ((Order)p.GetCustomAttributes(typeof(Order), false)[0]).Order);
}

Następnie użyj tego w ten sposób;

var test = new TestRecord { A = 1, B = 2, C = 3 };

foreach (var prop in GetSortedProperties<TestRecord>())
{
    Console.WriteLine(prop.GetValue(test, null));
}

Gdzie;

class TestRecord
{
    [Order(1)]
    public int A { get; set; }

    [Order(2)]
    public int B { get; set; }

    [Order(3)]
    public int C { get; set; }
}

Metoda będzie barf, jeśli uruchomisz ją na typie bez porównywalnych atrybutów dla wszystkich twoich właściwości, więc uważaj, jak jest używana i powinna wystarczyć do wymagania.

Pominąłem definicję Order: Attribute, ponieważ w linku Yahia do posta Marca Gravella znajduje się dobry przykład.

Christopher McAtackney
źródło
2
Jeśli jest to wymaganie dotyczące wyświetlania, należałoby użyć DataAnnotations.DisplayAttribute , która ma pole Order.
Jerph
12

Według MSDN MetadataToken jest wyjątkowy wewnątrz jednego modułu - nic nie mówi, że gwarantuje w ogóle jakiekolwiek zamówienie.

NAWET gdyby zachowywał się tak, jak chcesz, byłoby to zależne od implementacji i mogłoby się zmienić w dowolnym momencie bez powiadomienia.

Zobacz ten stary wpis w blogu MSDN .

Zdecydowanie polecam trzymać się z daleka od wszelkich zależności od takich szczegółów implementacji - zobacz tę odpowiedź od Marca Gravella .

JEŚLI potrzebujesz czegoś w czasie kompilacji, możesz rzucić okiem na Roslyn (chociaż jest na bardzo wczesnym etapie).

Yahia
źródło
5

To, co przetestowałem, sortowanie według MetadataToken działa.

Niektórzy użytkownicy tutaj twierdzą, że nie jest to dobre podejście / niewiarygodne, ale nie widziałem jeszcze żadnych dowodów na to - może możesz zamieścić tutaj fragment kodu, gdy dane podejście nie działa?

O kompatybilności wstecznej - gdy teraz pracujesz nad .net 4 / .net 4.5 - Microsoft tworzy .net 5 lub nowszy, więc możesz założyć, że ta metoda sortowania nie zostanie zepsuta w przyszłości.

Oczywiście, być może do 2017 r., Kiedy będziesz aktualizować do .net9, dojdzie do zerwania kompatybilności, ale do tego czasu pracownicy Microsoft prawdopodobnie wymyślą „oficjalny mechanizm sortowania”. Nie ma sensu cofać się lub przerywać.

Granie z dodatkowymi atrybutami do porządkowania właściwości również wymaga czasu i implementacji - po co zawracać sobie głowę, czy sortowanie MetadataToken działa?

TarmoPikaro
źródło
1
Jest rok 2019, a nawet .net-4.9 nie został jeszcze wydany :-p.
binki
Za każdym razem, gdy .net wydaje wersję główną, jest dla nich świetną okazją do wprowadzenia zmian w takich rzeczach, jak MetadataToken lub kolejność zwrotu z GetProperties(). Twój argument przemawiający za tym, dlaczego możesz polegać na kolejności, jest dokładnie taki sam, jak argument, dlaczego nie możesz polegać na przyszłych wersjach .net, które nie zmieniają tego zachowania: każda nowa wersja może zmieniać szczegóły implementacji. Teraz .net faktycznie wyznaje filozofię, według której „błędy to cechy”, więc w rzeczywistości prawdopodobnie nigdy nie zmieniłyby kolejności GetProperties(). Po prostu API mówi, że mogą.
binki
1

Możesz użyć DisplayAttribute w System.Component.DataAnnotations zamiast atrybutu niestandardowego. Twoje wymaganie i tak musi zrobić coś z wyświetlaniem.

Panos Roditakis
źródło
0

Jeśli jesteś zadowolony z dodatkowej zależności, można to zrobić za pomocą Protobuf-Net Marca Gravella bez martwienia się o najlepszy sposób implementacji odbicia i buforowania itp. Po prostu udekoruj swoje pola, [ProtoMember]a następnie uzyskaj dostęp do pól w kolejności numerycznej za pomocą:

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

metaData.GetFields();
Tom Makin
źródło
0

Zrobiłem to w ten sposób:

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




    }

z właściwością zadeklarowaną w następujący sposób:

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

}
strażnik
źródło
Jak to się różni lub jest lepsze od zaakceptowanej odpowiedzi?
Ian Kemp,
0

Opierając się na powyższym zaakceptowanym rozwiązaniu, aby uzyskać dokładny indeks, możesz użyć czegoś takiego

Dany

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

Rozszerzenia

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

Stosowanie

var myClass = new MyClass();
var index = myClass.GetOrder(c => c.String2);

Uwaga , nie ma sprawdzania błędów ani tolerancji na błędy, możesz dodać pieprz i sól do smaku

00110001
źródło
0

Inną możliwością jest skorzystanie z System.ComponentModel.DataAnnotations.DisplayAttribute Ordernieruchomości. Ponieważ jest wbudowany, nie ma potrzeby tworzenia nowego określonego atrybutu.

Następnie wybierz uporządkowane właściwości, takie jak ten

const int defaultOrder = 10000;
var properties = type.GetProperties().OrderBy(p => p.FirstAttribute<DisplayAttribute>()?.GetOrder() ?? defaultOrder).ToArray();

I tak można przedstawić klasę

public class Toto {
    [Display(Name = "Identifier", Order = 2)
    public int Id { get; set; }

    [Display(Name = "Description", Order = 1)
    public string Label {get; set; }
}
labilbe
źródło