Я рассматриваю часть кода, который я написал не слишком долго назад, и я просто ненавижу способ, которым я обработал сортировку - я задаюсь вопросом, смог ли кто-либо показывать мне лучший путь.
У меня есть класс, 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
блок к нему. Я просто боюсь, что в конечном счете этот оператор переключения станет столь большим, что это совершенно неуправляемо.
Кто-то может сказать мне, если существует лучший способ отсортировать мой список?
Вы повторно назначаете отсортированные данные обратно свойству 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));
}
Изучили ли вы 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 ()
. На данный момент я просто настаиваю на своем ответе на ваш вопрос, чтобы это, по крайней мере, было рабочим решением.
как насчет:
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, что не совсем хороший способ полностью избавиться от него, но вы можете по крайней мере скрыть его от середины вашей процедуры!
Мне кажется, что есть два немедленных улучшения, которые мы можем внести:
логику, которая использует frm.SortAscending
для выбора между OrderBy
и OrderByDesccending
дублируется в каждом случае
, и его можно вытащить после переключателя
, если case
s изменен ничего не делать, кроме как установить ключ сортировки и поместить его в Func
, который, конечно, по-прежнему оставляет сам переключатель
- и это можно заменить статической картой (в ] Dictionary
, скажем) от PortfolioSheetMapping
до Func
, принимая Holding
и возвращая ключ сортировки. например
Если свойства в классе 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
Вы можете попробовать сократить переключатель до чего-то вроде этого:
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 для сохранения сильной типизации.
Вы можете реализовать собственный класс 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();