Using the LINQ Max Operator

This article explains the usage of the Max operator from LINQ to objects.

The Max operator (an extension method defined on the Enumerable class) allows finding the maximum value in a sequence based on arbitrary criteria.

While there are many overloads of it, let’s consider the following first:

public static TSource Max<TSource>(
this IEnumerable<TSource> source
)

Here, source parameter represents the sequence. Here, Max returns the maximum value for the type TSource in the sequence.
Now, the notion of Maximum will depend on the comparison logic between individual items of the source sequence. For this overload to work, TSource needs to implement IComparable<TSource> or IComparable. If TSource implements IComparable<TSource>, Max uses that implementation else if TSource implements IComparable, it uses that. For an empty sequence or  sequence with only null items, it returns null.

Example:

Let’s consider the following sequence:
var rects = new List<Rect>
{
new Rect { Length = 2, Breadth = 2},
new Rect { Length = 2, Breadth = 2 },
new Rect { Length = 3, Breadth = 3}
};

Rect is defined as follows:

class Rect : IComparable<Rect>
{
public int Length { get; set; }
public int Breadth { get; set; }

public int Area
{
get
{
return this.Length * this.Breadth;
}
}

public int CompareTo(Rect other)
{
if (this.Area < other.Area)
return -1;
else if (this.Area > other.Area)
return 1;
else
return 0;
}

public override string ToString()
{
return string.Format("Rect with dimensions: {0} X {1}", this.Length, this.Breadth);
}
}

As you can see, the implementation for IComparable<Rect>.CompareTo method compares the areas of the rectangles. The CompareTo method needs to return a negative, positive or a zero value to indicate that the given instance (of Rect, in this case) is smaller, greater or equal to the passed instance (again, Rect) respectively.

Now, to get the Rect with maximum area, we can use the Max operator as:
Console.WriteLine("Rect with maximum area {0}", rects.Max());

Here is the output we get:
Rect with maximum area Rect with dimensions: 3 X 3

The text 3X3 comes from the overridden ToString implementation of the Rect class.
What if we want to print the maximum perimeter value out of all the given rectangles?

There are two points to address here:
Firstly, we cannot provide another implementation of IComparable<Rect> in the same Rect class. Secondly, we need to get back an integer (representing the maximum perimeter value) and not a Rect instance. So, we need to apply a transformation to each Rect instance of the sequence (to get their perimeters) and then get Max out of them.

This is how we can do it:
Console.WriteLine("Rect having maximum perimeter: {0}", rects.Max(r => 2 * (r.Length + r.Breadth)));

Here, we use another overload of Max that takes in an instance of a Func<Rect, int> delegate (in this case, a lambda that calculates the perimeter of each Rect). Max then calculates the maximum value out of all those perimeters and returns that value.
As we can see that the return type doesn’t have to be Rect (in fact, there are multiple overloads for Max that can return a decimal, double, int or a long).

There is even a generic overload of Max, that can return even an arbitrary type TResult. Here is how its signature looks like:
public static TResult Max<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector
)

Here, Func<TSource, TResult> is the transformation function to apply on each item of source (of type TSource) that transforms it to an instance of TResult.
Max then calculates the maximum value out of TResult (so long as TResult implements IComparable<TResult> or IComparable).

Example:
Let’s consider that we want to transform every Rect into a Cuboid by applying a standard height, say 10 (but keep the length and breadth same as that of a Rect). Then we want to get the Cuboid with maximum volume.

The Cuboid class is defined as follows:
class Cuboid : IComparable<Cuboid>
{
public int Length { get; set; }
public int Breadth { get; set; }
public int Height { get; set; }

public Cuboid(Rect rect, int height)
{
this.Length = rect.Length;
this.Breadth = rect.Breadth;
this.Height = height;
}

public int Volume
{
get
{
return this.Length * this.Breadth * this.Height;
}
}

public int CompareTo(Cuboid other)
{
if (this.Volume < other.Volume)
return -1;
else if (this.Volume > other.Volume)
return 1;
else
return 0;
}

public override string ToString()
{
return string.Format("Cuboid with dimensions: {0} X {1} X {2}", this.Length, this.Breadth, this.Height);
}
}

The implementation of IComparable<Cuboid> is similar (just that we compare Volume instead of Area). The implementation of ToString is similar as well.

Now, to get the cuboid with maximum volume, we can use the following code snippet:
Console.WriteLine("Cuboid with maximum volume: {0}", rects.Max(r => new Cuboid(r, 10)));

This time, we use a Func<Rect, Cuboid> that instantiates a Cuboid out of a Rect by calling the Cuboid constructor that takes in a Rect and a standard height parameter (10). The reason Max is able to calculate the Cuboid with maximum volume is because of the implementation of the IComparable<Cuboid> interface on the Cuboid class that compares volumes.

As you might have already guessed, just like Max, there are similar overloads for Min operator, that calculates the minimum of a sequence (there too, one can similarly provide an arbitrary logic for comparison by implementing the same interface IComparable<TSource> or IComparable<TResult>). One point to note here is, if the intended result type is any one of the standard built in numeric types (i.e. int, long, decimal, double etc.), we don’t need the complications of implementing IComparable interface for the result type. There are multiple overloads of Max (or Min) that can return any one of these standard types so long as we supply an appropriate instance of the Func delegate that translate each source item to one of these built in types. We already saw an example with Max where we found out the maximum perimeter value out of all Rect instances.

There are more such operators with LINQ, but that’s for later.

By Indranil Chatterjee   Popularity  (1988 Views)