Jak już wspomniano, najładniejszy pod względem zrobienia tego raz w małej ilości kodu jest:
decimal result = (from Item itm in itemList
where itm.Amount > 0
select itm.Amount).DefaultIfEmpty().Min();
Z odlewania itm.Amount
do decimal?
i uzyskiwania Min
z tego bycia neatest jeśli chcemy być w stanie wykryć tę pustą warunek.
Jeśli jednak chcesz faktycznie zapewnić MinOrDefault()
, możemy oczywiście zacząć od:
public static TSource MinOrDefault<TSource>(this IQueryable<TSource> source, TSource defaultValue)
{
return source.DefaultIfEmpty(defaultValue).Min();
}
public static TSource MinOrDefault<TSource>(this IQueryable<TSource> source)
{
return source.DefaultIfEmpty(defaultValue).Min();
}
public static TResult MinOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, TSource defaultValue)
{
return source.DefaultIfEmpty(defaultValue).Min(selector);
}
public static TResult MinOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector)
{
return source.DefaultIfEmpty().Min(selector);
}
Masz teraz pełny zestaw tego, MinOrDefault
czy chcesz dołączyć selektor i czy określasz wartość domyślną, czy nie.
Od tego momentu Twój kod jest po prostu:
decimal result = (from Item itm in itemList
where itm.Amount > 0
select itm.Amount).MinOrDefault();
Tak więc, chociaż na początku nie jest tak schludnie, odtąd jest schludniej.
Ale poczekaj! Jest więcej!
Powiedzmy, że używasz EF i chcesz skorzystać ze async
wsparcia. Łatwe do zrobienia:
public static Task<TSource> MinOrDefaultAsync<TSource>(this IQueryable<TSource> source, TSource defaultValue)
{
return source.DefaultIfEmpty(defaultValue).MinAsync();
}
public static Task<TSource> MinOrDefaultAsync<TSource>(this IQueryable<TSource> source)
{
return source.DefaultIfEmpty(defaultValue).MinAsync();
}
public static Task<TSource> MinOrDefaultAsync<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, TSource defaultValue)
{
return source.DefaultIfEmpty(defaultValue).MinAsync(selector);
}
public static Task<TSource> MinOrDefaultAsync<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector)
{
return source.DefaultIfEmpty().MinAsync(selector);
}
(Zwróć uwagę, że nie używam await
tutaj; możemy bezpośrednio stworzyć Task<TSource>
bez niego to, czego potrzebujemy, a tym samym uniknąć ukrytych komplikacji await
).
Ale czekaj, jest więcej! Powiedzmy, że używamy tego IEnumerable<T>
czasami. Nasze podejście jest nieoptymalne. Z pewnością możemy zrobić to lepiej!
Po pierwsze, Min
określa się na int?
, long?
, float?
double?
a decimal?
już w każdym razie to, co my chcemy (jako odpowiedź marki Marc Gravell korzystają z). Podobnie, otrzymujemy również zachowanie, które chcemy, z Min
już zdefiniowanego, jeśli zostanie wywołane do innego T?
. Zróbmy więc kilka małych, a przez to łatwych do wprowadzenia metod, aby skorzystać z tego faktu:
public static TSource? MinOrDefault<TSource>(this IEnumerable<TSource?> source, TSource? defaultValue) where TSource : struct
{
return source.Min() ?? defaultValue;
}
public static TSource? MinOrDefault<TSource>(this IEnumerable<TSource?> source) where TSource : struct
{
return source.Min();
}
public static TResult? Min<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult?> selector, TResult? defaultValue) where TResult : struct
{
return source.Min(selector) ?? defaultValue;
}
public static TResult? Min<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult?> selector) where TResult : struct
{
return source.Min(selector);
}
Teraz zacznijmy od bardziej ogólnego przypadku:
public static TSource MinOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue)
{
if(default(TSource) == null)
{
var result = source.Min();
return result == null ? defaultValue : result;
}
else
{
var comparer = Comparer<TSource>.Default;
using(var en = source.GetEnumerator())
if(en.MoveNext())
{
var currentMin = en.Current;
while(en.MoveNext())
{
var current = en.Current;
if(comparer.Compare(current, currentMin) < 0)
currentMin = current;
}
return currentMin;
}
}
return defaultValue;
}
Teraz oczywiste nadpisania, które wykorzystują to:
public static TSource MinOrDefault<TSource>(this IEnumerable<TSource> source)
{
var defaultValue = default(TSource);
return defaultValue == null ? source.Min() : source.MinOrDefault(defaultValue);
}
public static TResult MinOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector, TResult defaultValue)
{
return source.Select(selector).MinOrDefault(defaultValue);
}
public static TResult MinOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
return source.Select(selector).MinOrDefault();
}
Jeśli naprawdę zależy nam na wydajności, możemy zoptymalizować ją pod kątem określonych przypadków, tak jak Enumerable.Min()
:
public static int MinOrDefault(this IEnumerable<int> source, int defaultValue)
{
using(var en = source.GetEnumerator())
if(en.MoveNext())
{
var currentMin = en.Current;
while(en.MoveNext())
{
var current = en.Current;
if(current < currentMin)
currentMin = current;
}
return currentMin;
}
return defaultValue;
}
public static int MinOrDefault(this IEnumerable<int> source)
{
return source.MinOrDefault(0);
}
public static int MinOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector, int defaultValue)
{
return source.Select(selector).MinOrDefault(defaultValue);
}
public static int MinOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector)
{
return source.Select(selector).MinOrDefault();
}
I tak dalej long
, float
, double
i decimal
dopasować zestaw Min()
zapewnia Enumerable
. W takich sytuacjach przydatne są szablony T4.
Na koniec mamy tak wydajną implementację, MinOrDefault()
na jaką mogliśmy liczyć, dla szerokiej gamy typów. Z pewnością nie „schludny” w obliczu jednego użycia do tego (ponownie, po prostu użyj DefaultIfEmpty().Min()
), ale bardzo „schludny”, jeśli często go używamy, więc mamy fajną bibliotekę, którą możemy ponownie wykorzystać (lub rzeczywiście wkleić do odpowiedzi na StackOverflow…).