C #: получение максимальных и минимальных значений произвольных свойств всех элементов в списке - PullRequest
18 голосов
/ 30 сентября 2008

У меня есть специализированный список, который содержит элементы типа IThing:

public class ThingList : IList<IThing>
{...}

public interface IThing
{
    Decimal Weight { get; set; }
    Decimal Velocity { get; set; }
    Decimal Distance { get; set; }
    Decimal Age { get; set; }
    Decimal AnotherValue { get; set; }

    [...even more properties and methods...]
}

Иногда мне нужно знать максимум или минимум определенного свойства всех вещей в списке. Из-за «Скажи, не спрашивай» мы позволили списку выяснить это:

public class ThingList : IList<IThing>
{
    public Decimal GetMaximumWeight()
    {
        Decimal result = 0;
        foreach (IThing thing in this) {
            result = Math.Max(result, thing.Weight);
        }
        return result;
    }
}

Это очень мило. Но иногда мне нужен минимальный вес, иногда максимальная скорость и так далее. Мне не нужна пара GetMaximum*()/GetMinimum*() для каждого свойства.

Одним из решений было бы отражение. Что-то вроде (держи свой нос, сильный запах кода!):

Decimal GetMaximum(String propertyName);
Decimal GetMinimum(String propertyName);

Есть ли лучшие, менее вонючие способы сделать это?

Спасибо, Eric

Редактировать: @Matt: .Net 2.0

Вывод: лучшего способа для .Net 2.0 (с Visual Studio 2005) не существует. Возможно, нам стоит перейти на .Net 3.5 и Visual Studio 2008 в ближайшее время. Спасибо, ребята.

Заключение: Есть разные способы, которые намного лучше, чем отражение. В зависимости от времени выполнения и версии C #. Посмотрите на ответ Джона Скитса для различий. Все ответы очень полезны.

Я пойду на предложение Sklivvz (анонимные методы). Есть несколько фрагментов кода от других людей (Конрад Рудольф, Мэтт Гамильтон и Coincoin), которые реализуют идею Sklivvz. К сожалению, я могу только «принять» один ответ.

Большое спасибо. Вы все можете чувствовать себя «принятым», хотя только Sklivvz получает кредиты; -)

Ответы [ 8 ]

32 голосов
/ 30 сентября 2008

(отредактировано для отражения ответа .NET 2.0 и LINQBridge в VS2005 ...)

Здесь есть три ситуации - хотя у ОП есть только .NET 2.0, другие люди, сталкивающиеся с такой же проблемой, могут не ...

1) Использование .NET 3.5 и C # 3.0: используйте LINQ to Objects следующим образом:

decimal maxWeight = list.Max(thing => thing.Weight);
decimal minWeight = list.Min(thing => thing.Weight);

2) Использование .NET 2.0 и C # 3.0: используйте LINQBridge и тот же код

3) Использование .NET 2.0 и C # 2.0: используйте LINQBridge и анонимные методы:

decimal maxWeight = Enumerable.Max(list, delegate(IThing thing) 
    { return thing.Weight; }
);
decimal minWeight = Enumerable.Min(list, delegate(IThing thing)
    { return thing.Weight; }
);

