65.9K
CodeProject 正在变化。 阅读更多。
Home

流利的 IEnumerable_T_.Except()

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (3投票s)

2008年11月30日

CPOL

2分钟阅读

viewsIcon

26443

downloadIcon

83

本文提出了一种解决方案,用于在保持流畅编程风格的同时,对 IEnumerable 执行灵活的集合排除(A-B)操作。

引言

通常需要能够找到一个集合中不在另一个集合中的项目。

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 - 首次博客条目。
© . All rights reserved.