Поиск лучшего способа отсортировать мой Список <T>

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

У меня есть класс, Holding, который содержит некоторую информацию. У меня есть другой класс, HoldingsList, который содержит a List<Holding> участник. У меня также есть перечисление, 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 ...
}

У меня есть метод, который может вызвать List, который будет отсортирован, в зависимости от которого перечисления пользователь выбирает. Метод использует 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 плотно связывается с PortfolioSheetMapping перечисление, которое может измениться завтра или на следующий день. Каждый раз, когда это изменяется, я оказываюсь перед необходимостью пересматривать этот оператор переключения и добавлять еще один case блок к нему. Я просто боюсь, что в конечном счете этот оператор переключения станет столь большим, что это совершенно неуправляемо.

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

7
задан code4life 29 June 2010 в 14:06
поделиться

7 ответов

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

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

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));
}
5
ответ дан 6 December 2019 в 12:46
поделиться

Изучили ли вы Dynamic 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();
}

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

РЕДАКТИРОВАТЬ

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

3
ответ дан 6 December 2019 в 12:46
поделиться

как насчет:

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
ответ дан 6 December 2019 в 12:46
поделиться

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

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

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

1
ответ дан 6 December 2019 в 12:46
поделиться

Если свойства в классе 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);
    }
}

, после чего отобразится:

Исходные значения:
Количество = 2, цена = 5
Количество = 7, цена = 2
Количество = 1, цена = 3

Значения, отсортированные по цене:
Количество = 7, цена = 2
Количество = 1, цена = 3
Количество = 2, цена = 5

1
ответ дан 6 December 2019 в 12:46
поделиться

Вы можете попробовать сократить переключатель до чего-то вроде этого:

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

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

Edit: См. решение LukeH для сохранения сильной типизации.

4
ответ дан 6 December 2019 в 12:46
поделиться

Вы можете реализовать собственный класс 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();
1
ответ дан 6 December 2019 в 12:46
поделиться
Другие вопросы по тегам:

Похожие вопросы: