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.