views:

187

answers:

6

i am new to .net 3.5. I have a collection of items:

IList<Model> models;

where

class Model
{
    public string Name
    {
       get;
       private set;
    }
}

I would like to get the element, which has the longest name's length. I tried

string maxItem = models.Max<Model>(model => model.Name.Length);

but it of course returns the maximum length (and I need a Model object). I know there is a way of doing this using the extension methods but I don't know how.

+7  A: 

There isn't a built-in way of doing this, unfortunately - but it's really easy to write an extension method to do it.

It was in one of my very first blog posts, in fact... note that there's a better implementation in one of the comments. I'll move it into the body if I get time.

EDIT: Okay, I have a slightly abbreviated version - it just returns the maximal element, using the given selector. No need to do a projection as well - do that once afterwards if you need to. Note that you could remove the constraint on TValue and use Comparer<TValue>.Default instead, or have an overload which allows the comparison to be specified as another parameter.

public static TSource MaxBy<TSource, TValue>(this IEnumerable<TSource> source,
                                             Func<TSource, TValue> selector)
    where TValue : IComparable<TValue>
{
    TValue maxValue = default(TValue);
    TSource maxElement = default(TSource);
    bool gotAny = false;

    foreach (TSource sourceValue in source)
    {
        TValue value = selector(sourceValue);
        if (!gotAny || value.CompareTo(maxValue) > 0)
        {
            maxValue = value;
            maxElement = sourceValue;
            gotAny = true;
        }
    }
    if (!gotAny)
    {
        throw new InvalidOperationException("source is empty");
    }
    return maxElement;
}

Sample use: (note type inference):

string maxName = models.MaxBy(model => model.Name.Length).Name;
Jon Skeet
i'm afraid models.Max(model => model.Name.Length) returns int (length) no a Model so you can't use it like this (i mean the last line)
agnieszka
@agnieszka: That's using the built-in Max method, not the one I've given in the answer. To make it clearer, you could rename the new one to "MaxElement". Editing answer to reflect this.
Jon Skeet
What happens if you have a set with the comparison value multiple times: i.e. {5, 3, 5, 2}. Element 0 will be returned if my understanding is correct. But what makes element 0 more special than element 2 which has the same value... maybe returning a list of the things with the max value would help?
Jennifer
imo it's lame the built-in Max<> function doesn't work like this naturally--wouldn't that be better/more intuitive?
chaiguy
The built-in Enumerable.Max() is just broken. What it does today could be written as "models.Max(model => model.Name.Length).Select(model => model.Name);" if Max() was written the right way. (The justification is that it handles both single- and multiple- result cases)
Jay Bazuzi
Or you can write "models.Select(model => model.Name).Max()" with a fixed Max() to get the behavior that Max() does today. I think that's what I meant. :-)
Jay Bazuzi
You could use the name MaxBy, just like OrderBy.
Jules
@Jay: I completely agree. I'm amazed that it's been done in the less flexible way.
Jon Skeet
@Jennifer: I think in *most* use cases you probably don't mind which gets returned, and it's more convenient to return a single element. There could always be a differently-named method with your suggested behaviour though.
Jon Skeet
@julesjacobs: Yes, I like your idea. I've edited the answer accordingly.
Jon Skeet
A: 

This is how I got it to work. Maybe there's a better way, I'm not sure:

    decimal de = d.Max(p => p.Name.Length);
    Model a = d.First(p => p.Name.Length == de);
BFree
The problem with that is that it has to go through the list twice. That *may* be very expensive (or require buffering of a stream of data). It's nicer to be able to do the whole thing in one pass where possible.
Jon Skeet
If you're writing library code that will be used in unknown contexts, Jon is right. If you know the context, you can know if two passes will be expensive or not. I'd write `d.OrderBy(p => p.Name.Length).First()` if I knew the context.
Jay Bazuzi
A: 

Is there anything gained by using the extension methods?

Perhaps a method or procedure with a simple iteration of the list would suffice?

Something to the effect of

Dim result as string = models(0).Name
for each m as Model in models
  if m.Name.length > result.length then
    result = m.Name
  end if
next
A: 

Another way might be:

var item = (from m in models select m orderby m.Name.Length descending).FirstOrDefault();

First one will be the one with the longest length.

NR
This requires buffering the whole stream, and sorting - O(n log n) for a fundamentally O(n) operation.
Jon Skeet
To put it another way: Max() only requires sorting the first element, but your algorithm requires sorting all elements.
Jay Bazuzi
+1  A: 

Here's another way of doing it. There's a version of Max that takes no criterion, and uses IComparable. So we could provide a way to wrap anything in a comparable object, with a delegate providing the comparison.

public class Comparable<T> : IComparable<Comparable<T>>
{
    private readonly T _value;
    private readonly Func<T, T, int> _compare;

    public Comparable(T v, Func<T, T, int> compare)
    {
        _value = v;
        _compare = compare;
    }

    public T Value { get { return _value; } }

    public int CompareTo(Comparable<T> other)
    {
        return _compare(_value, other._value);
    }
}

Then we can say:

Model maxModel = models.Select(m => new Comparable<Model>(m, (a, b) => a.Name.Length - b.Name.Length)).Max().Value;

This involves a lot of extra allocation, but it's sort of academically interesting (I think).

Daniel Earwicker
Someone once told me "a delegate is the same as an interface with only one member". You're basically making an Adapater between Func<T, T, int> and IComparable. I like it. The allocation cost isn't serious: it's not big, and the GC can handle the cleanup.
Jay Bazuzi
Thanks. I've updated it to use the generic IComparable which gets rid of the cast. Regarding delegates and one-member interfaces, check out http://tronicek.blogspot.com/2007/12/closure-conversion.html - he discusses this aspect in a proposed Java lauguage feature.
Daniel Earwicker
A: 

You can use Aggregate. It can be done without writing new extension method.

models.Aggregate(
                new KeyValuePair<Model, int>(),
                (a, b) => (a.Value < b.Name.Length) ? new KeyValuePair<Model, int>(b, b.Name.Length) : a,
                a => a.Key);
chaowman