2010-06-29 9 views
7

sto rivedendo un pezzo di codice non scritto molto tempo fa, e ho appena odio il modo in cui ha gestito la selezione - Mi chiedo se qualcuno potrebbe essere in grado di mostrare io un modo migliore.ricerca di un modo migliore per risolvere la mia lista <T>

Ho una classe, Holding, che contiene alcune informazioni. Ho un'altra classe, HoldingsList, che contiene un membro List<Holding>. Ho anche un enum, PortfolioSheetMapping, che ha circa 40 elementi.

E 'sorta di simile a questo:

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

Ho un metodo che può richiamare l'elenco di essere ordinati a seconda di quale enumerazione l'utente seleziona. Il metodo utilizza un'istruzione switch mondiale che ha oltre 40 casi (ugh!).

Un breve frammento seguente illustra il codice:

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 .... 

Il mio problema è con l'istruzione switch. Lo switch è strettamente legato all'enum PortfolioSheetMapping, che può cambiare domani o il giorno successivo. Ogni volta che cambierò, dovrò rivisitare questa istruzione switch e aggiungere ancora un altro blocco case ad esso. Ho solo paura che alla fine questa affermazione di commutazione diventerà così grande da essere completamente ingestibile.

Qualcuno può dirmi se c'è un modo migliore per ordinare la mia lista?

+0

Perché questo ordinamento stato fatto? È solo a scopo di visualizzazione? –

+0

@Hans, l'istanza di classe viene deserializzata da un foglio di calcolo di Excel contenente dati di analisi di portafoglio.L'azione effettiva viene richiamata da un pulsante della barra degli strumenti di Excel (ma ciò è davvero irrilevante) e una volta che l'ordinamento è stato completato, rioricalizzo nuovamente gli oggetti nel foglio di calcolo. L'ordinamento influisce su vari altri elementi nel foglio di calcolo di Excel, quindi non è puramente di sola visualizzazione. – code4life

+2

Prolungando il mio commento sulla risposta di Mark ... sarebbe possibile per voi refactoring il codice per gestire i dati in un modulo più simile al database per cominciare? Potresti non aver nemmeno bisogno dell'enumerazione se hai usato un formato più in stile database (dato che potresti semplicemente aggiungere e rimuovere le colonne dalla tua "tabella" se necessario), quindi se sei disposto a dedicare tempo/sforzi tale refactoring, si potrebbe finire con qualcosa di più elegante, per così dire. Certamente, potrebbero esserci degli aspetti del tuo programma altrove che potrebbero rendere questo approccio meno fattibile di quanto possa sembrare ... – JAB

risposta

5

Si sta ri-assegnando i dati ordinati subito indietro alla vostra proprietà pf.Holdings, quindi perché non bypassare il sovraccarico di OrderBy e ToList e basta usare il metodo della lista Sort direttamente invece?

Si potrebbe utilizzare una mappa per tenere Comparison<T> delegati di tutte le cernite supportati e quindi chiamare Sort(Comparison<T>) con il delegato appropriato:

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

Buon consiglio, sicuramente da provare. – code4life

+0

grazie! Aneddoticamente, lo .Sort è leggermente ma decisamente più veloce di .OrderBy(). ToList(). – code4life

+0

Bello, mi stavo chiedendo come scrivere fortemente la mia soluzione: questo, è meglio: tenerlo incapsulato nel delegato. – Douglas

3

Hai guardato in Dynamic LINQ

In particolare, si può semplicemente fare qualcosa di simile:

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

Nota: Questo ha il vincolo che il vostro enum corrispondono ai nomi di colonna, se questo ti si addice .

EDIT

saltata la proprietà Product la prima volta. In questi casi, DynamicLINQ dovrà vedere, ad esempio, "Product.ProductId". È possibile riflettere il nome della proprietà o semplicemente utilizzare un valore "noto" e concat con l'enum .ToString(). A questo punto, sto solo forzando la mia risposta alla tua domanda in modo che sia almeno una soluzione funzionante.

+2

Questa è una cattiva risposta, penso, sembra "leggi questo, potrebbe essere utile e io sono pigro per aiutarti a trovare una soluzione" =) – Restuta