(У меня нет компилятора C # 2.0 для проверки вышеупомянутого - если он жалуется на неоднозначное преобразование, приведите делегат к Func .)

LINQBridge будет работать с VS2005, но у вас нет методов расширения, лямбда-выражений, выражений запросов и т. Д. Четкая миграция на C # 3 - более приятный вариант, но я бы предпочел использовать LINQBridge для реализации той же функциональности самостоятельно.

Все эти предложения предполагают, что вы должны пройти список дважды, если вам нужно получить как максимум, так и минимум. Если у вас возникла ситуация, когда вы загружаете с диска лениво или что-то в этом роде, и вы хотите рассчитать несколько агрегатов за один раз, вы можете посмотреть на мой "Push LINQ" код в MiscUtil . (Это работает и с .NET 2.0.)

19 голосов
/ 30 сентября 2008

Если вы используете .NET 3.5 и LINQ:

Decimal result = myThingList.Max(i => i.Weight);

Это сделало бы вычисление Min и Max довольно тривиальным.

10 голосов
/ 30 сентября 2008

Да, вы должны использовать делегат и анонимные методы.

Для примера см. здесь .

В основном вам нужно реализовать что-то похожее на метод поиска списков .

Вот пример реализации

public class Thing
{
    public int theInt;
    public char theChar;
    public DateTime theDateTime;

    public Thing(int theInt, char theChar, DateTime theDateTime)
    {
        this.theInt = theInt;
        this.theChar = theChar;
        this.theDateTime = theDateTime;
    }

    public string Dump()
    {
        return string.Format("I: {0}, S: {1}, D: {2}", 
            theInt, theChar, theDateTime);
    }
}

public class ThingCollection: List<Thing>
{
    public delegate Thing AggregateFunction(Thing Best, 
                        Thing Candidate);

    public Thing Aggregate(Thing Seed, AggregateFunction Func)
    {
        Thing res = Seed;
        foreach (Thing t in this) 
        {
            res = Func(res, t);
        }
        return res;
    }
}

class MainClass
{
    public static void Main(string[] args)
    {
        Thing a = new Thing(1,'z',DateTime.Now);
        Thing b = new Thing(2,'y',DateTime.Now.AddDays(1));
        Thing c = new Thing(3,'x',DateTime.Now.AddDays(-1));
        Thing d = new Thing(4,'w',DateTime.Now.AddDays(2));
        Thing e = new Thing(5,'v',DateTime.Now.AddDays(-2));

        ThingCollection tc = new ThingCollection();

        tc.AddRange(new Thing[]{a,b,c,d,e});

        Thing result;

        //Max by date
        result = tc.Aggregate(tc[0], 
            delegate (Thing Best, Thing Candidate) 
            { 
                return (Candidate.theDateTime.CompareTo(
                    Best.theDateTime) > 0) ? 
                    Candidate : 
                    Best;  
            }
        );
        Console.WriteLine("Max by date: {0}", result.Dump());

        //Min by char
        result = tc.Aggregate(tc[0], 
            delegate (Thing Best, Thing Candidate) 
            { 
                return (Candidate.theChar < Best.theChar) ? 
                    Candidate : 
                    Best; 
            }
        );
        Console.WriteLine("Min by char: {0}", result.Dump());               
    }
}

Результаты:

Max by date: I: 4, S: w, D: 10/3/2008 12:44:07 AM
Min by char: I: 5, S: v, D: 9/29/2008 12:44:07 AM

8 голосов
/ 30 сентября 2008

Если вы используете .NET 3.5, почему бы не использовать лямбды?

public Decimal GetMaximum(Func<IThing, Decimal> prop) {
    Decimal result = Decimal.MinValue;
    foreach (IThing thing in this)
        result = Math.Max(result, prop(thing));

    return result;
}

Использование:

Decimal result = list.GetMaximum(x => x.Weight);

Это строго типизировано и эффективно. Есть также методы расширения, которые уже делают именно это.

3 голосов
/ 30 сентября 2008

Вот попытка, используя C # 2.0, для идеи Skilwz.

public delegate T GetPropertyValueDelegate<T>(IThing t);

public T GetMaximum<T>(GetPropertyValueDelegate<T> getter)
    where T : IComparable
{
    if (this.Count == 0) return default(T);

    T max = getter(this[0]);
    for (int i = 1; i < this.Count; i++)
    {
        T ti = getter(this[i]);
        if (max.CompareTo(ti) < 0) max = ti;
    }
    return max;
}

Вы бы использовали это так:

ThingList list;
Decimal maxWeight = list.GetMaximum(delegate(IThing t) { return t.Weight; });
3 голосов
/ 30 сентября 2008

Для C # 2.0 и .Net 2.0 вы можете сделать следующее для Max:

public delegate Decimal GetProperty<TElement>(TElement element);

public static Decimal Max<TElement>(IEnumerable<TElement> enumeration, 
                                    GetProperty<TElement> getProperty)
{
    Decimal max = Decimal.MinValue;

    foreach (TElement element in enumeration)
    {
        Decimal propertyValue = getProperty(element);
        max = Math.Max(max, propertyValue);
    }

    return max;
}

А вот как вы бы это использовали:

string[] array = new string[] {"s","sss","ddsddd","333","44432333"};

Max(array, delegate(string e) { return e.Length;});

Вот как вы могли бы сделать это с C # 3.0, .Net 3.5 и Linq, без функции выше:

string[] array = new string[] {"s","sss","ddsddd","333","44432333"};
array.Max( e => e.Length);
2 голосов
/ 30 сентября 2008

Как насчет обобщенного решения .Net 2?

public delegate A AggregateAction<A, B>( A prevResult, B currentElement );

public static Tagg Aggregate<Tcoll, Tagg>( 
    IEnumerable<Tcoll> source, Tagg seed, AggregateAction<Tagg, Tcoll> func )
{
    Tagg result = seed;

    foreach ( Tcoll element in source ) 
        result = func( result, element );

    return result;
}

//this makes max easy
public static int Max( IEnumerable<int> source )
{
    return Aggregate<int,int>( source, 0, 
        delegate( int prev, int curr ) { return curr > prev ? curr : prev; } );
}

//but you could also do sum
public static int Sum( IEnumerable<int> source )
{
    return Aggregate<int,int>( source, 0, 
        delegate( int prev, int curr ) { return curr + prev; } );
}
2 голосов
/ 30 сентября 2008

Вывод: лучшего способа для .Net 2.0 (с Visual Studio 2005) не существует.

Вы, похоже, неправильно поняли ответы (особенно Джона). Вы можете использовать вариант 3 из его ответа. Если вы не хотите использовать LinqBridge, вы все равно можете использовать делегата и реализовать метод Max самостоятельно, аналогично методу, который я опубликовал:

delegate Decimal PropertyValue(IThing thing);

public class ThingList : IList<IThing> {
    public Decimal Max(PropertyValue prop) {
        Decimal result = Decimal.MinValue;
        foreach (IThing thing in this) {
            result = Math.Max(result, prop(thing));
        }
        return result;
    }
}

Использование:

ThingList lst;
lst.Max(delegate(IThing thing) { return thing.Age; });
...