Ich habe Marcs Antwort genommen, sie so korrigiert, dass sie mit TSource als Werttyp funktioniert (Test auf default(TSource) statt null), einige redundante Typspezifikationen bereinigt und einige Tests dafür geschrieben. Hier ist das, was ich heute verwende. Vielen Dank, Marc, für die tolle Idee und Umsetzung.
public static class LINQExtensions
{
public static IEnumerable<TSource> DistinctBy<TSource, TValue>(
this IEnumerable<TSource> source,
Func<TSource, TValue> selector)
{
var comparer = ProjectionComparer<TSource>.CompareBy(
selector, EqualityComparer<TValue>.Default);
return new HashSet<TSource>(source, comparer);
}
}
public static class ProjectionComparer<TSource>
{
public static IEqualityComparer<TSource> CompareBy<TValue>(
Func<TSource, TValue> selector)
{
return CompareBy(selector, EqualityComparer<TValue>.Default);
}
public static IEqualityComparer<TSource> CompareBy<TValue>(
Func<TSource, TValue> selector,
IEqualityComparer<TValue> comparer)
{
return new ComparerImpl<TValue>(selector, comparer);
}
sealed class ComparerImpl<TValue> : IEqualityComparer<TSource>
{
private readonly Func<TSource, TValue> _selector;
private readonly IEqualityComparer<TValue> _comparer;
public ComparerImpl(
Func<TSource, TValue> selector,
IEqualityComparer<TValue> comparer)
{
if (selector == null) throw new ArgumentNullException("selector");
if (comparer == null) throw new ArgumentNullException("comparer");
_selector = selector;
_comparer = comparer;
}
bool IEqualityComparer<TSource>.Equals(TSource x, TSource y)
{
if (x.Equals(default(TSource)) && y.Equals(default(TSource)))
{
return true;
}
if (x.Equals(default(TSource)) || y.Equals(default(TSource)))
{
return false;
}
return _comparer.Equals(_selector(x), _selector(y));
}
int IEqualityComparer<TSource>.GetHashCode(TSource obj)
{
return obj.Equals(default(TSource)) ? 0 : _comparer.GetHashCode(_selector(obj));
}
}
}
Und die Testklasse:
[TestClass]
public class LINQExtensionsTest
{
[TestMethod]
public void DistinctByTestDate()
{
var list = Enumerable.Range(0, 200).Select(i => new
{
Index = i,
Date = DateTime.Today.AddDays(i%4)
}).ToList();
var distinctList = list.DistinctBy(l => l.Date).ToList();
Assert.AreEqual(4, distinctList.Count);
Assert.AreEqual(0, distinctList[0].Index);
Assert.AreEqual(1, distinctList[1].Index);
Assert.AreEqual(2, distinctList[2].Index);
Assert.AreEqual(3, distinctList[3].Index);
Assert.AreEqual(DateTime.Today, distinctList[0].Date);
Assert.AreEqual(DateTime.Today.AddDays(1), distinctList[1].Date);
Assert.AreEqual(DateTime.Today.AddDays(2), distinctList[2].Date);
Assert.AreEqual(DateTime.Today.AddDays(3), distinctList[3].Date);
Assert.AreEqual(200, list.Count);
}
[TestMethod]
public void DistinctByTestInt()
{
var list = Enumerable.Range(0, 200).Select(i => new
{
Index = i % 4,
Date = DateTime.Today.AddDays(i)
}).ToList();
var distinctList = list.DistinctBy(l => l.Index).ToList();
Assert.AreEqual(4, distinctList.Count);
Assert.AreEqual(0, distinctList[0].Index);
Assert.AreEqual(1, distinctList[1].Index);
Assert.AreEqual(2, distinctList[2].Index);
Assert.AreEqual(3, distinctList[3].Index);
Assert.AreEqual(DateTime.Today, distinctList[0].Date);
Assert.AreEqual(DateTime.Today.AddDays(1), distinctList[1].Date);
Assert.AreEqual(DateTime.Today.AddDays(2), distinctList[2].Date);
Assert.AreEqual(DateTime.Today.AddDays(3), distinctList[3].Date);
Assert.AreEqual(200, list.Count);
}
struct EqualityTester
{
public readonly int Index;
public readonly DateTime Date;
public EqualityTester(int index, DateTime date) : this()
{
Index = index;
Date = date;
}
}
[TestMethod]
public void TestStruct()
{
var list = Enumerable.Range(0, 200)
.Select(i => new EqualityTester(i, DateTime.Today.AddDays(i%4)))
.ToList();
var distinctDateList = list.DistinctBy(e => e.Date).ToList();
var distinctIntList = list.DistinctBy(e => e.Index).ToList();
Assert.AreEqual(4, distinctDateList.Count);
Assert.AreEqual(200, distinctIntList.Count);
}
}