2010-06-29 15 views
7

Przeglądam kawałek kodu, który napisałem nie tak dawno temu, i po prostu nienawidzę sposobu, w jaki zajmowałem się sortowaniem - zastanawiam się, czy ktoś może być w stanie pokazać ja lepszy sposób.Szukam lepszego sposobu sortowania mojej listy <T>

Mam klasę, Holding, która zawiera pewne informacje. Mam inną klasę, HoldingsList, która zawiera element List<Holding>. Mam również enum, PortfolioSheetMapping, który ma około 40 elementów.

To rodzaj wygląda następująco:

public class Holding 
{ 
    public ProductInfo Product {get;set;} 
    // ... various properties & methods ... 
} 

public class ProductInfo 
{ 
    // .. various properties, methods... 
} 

public class HoldingsList 
{ 
    public List<Holding> Holdings {get;set;} 
    // ... more code ... 
} 

public enum PortfolioSheetMapping 
{ 
    Unmapped = 0, 
    Symbol, 
    Quantitiy, 
    Price, 
    // ... more elements ... 
} 

Mam metodę, która może powołać się na liście, aby być posortowane w zależności od wyliczenie użytkownik wybierze. Metoda używa instrukcji switch mondo, która ma ponad 40 przypadków (ugh!). poniżej

Krótki fragment ilustruje kod:

