Использование LINQ to Objects для поиска предметов в одной коллекции, которые не соответствуют другой - PullRequest
23 голосов
/ 30 октября 2009

Я хочу найти все элементы в одной коллекции, которые не соответствуют другой коллекции. Коллекции не одного типа, хотя; Я хочу написать лямбда-выражение для указания равенства.

A LINQPad пример того, что я пытаюсь сделать:

void Main()
{
    var employees = new[]
    {
        new Employee { Id = 20, Name = "Bob" },
        new Employee { Id = 10, Name = "Bill" },
        new Employee { Id = 30, Name = "Frank" }
    };

    var managers = new[]
    {
        new Manager { EmployeeId = 20 },
        new Manager { EmployeeId = 30 }
    };

    var nonManagers =
    from employee in employees
    where !(managers.Any(x => x.EmployeeId == employee.Id))
    select employee;

    nonManagers.Dump();

    // Based on cdonner's answer:

    var nonManagers2 =
    from employee in employees
    join manager in managers
        on employee.Id equals manager.EmployeeId
    into tempManagers
    from manager in tempManagers.DefaultIfEmpty()
    where manager == null
    select employee;

    nonManagers2.Dump();

    // Based on Richard Hein's answer:

    var nonManagers3 =
    employees.Except(
        from employee in employees
        join manager in managers
            on employee.Id equals manager.EmployeeId
        select employee);

    nonManagers3.Dump();
}

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Manager
{
    public int EmployeeId { get; set; }
}

