2010-05-26 6 views
19

У меня есть две коллекции a и b. Я хотел бы вычислить набор элементов в a или b, но не в обоих (логический эксклюзив или). С помощью LINQ, я могу придумать с этим:LINQ и разность заданий

IEnumerable<T> Delta<T>(IEnumerable<T> a, IEnumerable<T> b) 
{ 
    return a.Except (b).Union (b.Except (a)); 
} 

Интересно, если есть другие, более эффективные и более компактные способы получения разницы между этими двумя коллекциями.

Редактировать 1: Jon Skeet опубликовал первое решение, которое не сохраняет порядок позиций, опираясь на HashSet. Интересно, есть ли другие подходы, которые сохраняли бы порядок a и b на выходе.

+0

Что делать, если a или b содержат дубликаты? –

+0

В моем случае 'a' и' b' не содержат дубликатов, поэтому для меня это не проблема. –

ответ

24

Использование HashSet<T> непосредственно - он имеет SymmetricExceptWith метод:

HashSet<T> data = new HashSet<T>(a); 
data.SymmetricExceptWith(b); 

EDIT: Если вы хотите, чтобы поддерживать порядок, вот альтернатива:

HashSet<T> data = new HashSet<T>(a); 
data.IntersectWith(b); 
foreach (T t in a.Concat(b)) 
{ 
    if (!data.Contains(t)) 
    { 
     yield return t; 
    } 
} 

Это имеет следующие важные отличия:

  • И a и b повторяются дважды. В некоторых случаях это может быть очень плохо - вы можете позвонить ToList по каждому из них, чтобы начать с сохранения буфера.
  • Если есть дубликаты в a или b, они будут выдаваться несколько раз. Если вы хотите избежать этого, вы можете сохранить набор уже полученных значений. На данный момент, это было бы эквивалентно:

    a.Concat(b).Except(a.Intersect(b)) 
    

Это еще только два набор операций вместо трех в исходном коде, хотя.

+0

Спасибо, Джон за ваш быстрый ответ. HashSet работает нормально, пока вас не интересует первоначальный порядок элементов. Что делать, если я хочу сохранить порядок элементов в 'a' и' b' в разнице? –

+0

@Pierre: Я отредактировал свой ответ с еще несколькими вариантами. –

+0

Большое спасибо за ваше время. В моем случае 'a' и' b' не содержат дубликатов, поэтому это не проблема. Выражение LINQ, которое вы предлагаете, гораздо читаемо (и, следовательно, поддерживается), чем фрагмент кода с использованием 'HashSet'. Мне это нравится! –

3

Учитывая a.Except (b) и b.Except (a) не пересекаются, вы можете использовать concat вместо union, сохраняя оператор набора (и concat является более эффективным).

return a.Except (b).Concat (b.Except (a)); 

Это все еще проходит через каждый список дважды.

+0

Спасибо; вы правы, 'Concat' будет быстрее, чем' Union', так как мои входы не пересекаются; Я не обратил на это внимания. –

0

Мы имели аналогичную потребность проекта в моей компании, поэтому мы написали это расширение:

public class EnumerablePair<T> : IReadOnlyCollection<T> 
{ 
    private IReadOnlyCollection<T> _Left; 
    private IReadOnlyCollection<T> _Right; 
    private IEnumerable<T> _Union; 
    private int _Count; 
    public EnumerablePair(IEnumerable<T> left, IEnumerable<T> right) 
    { 
     _Left = left?.ToList() ?? Enumerable.Empty<T>().ToList(); 
     _Right = right?.ToList() ?? Enumerable.Empty<T>().ToList(); 
     _Count = Left.Count + Right.Count; 
     _Union = Left.Union(Right); 
    } 

    public int Count => _Count; 
    public IReadOnlyCollection<T> Left { get => _Left; } 
    public IReadOnlyCollection<T> Right { get => _Right; } 

    public IEnumerator<T> GetEnumerator() 
    { 
     return _Union.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return _Union.GetEnumerator(); 
    } 
} 

public static class EnumerableExtension 
{ 
    public static EnumerablePair<T> ExclusiveDisjunction<T>(this IEnumerable<T> leftOperand, IEnumerable<T> rightOperand, IEqualityComparer<T> comparer = null) 
    { 
     if (leftOperand == null) 
      throw new ArgumentNullException(nameof(leftOperand), $"{nameof(leftOperand)} is null."); 
     if (rightOperand == null) 
      throw new ArgumentNullException(nameof(rightOperand), $"{nameof(rightOperand)} is null."); 

     // TODO : Can be optimized if one of the IEnumerable parameters is empty. 

     bool leftIsBigger = leftOperand.Count() > rightOperand.Count(); 
     var biggestOperand = leftIsBigger ? leftOperand.ToList() : rightOperand.ToList(); 
     var smallestOperand = leftIsBigger ? rightOperand.ToList() : leftOperand.ToList(); 

     var except1 = biggestOperand.ToList(); 
     var except2 = Enumerable.Empty<T>().ToList(); 

     Func<T, T, bool> areEquals; 
     if (comparer != null) 
      areEquals = (one, theOther) => comparer.Equals(one, theOther); 
     else 
      areEquals = (one, theOther) => one?.Equals(theOther) ?? theOther == null; 

     foreach (T t in smallestOperand) 
      if (except1.RemoveAll(item => areEquals(item, t)) == 0) 
       except2.Add(t); 

     if (leftIsBigger) 
      return new EnumerablePair<T>(except1, except2); 
     return new EnumerablePair<T>(except2, except1); 
    } 
} 

Он сравнивает элементы двух коллекций (с помощью IEqualityComparer или нет, по вашему выбору).

  • Возвращаемый объект, EnumerablePair<T>, содержит объекты, которые находятся в leftOperand или rightOperand, но не оба (XOR).
  • EnumerablePair<T>.Left содержит объекты, которые находятся в leftOperand, но не в rightOperand.
  • EnumerablePair<T>.Right содержит объекты, которые находятся в rightOperand, но не в leftOperand.

Вы можете использовать расширение, как это:

var xorList = list1.ExclusiveDisjunction(list2); 
var leftXor = xorList.Left; 
var rightXor = xorList.Right; 

xorList, leftXor и rightXor являются IEnumerable<T>.