if (frm.SelectedSortColumn.IsBaseColumn) 
{ 
    switch (frm.SelectedSortColumn.BaseColumn) 
    { 
     case PortfolioSheetMapping.IssueId: 
      if (frm.SortAscending) 
      { 
       // here I'm sorting the Holding instance's 
       // Product.IssueId property values... 
       // this is the pattern I'm using in the switch... 
       pf.Holdings = pf.Holdings.OrderBy 
        (c => c.Product.IssueId).ToList(); 
      } 
      else 
      { 
       pf.Holdings = pf.Holdings.OrderByDescending 
        (c => c.Product.IssueId).ToList(); 
      } 
      break; 
     case PortfolioSheetMapping.MarketId: 
      if (frm.SortAscending) 
      { 
       pf.Holdings = pf.Holdings.OrderBy 
        (c => c.Product.MarketId).ToList(); 
      } 
      else 
      { 
       pf.Holdings = pf.Holdings.OrderByDescending 
        (c => c.Product.MarketId).ToList(); 
      } 
      break; 
     case PortfolioSheetMapping.Symbol: 
      if (frm.SortAscending) 
      { 
       pf.Holdings = pf.Holdings.OrderBy 
        (c => c.Symbol).ToList(); 
      } 
      else 
      { 
       pf.Holdings = pf.Holdings.OrderByDescending 
        (c => c.Symbol).ToList(); 
      } 
      break; 
     // ... more code .... 

Mój problem jest z instrukcji switch. Model switch jest ściśle powiązany z enumem PortfolioSheetMapping, który może zostać zmieniony jutro lub na następny dzień. Za każdym razem, gdy się to zmieni, będę musiał ponownie przejrzeć tę instrukcję przełączania i dodać do niej kolejny blok case. Obawiam się, że w końcu ta zmiana będzie tak duża, że ​​nie da się nią zarządzać.

Czy ktoś może mi powiedzieć, czy istnieje lepszy sposób sortowania mojej listy?

+0

Dlaczego jest to sortowanie robione? Czy jest to wyłącznie w celach pokazowych? –

+0

@Hans, instancja klasy jest deserialized z arkusza kalkulacyjnego Excel zawierającego dane analityczne portfela.Faktyczna akcja jest wywoływana z przycisku paska narzędzi programu Excel (ale to naprawdę nieistotne), a po zakończeniu sortowania ponownie przekształcam obiekty z powrotem do arkusza kalkulacyjnego. Sortowanie ma wpływ na różne inne elementy arkusza kalkulacyjnego Excel, więc nie jest to wyłącznie wyświetlanie. – code4life

+2

Poszerzenie mojego komentarza na temat odpowiedzi Marka ... Czy możliwe jest, abyś zreorganizował swój kod tak, aby obsługiwał dane w formie bardziej zbliżonej do bazy danych? Możesz nawet nie potrzebować wyliczenia, jeśli używałeś więcej formatu bazy danych w stylu bazy danych (możesz w prosty sposób dodawać i usuwać kolumny z "tabeli"), więc jeśli chcesz poświęcić czas/wysiłek, aby to zrobić, takie refaktoryzowanie, możesz skończyć z czymś bardziej eleganckim, że tak powiem. Oczywiście, niektóre aspekty twojego programu mogą być inne, co może sprawić, że takie podejście stanie się mniej wykonalne niż się wydaje ... – JAB

Odpowiedz

5

pan ponownie przypisując dane posortowane prosto do swojej własności pf.Holdings, więc dlaczego nie ominąć napowietrznej OrderBy i ToList i po prostu korzystać z listy za Sort metodę bezpośrednio zamiast?

Można użyć mapy, aby trzymać Comparison<T> delegatów dla wszystkich obsługiwanych dobieranie a następnie zadzwonić Sort(Comparison<T>) z odpowiednim Delegat:

if (frm.SelectedSortColumn.IsBaseColumn) 
{ 
    Comparison<Holding> comparison; 
    if (!_map.TryGetValue(frm.SelectedSortColumn.BaseColumn, out comparison)) 
     throw new InvalidOperationException("Can't sort on BaseColumn"); 

    if (frm.SortAscending) 
     pf.Holdings.Sort(comparison); 
    else 
     pf.Holdings.Sort((x, y) => comparison(y, x)); 
} 

// ... 

private static readonly Dictionary<PortfolioSheetMapping, Comparison<Holding>> 
    _map = new Dictionary<PortfolioSheetMapping, Comparison<Holding>> 
    { 
     { PortfolioSheetMapping.IssueId, GetComp(x => x.Product.IssueId) }, 
     { PortfolioSheetMapping.MarketId, GetComp(x => x.Product.MarketId) }, 
     { PortfolioSheetMapping.Symbol, GetComp(x => x.Symbol) }, 
     // ... 
    }; 

private static Comparison<Holding> GetComp<T>(Func<Holding, T> selector) 
{ 
    return (x, y) => Comparer<T>.Default.Compare(selector(x), selector(y)); 
} 
+0

Dobra sugestia, zdecydowanie warta wypróbowania. – code4life

+0

dzięki! Anegdotycznie kod .Sort jest nieznaczny, ale zdecydowanie szybszy niż .OrderBy(). ToList(). – code4life

+0

Fajnie, zastanawiałem się, jak silnie napisać moje rozwiązanie - to, jest lepsze: trzymaj je w kapsule w delegacie. – Douglas

3

Czy spojrzał Dynamic LINQ

Konkretnie, można po prostu zrobić coś takiego:

var column = PortFolioSheetMapping.MarketId.ToString(); 
if (frm.SelectedSortColumn.IsBaseColumn) 
{ 
    if (frm.SortAscending) 
     pf.Holdings = pf.Holdings.OrderBy(column).ToList(); 
    else 
     pf.Holdings = pf.Holdings.OrderByDescending(column).ToList(); 
} 

Uwaga: To ma ograniczenie, że wyliczenia dopasować swoje nazwy kolumn, jeśli to Ci odpowiada .

EDIT

po raz pierwszy opuścił z obiektem Product. W takich przypadkach DynamicLINQ będzie musiał zobaczyć, na przykład, "Product.ProductId". Możesz podać nazwę właściwości lub po prostu użyć "dobrze znanej" wartości i dołączyć do enum .ToString(). W tym momencie po prostu zmuszam moją odpowiedź na twoje pytanie, aby przynajmniej było sprawnym rozwiązaniem.

+2

To jest zła odpowiedź Myślę, że wygląda to tak: "przeczytaj to, może pomóc i jestem leniwy, aby pomóc Ci znaleźć rozwiązanie" =) – Restuta

+0

Linki są dobre, unikają zbędnych informacji. –

+0

@Marc, dziękuję za link (gra słów nie jest przeznaczona), ale w jaki sposób pomogłoby to zmniejszyć mój ogromny komunikat zmiany? – code4life

1

Można zaimplementować niestandardową klasę IComparer, która używa refleksji. Jednak byłoby to wolniej.

Oto klasę, która kiedyś używany:

class ListComparer : IComparer 
{ 
    private ComparerState State = ComparerState.Init; 
    public string Field {get;set;} 


    public int Compare(object x, object y) 
    { 
     object cx; 
     object cy; 

     if (State == ComparerState.Init) 
     { 
      if (x.GetType().GetProperty(pField) == null) 
       State = ComparerState.Field; 
      else 
       State = ComparerState.Property; 
     } 

     if (State == ComparerState.Property) 
     { 
      cx = x.GetType().GetProperty(Field).GetValue(x,null); 
      cy = y.GetType().GetProperty(Field).GetValue(y,null); 
     } 
     else 
     { 
      cx = x.GetType().GetField(Field).GetValue(x); 
      cy = y.GetType().GetField(Field).GetValue(y); 
     } 


     if (cx == null) 
      if (cy == null) 
       return 0; 
      else 
       return -1; 
     else if (cy == null) 
      return 1; 

     return ((IComparable) cx).CompareTo((IComparable) cy); 

    } 

    private enum ComparerState 
    { 
     Init, 
     Field, 
     Property 
    } 
} 

następnie używać go tak:

var comparer = new ListComparer() { 
    Field= frm.SelectedSortColumn.BaseColumn.ToString() }; 
if (frm.SortAscending) 
    pf.Holding = pf.Holding.OrderBy(h=>h.Product, comparer).ToList(); 
else 
    pf.Holding = pf.Holding.OrderByDescending(h=>h.Product, comparer).ToList(); 
+0

Jestem gotów na to spojrzeć - czy możesz nieco bardziej szczegółowo opisać, w jaki sposób zostanie zastosowana refleksja? Nie wiem jak to pasuje do mojej instrukcji switch i wartości wyliczeniowych ... – code4life

+0

Jednak jeśli pozwala na to twoja wersja framework, wolałbym rozwiązanie Dynamic LINQ! –

1

jak o:

Func<Holding, object> sortBy; 

switch (frm.SelectedSortColumn.BaseColumn) 
{ 
    case PortfolioSheetMapping.IssueId: 
     sortBy = c => c.Product.IssueId; 
     break; 
    case PortfolioSheetMapping.MarketId: 
     sortBy = c => c.Product.MarketId; 
     break; 
    /// etc. 
} 

/// EDIT: can't use var here or it'll try to use IQueryable<> which doesn't Reverse() properly 
IEnumerable<Holding> sorted = pf.Holdings.OrderBy(sortBy); 
if (!frm.SortAscending) 
{ 
    sorted = sorted.Reverse(); 
} 

?

Niezupełnie najszybsze rozwiązanie, ale jest dość eleganckie, o co prosisz!

EDYTOWANIE: Aha, i ze sprawą case, prawdopodobnie potrzebuje refaktoryzacji do oddzielnej funkcji, która zwraca Func, nie jest to dobry sposób na pozbycie się go w całości, ale możesz przynajmniej ukryć go przed środek twojej procedury!

+0

@Michael naprawił oryginał, w którym skopiowałem niewłaściwą, jeśli to prawda, tak, tak? –

+0

To prawda, ale wolałbym IEnumerable posortowane = (frm.SortAscending)? pf.Holdings.OrderBy (sortBy): pf.Holdings.OrderByDescending (sortBy). To pozwala uniknąć Reverse() –

+0

@ Michael Nie jestem pewien, czy robi to ogromną różnicę, wydaje się, że OrderByDescending po prostu zwraca wewnętrznie wewnętrznie odwróconą enumerator, sądząc po wynikach z Reflectora, ale rozumiem. –

4

Można spróbować zmniejszyć przełącznik do czegoś takiego:

private static readonly Dictionary<PortfolioSheetMapping, Func<Holding, object>> sortingOperations = new Dictionary<PortfolioSheetMapping, Func<Holding, object>> 
    { 
     {PortfolioSheetMapping.Symbol, h => h.Symbol}, 
     {PortfolioSheetMapping.Quantitiy, h => h.Quantitiy}, 
     // more.... 
    }; 

    public static List<Holding> SortHoldings(this List<Holding> holdings, SortOrder sortOrder, PortfolioSheetMapping sortField) 
    { 
     if (sortOrder == SortOrder.Decreasing) 
     { 
      return holdings.OrderByDescending(sortingOperations[sortField]).ToList(); 
     } 
     else 
     { 
      return holdings.OrderBy(sortingOperations[sortField]).ToList();     
     } 
    } 

Można wypełnić sortingOperations z refleksji, lub utrzymać ją ręcznie. Możesz także sprawić, by SortHoldings akceptował i zwracał wartość IEnumerable i usuwał wywołania ToList, jeśli nie masz nic przeciwko wywołaniu ToList w dzwoni później. Nie jestem w 100% pewna, że ​​OrderBy jest szczęśliwy, otrzymując obiekt, ale wart strzał.

Edycja: Zobacz rozwiązanie LukeH, aby trzymać rzeczy mocno wpisane.

+0

Chciałem zaproponować słownik klas porównawczych, ale jest to DUŻO schludniejsze! –

+0

Dzięki - to pomaga! – code4life

+0

dzięki za napiwek! Jest to zdecydowanie łatwiejsze do zakodowania, ale myślę, że pójdę z sugestią @ LukeH do użycia .Sort() zamiast, ponieważ jest nieco szybszy. Ale koncepcyjnie to samo podejście, co ty (używając słownika itp.). – code4life

1

Wydaje mi się, że istnieją dwa natychmiastowe ulepszenia możemy zrobić:

  • logika, która wykorzystuje frm.SortAscending zdecydować między OrderBy i OrderByDesccending jest powielany w każdym case i może być wyciągnięta do po switch jeśli case s zmieniane są nic nie robić więcej niż ustalenia klucza sortowania i wprowadzenie go w Func

  • że nadal pozostawia switch się z Oczywiście - i to może być zastąpione statyczną mapą (w Dictionary, powiedzmy) z PortfolioSheetMapping na Func biorąc Holding i zwracając klucz sortowania. np

+0

Dzięki, są to dobre sugestie, zdecydowanie pomaga modularyzacji kodu lepiej. – code4life

1

Jeśli właściwości w klasie Holding (symbol, cena itp) są takie same wpisać można wykonać następujące czynności:

var holdingList = new List<Holding>() 
{ 
     new Holding() { Quantity = 2, Price = 5 }, 
     new Holding() { Quantity = 7, Price = 2 }, 
     new Holding() { Quantity = 1, Price = 3 } 
}; 

var lookup = new Dictionary<PortfolioSheetMapping, Func<Holding, int>>() 
{ 
     { PortfolioSheetMapping.Price, new Func<Holding, int>(x => x.Price) }, 
     { PortfolioSheetMapping.Symbol, new Func<Holding, int>(x => x.Symbol) }, 
     { PortfolioSheetMapping.Quantitiy, new Func<Holding, int>(x => x.Quantity) } 
}; 

Console.WriteLine("Original values:"); 
foreach (var sortedItem in holdingList) 
{ 
    Console.WriteLine("Quantity = {0}, price = {1}", sortedItem.Quantity, sortedItem.Price); 
} 

var item = PortfolioSheetMapping.Price; 
Func<Holding, int> action; 
if (lookup.TryGetValue(item, out action)) 
{ 
    Console.WriteLine("Values sorted by {0}:", item); 
    foreach (var sortedItem in holdingList.OrderBy(action)) 
    { 
     Console.WriteLine("Quantity = {0}, price = {1}", sortedItem.Quantity, sortedItem.Price); 
    } 
} 

który następnie wyświetla:

Original values:
Quantity = 2, price = 5
Quantity = 7, price = 2
Quantity = 1, price = 3

Values sorted by Price:
Quantity = 7, price = 2
Quantity = 1, price = 3
Quantity = 2, price = 5

+0

Podoba mi się pomysł Funca. Właściwości w klasie Holding są różnymi typami, więc musiałbym przyjąć zmodyfikowane podejście z tego, co sugerujesz, ale mimo to dobry pomysł. – code4life

+0

Właśnie widziałem inne odpowiedzi, które zostały zamieszczone, kiedy robiłem moje, i możesz zmienić odnośnik na lookup = new Dictionary > i to powinno działać –

Powiązane problemy