流利的 IEnumerable_T_.Except()






4.43/5 (3投票s)
本文提出了一种解决方案,用于在保持流畅编程风格的同时,对 IEnumerable 执行灵活的集合排除(A-B)操作。
引言
通常需要能够找到一个集合中不在另一个集合中的项目。
本文介绍了一种在 C# 中执行此操作的方法,同时保持流畅的编程风格。
问题所在
集合减法是一种常见的编码问题。例如,我经常在 SQL Server 中使用类似以下的查询来执行此操作:
select u.*
from Users u
left join Administrators a on u.UserId = a.UserId
where a.AdministratorId is null
LINQ 提供了 IEnumerable<T>.Except()
方法,该方法提供相同的功能。
IEnumerable<string> result = users.Except(administrators);
不幸的是,为了对其进行任何有趣的修改,你必须提供一个 IEqualityComparer<T>
……我将在本文中不对此进行介绍。幸运的是,我们也可以使用 join
语法来实现 Except 概念,如下所示:
IEnumerable<string> result =
from item in users
join otherItem in administrators on item equals otherItem into tempItems
from temp in tempItems.DefaultIfEmpty()
where temp != null
select item;
这看起来很像 SQL 实现。但是,在构建复杂的查询时,符号会很快变得混乱,并可能导致难以维护的代码。在本文中,我将基于 join
实现,以获得更大的灵活性。
一种解决方案
首先,构建一个扩展方法来隐藏 join
语法的复杂性。
[NotNull]
public static IEnumerable<T> Except<T>([NotNull] this IEnumerable<T> items,
[CanBeNull] IEnumerable<T> other)
{
// ... argument checks
return from item in items
join otherItem in other on item equals otherItem into tempItems
from temp in tempItems.DefaultIfEmpty()
where ReferenceEquals(null, temp) || temp.Equals(default(T))
select item;
}
请注意,where
子句已更改,以允许扩展方法在 T
是结构体还是类时工作。另请注意,该方法返回一个 IEnumerable<T>
,因此你可以将结果流畅地链接到另一个 LINQ 方法;例如:
IEnumerable<string> result =
users.Except(administrators).ToList().ForEach(Console.WriteLine);
以下是更有趣的 NUnit 测试:
[TestFixture]
public class When_asked_to_get_items_from_a_set_that_are_not_in_another_set
{
[Test]
public void Should_return_only_those_items_that_are_not_in_the_
other_set_where_T_is_a_class()
{
List<string> input = new List<string> {"cat",
"ran", "fast"};
List<string> other = new List<string> {"dog",
"ran", "too", "slow"};
IEnumerable<string> result = input.Except(other);
Assert.IsNotNull(result, "result should never be null");
Assert.AreEqual(2, result.Count(), "count does not match");
Assert.AreEqual("cat", result.First(), "first item in result is incorrect");
Assert.AreEqual("fast", result.Last(), "last item in result is incorrect");
}
[Test]
public void Should_return_only_those_items_that_are_
not_in_the_other_set_where_T_is_a_struct()
{
List<int> input = new List<int> {1, 2, 3};
List<int> other = new List<int> {0, 2, 4, 6};
IEnumerable<int> result = input.Except(other);
Assert.IsNotNull(result, "result should never be null");
Assert.AreEqual(2, result.Count(), "count does not match");
Assert.IsTrue(result.All(item => item.IsOdd()));
Assert.AreEqual(1, result.First(), "first item in result is incorrect");
Assert.AreEqual(3, result.Last(), "last item in result is incorrect");
}
}
提供比较方法
接下来,我们将创建一个重载,该重载接受 Lambda 表达式,用于比较两个集合中的项目。这允许你使用其他自然相等键来比较它们。
[NotNull]
public static IEnumerable<T> Except<T, TKey>([NotNull] this IEnumerable<T> items,
[CanBeNull] IEnumerable<T> other,
[NotNull] Func<T, TKey> getKey)
{
// ... argument checks
return from item in items
join otherItem in other on getKey(item)
equals getKey(otherItem) into tempItems
from temp in tempItems.DefaultIfEmpty()
where ReferenceEquals(null, temp) ||
temp.Equals(default(T))
select item;
}
可以使用以下内容测试重载方法:
public class TestItem
{
public string Name { get; set; }
}
[Test]
public void Should_return_only_those_items_that_are_not_in_the_other_set()
{
List<TestItem> input = new List<TestItem>
{
new TestItem {Name = "cat"},
new TestItem {Name = "ran"},
new TestItem {Name = "fast"}
};
List<TestItem> other = new List<TestItem>
{
new TestItem {Name = "dog"},
new TestItem {Name = "ran"},
new TestItem {Name = "too"},
new TestItem {Name = "slow"}
};
IEnumerable<TestItem> result = input.Except(other, item => item.Name);
Assert.IsNotNull(result, "result should never be null");
Assert.AreEqual(2, result.Count(), "count does not match");
Assert.AreEqual("cat", result.First().Name, "first item in result is incorrect");
Assert.AreEqual("fast", result.Last().Name, "last item in result is incorrect");
}
使用不同类型的排除
我们将添加的最后一个也是最灵活的重载允许集合包含不同的类型。例如,你可能在主集合中拥有用户,但在比较集合中只有管理员的 ID,并且你可能希望能够获取不是管理员的用户。该重载提供了此功能。
[NotNull]
public static IEnumerable<T> Except<T, TOther, TKey>(
[NotNull] this IEnumerable<T> items,
[CanBeNull] IEnumerable<TOther> other,
[NotNull] Func<T, TKey> getItemKey,
[NotNull] Func<TOther, TKey> getOtherKey)
{
// ... argument checks
return from item in items
join otherItem in other on getItemKey(item)
equals getOtherKey(otherItem) into tempItems
from temp in tempItems.DefaultIfEmpty()
where ReferenceEquals(null, temp) || temp.Equals(default(TOther))
select item;
}
测试用法如下:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
[Test]
public void Should_return_only_those_items_that_are_not_in_the_other_set()
{
List<User> users = new List<User>
{
new User {Id = 1, Name = "Maria"},
new User {Id = 2, Name = "ZiYi"},
new User {Id = 3, Name = "Altair"}
};
List<int> administratorIds = new List<int> {2, 4, 6};
IEnumerable<User> result = users.Except(administratorIds,
item => item.Id, administratorId => administratorId);
Assert.IsNotNull(result, "result should never be null");
Assert.AreEqual(2, result.Count(), "count does not match");
Assert.AreEqual("Maria", result.First().Name, "first item in result is incorrect");
Assert.AreEqual("Altair", result.Last().Name, "last item in result is incorrect");
}
历史
- 2008-11-30 - 首次在 CodeProject 上发布。
- 2008-11-23 - 首次博客条目。