Ищете лучший способ сортировки моего списка <T> - PullRequest
7 голосов
/ 29 июня 2010

Я рассматриваю фрагмент кода, который написал не так давно, и просто ненавижу способ обработки сортировки - мне интересно, сможет ли кто-нибудь показать мне лучший способ.

У меня есть класс, Holding, который содержит некоторую информацию. У меня есть другой класс, HoldingsList, который содержит List<Holding> член. У меня также есть enum, PortfolioSheetMapping, который содержит около 40 элементов.

Это выглядит примерно так:

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

У меня есть метод, который может вызывать сортировку списка в зависимости от того, какое перечисление выберет пользователь. В методе используется оператор переключения mondo, в котором более 40 случаев (тьфу!).

Ниже приведен короткий фрагмент кода:

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

Моя проблема с оператором switch. switch тесно связан с перечислением PortfolioSheetMapping, которое может измениться завтра или на следующий день. Каждый раз, когда он меняется, мне придется пересматривать этот оператор switch и добавлять к нему еще один блок case. Я просто боюсь, что в конечном итоге это заявление о переключении станет настолько большим, что станет совершенно неуправляемым.

Может кто-нибудь сказать мне, есть ли лучший способ отсортировать мой список?

Ответы [ 7 ]

5 голосов
/ 29 июня 2010

Вы переназначаете отсортированные данные прямо обратно к свойству pf.Holdings, так почему бы не обойти накладные расходы OrderBy и ToList и просто использовать метод списка Sort прямо вместо?

Вы можете использовать карту для хранения Comparison<T> делегатов для всех поддерживаемых сортировок и затем вызвать Sort(Comparison<T>) с соответствующим делегатом:

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));
}
4 голосов
/ 29 июня 2010

Вы можете попробовать уменьшить значение переключателя до следующего:

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

Вы можете заполнить операции сортировки отражением или сохранить его вручную.Вы также можете сделать так, чтобы SortHoldings принимал и возвращал IEnumerable и удалял вызовы ToList, если позже не возражаете против вызова ToList в вызывающей стороне.Я не уверен на 100%, что OrderBy счастлив получить объект, но стоит попробовать.

Редактировать: См. Решение LukeH, чтобы держать вещи строго напечатанными.

3 голосов
/ 29 июня 2010

Вы смотрели на Динамическое LINQ

В частности, вы могли бы просто сделать что-то вроде:

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

Примечание : Это действительно имеетограничение, что ваше перечисление соответствует вашим именам столбцов, если это вас устраивает.

EDIT

Пропустил свойство Product в первый раз.В этих случаях DynamicLINQ нужно будет увидеть, например, "Product.ProductId".Вы можете отразить имя свойства или просто использовать «общеизвестное» значение и согласиться с перечислением .ToString().На данный момент я просто заставляю свой ответ на ваш вопрос, чтобы он по крайней мере был рабочим решением.

1 голос
/ 29 июня 2010

Если свойства класса Holding (символ, цена и т. Д.) Одного типа, вы можете сделать следующее:

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

, который затем отображает:

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

1 голос
/ 29 июня 2010

Мне кажется, что мы можем сделать два немедленных улучшения:

  • логика, которая использует frm.SortAscending для выбора между OrderBy и OrderByDesccending, дублируется в каждомcase, и может быть извлечен после switch, если case s изменены, чтобы ничего не делать, кроме как установить ключ сортировки и вставить его в Func

  • , который все еще оставляет switch сам по себе, разумеется - и его можно заменить статической картой (скажем, в Dictionary) с PortfolioSheetMapping на Func, взяв Holding и вернув ключ сортировки,например,

1 голос
/ 29 июня 2010

как насчет:

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

?

Не совсем быстрое решение, но оно достаточно элегантное, о чем вы и просили!и с оператором case, возможно, потребуется рефакторинг для отдельной функции, которая возвращает Func, не очень хороший способ полностью избавиться от него, но вы можете, по крайней мере, скрыть его от середины вашей процедуры!

1 голос
/ 29 июня 2010

Вы можете реализовать собственный класс IComparer, который использует отражение. Однако это будет медленнее.

Вот класс, который я когда-то использовал:

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

Тогда используйте это так:

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