+0

I link sono buoni, evitano le informazioni ridondanti. –

+0

@Marc, grazie per il link (nessun gioco di parole previsto), ma come questo potrebbe aiutare a ridurre la mia enorme affermazione di switch? – code4life

1

È possibile implementare una classe IComparer personalizzata che utilizza la riflessione. Tuttavia questo sarebbe più lento.

Ecco una classe, che una volta ho usato:

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

quindi utilizzarlo in questo modo:

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

Sono disposto a dare un'occhiata a questo - puoi approfondire un po 'il modo in cui il riflesso sarebbe usato? Non sono sicuro di come si adatti alla mia istruzione switch e ai valori di enumerazione ... – code4life

+0

Tuttavia, se la versione della framework lo consente, preferirei la soluzione Dynamic LINQ! –

1

come circa:

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

?

Non esattamente la soluzione più veloce, ma è ragionevolmente elegante, che è quello che stavi chiedendo!

EDIT: Oh, e con l'istruzione case, probabilmente ha bisogno di refactoring per una funzione separata che restituisce un Func, non proprio un bel modo per sbarazzarsi del tutto, ma almeno lo si può nascondere lontano dal metà della tua procedura!

+0

@Michael ha corretto l'originale in cui avevo copiato male se, è corretto ora sì? –

+0

È corretto, ma preferirei IEnumerable ordinato = (frm.SortAscending)? pf.Holdings.OrderBy (sortBy): pf.Holdings.OrderByDescending (sortBy). Ciò evita il Reverse() –

+0

@Michael Non sono sicuro che faccia un'enorme differenza, sembra che OrderByDescending restituisca un enumeratore invertito internamente comunque a giudicare dall'output di Reflector, ma capisco il tuo punto. –

4

Si potrebbe provare a ridurre il passaggio a qualcosa di simile:

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

Si potrebbe compilare sortingOperations con la riflessione, o mantenere a mano. È inoltre possibile che SortHoldings accetti e restituisca un oggetto IEnumerable e rimuova le chiamate ToList se non si mente di chiamare ToList nel chiamante in seguito. Non sono sicuro al 100% che OrderBy sia felice di ricevere un oggetto, ma ne vale la pena.

Modifica: vedi la soluzione di LukeH per mantenere le cose fortemente digitate.

+0

Stavo per suggerire un dizionario di classi di confronto, ma questo è MOLTO più ordinato! –

+0

Grazie - questo aiuta! – code4life

+0

grazie per il suggerimento! Questo è sicuramente più facile da codificare, ma penso che andrò con il suggerimento di @ LukeH di usare .Sort() invece, perché è un po 'più veloce. Ma concettualmente lo stesso approccio (usando un dizionario, ecc. Ecc.). – code4life

1

Mi sembra che ci siano due miglioramenti immediati possiamo fare:

  • la logica che utilizza frm.SortAscending decidere tra OrderBy e OrderByDesccending è duplicato in ogni case, e può essere tirato fuori per dopo la switch se i case s vengono modificate a fare niente di più che istituisce la chiave di ordinamento e la messa in un Func

  • che lascia ancora la stessa switch di Naturalmente - e questo può essere sostituito da una mappa statica (in un Dictionary, ad esempio) da PortfolioSheetMapping a Func prendendo un Holding e restituendo la chiave di ordinamento. ad esempio

+0

Grazie, questi sono buoni suggerimenti, aiuta sicuramente a modulare meglio il codice. – code4life

1

Se le proprietà della classe Holding (simbolo, prezzo, ecc) sono uguali digitazione è possibile effettuare le seguenti operazioni:

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

che poi mostra:

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

Mi piace l'idea di Func. Le proprietà nella classe Holding sono di tipo diverso, quindi dovrei adottare un approccio modificato da quello che stai suggerendo, ma comunque una buona idea. – code4life

+0

Ho appena visto le altre risposte che sono state pubblicate quando stavo facendo il mio e puoi cambiare la ricerca per cercare = nuovo dizionario > e questo dovrebbe funzionare –

Problemi correlati