Подтвердить что ты не робот

Использование LINQ to Objects для поиска элементов в одной коллекции, которые не соответствуют другим

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

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 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 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?

EDIT: Обновлено для предотвращения решений, которые зависят от Id, равного индексу.

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

EDIT: Добавлен вариант ответа Ричарда Хейна, моего нынешнего фаворита. Спасибо всем за отличные ответы!

4b9b3361

Ответ 1

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

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

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

Ответ 2

    /// <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;
    }

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

Ответ 3

       var nonManagers = (от e1 у сотрудников                            выберите e1). Кроме (                                  от м в менеджерах                                  от e2 у сотрудников                                  где m.EmployeeId == e2.Id                                  выберите e2);

Ответ 4

var nonmanagers = employees.Select(e => e.Id)
    .Except(managers.Select(m => m.EmployeeId))
    .Select(id => employees.Single(e => e.Id == id));

Ответ 5

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

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

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

Тогда мне предложили еще более быстрое решение, используя собственные массивы, чтобы создать решение типа бит-иш (синтаксис запросов на массив массива меня заставил бы использовать их, кроме экстремальных причин производительности).

Чтобы дать этот ответ немного доверия после очень долгого времени, я расширил вашу программу и данные 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; }
}

Ответ 6

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

Ответ 7

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

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);

Ответ 8

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

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

var nonManagerEmployees = employeeList.Except(managerList);