Вышеуказанное работает и вернет счет работника (# 10). Это не кажется изящным, хотя, и это может быть неэффективно с большими коллекциями. В SQL я бы, вероятно, сделал LEFT JOIN и нашел элементы, где второй идентификатор был NULL. Как лучше всего это делать в LINQ?

РЕДАКТИРОВАТЬ: Обновлено, чтобы предотвратить решения, которые зависят от Id, равного индексу.

EDIT: добавлено решение cdonner - у кого-нибудь есть что-нибудь попроще?

РЕДАКТИРОВАТЬ: Добавил вариант ответа Ричарда Хейна, мой текущий любимый. Спасибо всем за отличные ответы!

Ответы [ 8 ]

30 голосов
/ 31 октября 2009

Это почти то же самое, что и некоторые другие примеры, но меньше кода:

employees.Except(employees.Join(managers, e => e.Id, m => m.EmployeeId, (e, m) => e));

Это не проще, чем работники. Однако (e =>! Manager.Any (m => m.EmployeeId == e.Id)) или ваш оригинальный синтаксис.

5 голосов
/ 30 октября 2009

         var nonManagers = ( from e1 in employees
                             select e1 ).Except(
                                   from m in managers
                                   from e2 in employees
                                   where m.EmployeeId == e2.Id
                                   select e2 );
5 голосов
/ 30 октября 2009
    /// <summary>
    /// This method returns items in a set that are not in 
    /// another set of a different type
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TOther"></typeparam>
    /// <typeparam name="TKey"></typeparam>
    /// <param name="items"></param>
    /// <param name="other"></param>
    /// <param name="getItemKey"></param>
    /// <param name="getOtherKey"></param>
    /// <returns></returns>
    public static IEnumerable<T> Except<T, TOther, TKey>(
                                           this IEnumerable<T> items,
                                           IEnumerable<TOther> other,
                                           Func<T, TKey> getItemKey,
                                           Func<TOther, TKey> getOtherKey)
    {
        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;
    }

Я не помню, где я нашел этот метод.

4 голосов
/ 08 марта 2012

Уже немного поздно (я знаю).

Я смотрел на ту же проблему и рассматривал HashSet из-за различных подсказок производительности в этом направлении, вкл. @ 1003 * @ Skeet's * Пересечение нескольких списков с IEnumerable.Intersect () - и спросил у меня в офисе, и все согласились с тем, что HashSet будет быстрее и более читабельным:

HashSet<int> managerIds = new HashSet<int>(managers.Select(x => x.EmployeeId));
nonManagers4 = employees.Where(x => !managerIds.Contains(x.Id)).ToList();

Затем мне предложили еще более быстрое решение, использующее собственные массивы для создания решения типа bit-mask-ish (хотя синтаксис запросов к нативному массиву не позволил бы мне их использовать, за исключением крайних соображений производительности).

Чтобы дать этому ответу немного доверия после очень долгого времени, я расширил вашу программу linqpad и данные временем, чтобы вы могли сравнить то, что сейчас шесть вариантов:

void Main()
{
    var employees = new[]
    {
        new Employee { Id = 20, Name = "Bob" },
        new Employee { Id = 10, Name = "Kirk NM" },
        new Employee { Id = 48, Name = "Rick NM" },
        new Employee { Id = 42, Name = "Dick" },
        new Employee { Id = 43, Name = "Harry" },
        new Employee { Id = 44, Name = "Joe" },
        new Employee { Id = 45, Name = "Steve NM" },
        new Employee { Id = 46, Name = "Jim NM" },
        new Employee { Id = 30, Name = "Frank"},
        new Employee { Id = 47, Name = "Dave NM" },
        new Employee { Id = 49, Name = "Alex NM" },
        new Employee { Id = 50, Name = "Phil NM" },
        new Employee { Id = 51, Name = "Ed NM" },
        new Employee { Id = 52, Name = "Ollie NM" },
        new Employee { Id = 41, Name = "Bill" },
        new Employee { Id = 53, Name = "John NM" },
        new Employee { Id = 54, Name = "Simon NM" }
    };

    var managers = new[]
    {
        new Manager { EmployeeId = 20 },
        new Manager { EmployeeId = 30 },
        new Manager { EmployeeId = 41 },
        new Manager { EmployeeId = 42 },
        new Manager { EmployeeId = 43 },
        new Manager { EmployeeId = 44 }
    };

    System.Diagnostics.Stopwatch watch1 = new System.Diagnostics.Stopwatch();

    int max = 1000000;

    watch1.Start();
    List<Employee> nonManagers1 = new List<Employee>();
    foreach (var item in Enumerable.Range(1,max))
    {
        nonManagers1 = (from employee in employees where !(managers.Any(x => x.EmployeeId == employee.Id)) select employee).ToList();

    }
    nonManagers1.Dump();
    watch1.Stop();
    Console.WriteLine("Any: " + watch1.ElapsedMilliseconds);

    watch1.Restart();       
    List<Employee> nonManagers2 = new List<Employee>();
    foreach (var item in Enumerable.Range(1,max))
    {
        nonManagers2 =
        (from employee in employees
        join manager in managers
            on employee.Id equals manager.EmployeeId
        into tempManagers
        from manager in tempManagers.DefaultIfEmpty()
        where manager == null
        select employee).ToList();
    }
    nonManagers2.Dump();
    watch1.Stop();
    Console.WriteLine("temp table: " + watch1.ElapsedMilliseconds);

    watch1.Restart();       
    List<Employee> nonManagers3 = new List<Employee>();
    foreach (var item in Enumerable.Range(1,max))
    {
        nonManagers3 = employees.Except(employees.Join(managers, e => e.Id, m => m.EmployeeId, (e, m) => e)).ToList();
    }
    nonManagers3.Dump();
    watch1.Stop();
    Console.WriteLine("Except: " + watch1.ElapsedMilliseconds);

    watch1.Restart();       
    List<Employee> nonManagers4 = new List<Employee>();
    foreach (var item in Enumerable.Range(1,max))
    {
        HashSet<int> managerIds = new HashSet<int>(managers.Select(x => x.EmployeeId));
        nonManagers4 = employees.Where(x => !managerIds.Contains(x.Id)).ToList();

    }
    nonManagers4.Dump();
    watch1.Stop();
    Console.WriteLine("HashSet: " + watch1.ElapsedMilliseconds);

      watch1.Restart();
      List<Employee> nonManagers5 = new List<Employee>();
      foreach (var item in Enumerable.Range(1, max))
      {
                   bool[] test = new bool[managers.Max(x => x.EmployeeId) + 1];
                   foreach (var manager in managers)
                   {
                        test[manager.EmployeeId] = true;
                   }

                   nonManagers5 = employees.Where(x => x.Id > test.Length - 1 || !test[x.Id]).ToList();


      }
      nonManagers5.Dump();
      watch1.Stop();
      Console.WriteLine("Native array call: " + watch1.ElapsedMilliseconds);

      watch1.Restart();
      List<Employee> nonManagers6 = new List<Employee>();
      foreach (var item in Enumerable.Range(1, max))
      {
                   bool[] test = new bool[managers.Max(x => x.EmployeeId) + 1];
                   foreach (var manager in managers)
                   {
                        test[manager.EmployeeId] = true;
                   }

                   nonManagers6 = employees.Where(x => x.Id > test.Length - 1 || !test[x.Id]).ToList();
      }
      nonManagers6.Dump();
      watch1.Stop();
      Console.WriteLine("Native array call 2: " + watch1.ElapsedMilliseconds);
}

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Manager
{
    public int EmployeeId { get; set; }
}
3 голосов
/ 30 октября 2009
var nonmanagers = employees.Select(e => e.Id)
    .Except(managers.Select(m => m.EmployeeId))
    .Select(id => employees.Single(e => e.Id == id));
2 голосов
/ 30 октября 2009

Посмотрите на функцию Except () LINQ. Он делает именно то, что вам нужно.

1 голос
/ 03 февраля 2014

Лучше, если вы оставите присоединение к элементу и отфильтруете его с нулевым условием

var finalcertificates = (from globCert in resultCertificate
                                         join toExcludeCert in certificatesToExclude
                                             on globCert.CertificateId equals toExcludeCert.CertificateId into certs
                                         from toExcludeCert in certs.DefaultIfEmpty()
                                         where toExcludeCert == null
                                         select globCert).Union(currentCertificate).Distinct().OrderBy(cert => cert.CertificateName);
0 голосов
/ 30 ноября 2015

Менеджеры тоже сотрудники! Таким образом, класс Manager должен быть подклассом из класса Employee (или, если вам это не нравится, тогда они оба должны быть подклассом из родительского класса или создать класс NonManager).

Тогда ваша проблема так же проста, как реализация интерфейса IEquatable на вашем Employee суперклассе (для GetHashCode просто вернуть EmployeeID), а затем использовать этот код:

var nonManagerEmployees = employeeList.Except(managerList);
...