linqを使用して2つのオブジェクトリストからリストを作成する


161

次のような状況です

class Person
{
    string Name;
    int Value;
    int Change;
}

List<Person> list1;
List<Person> list2;

2つのリストを新しいものList<Person> に結合する必要があります。それは、結合レコードがその名前を持っている同じ人である場合、list2の人の値、changeはlist2の値-list1の値です。重複がない場合、変更は0です。


2
linqは本当に必要ですか-linqっぽい表現を少し含む素敵なforeachも同様にできます。
Rashack 2009

1
このコメントを質問のタイトルのバージョンとして追加すると、実際の質問は一致しませんでした。これに対する本当の答えはMikeからのこの回答です。他のほとんどの回答は有用ですが、元の投稿者が提示した問題を実際には解決しません。
ジョシュア

回答:


254

これは、Linq拡張メソッドUnionを使用して簡単に行うことができます。例えば:

var mergedList = list1.Union(list2).ToList();

これは、2つのリストがマージされ、doubleが削除されたリストを返します。私の例のように、Union拡張メソッドで比較子を指定しない場合、PersonクラスのデフォルトのEqualsメソッドとGetHashCodeメソッドが使用されます。たとえば、Nameプロパティを比較して人物を比較する場合は、これらのメソッドをオーバーライドして、自分で比較を実行する必要があります。次のコードサンプルを確認してください。このコードをPersonクラスに追加する必要があります。

/// <summary>
/// Checks if the provided object is equal to the current Person
/// </summary>
/// <param name="obj">Object to compare to the current Person</param>
/// <returns>True if equal, false if not</returns>
public override bool Equals(object obj)
{        
    // Try to cast the object to compare to to be a Person
    var person = obj as Person;

    return Equals(person);
}

/// <summary>
/// Returns an identifier for this instance
/// </summary>
public override int GetHashCode()
{
    return Name.GetHashCode();
}

/// <summary>
/// Checks if the provided Person is equal to the current Person
/// </summary>
/// <param name="personToCompareTo">Person to compare to the current person</param>
/// <returns>True if equal, false if not</returns>
public bool Equals(Person personToCompareTo)
{
    // Check if person is being compared to a non person. In that case always return false.
    if (personToCompareTo == null) return false;

    // If the person to compare to does not have a Name assigned yet, we can't define if it's the same. Return false.
    if (string.IsNullOrEmpty(personToCompareTo.Name) return false;

    // Check if both person objects contain the same Name. In that case they're assumed equal.
    return Name.Equals(personToCompareTo.Name);
}

PersonクラスのデフォルトのEqualsメソッドが常にNameを使用して2つのオブジェクトを比較するように設定したくない場合は、IEqualityComparerインターフェイスを使用する比較クラスを作成することもできます。次に、この比較演算子をLinq拡張ユニオンメソッドの2番目のパラメーターとして指定できます。このような比較メソッドの作成方法の詳細については、http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspxを参照してください。


10
これが値のマージについての質問にどのように答えるかはわかりません。
Wagner da Silva

1
これは応答しません
。Union

7
@ J4Nあなたはおそらく混乱UnionしていIntersectますか?
コス

11
参考までに、Concat重複をマージしないものもあります
Kos

7
この回答を編集して、実際に質問に回答してもよろしいですか?タイトルと基本的なGoogleクエリ( "linqマージリスト")に答えるだけで、質問に答えないという事実にもかかわらず、回答が非常に投票されるのはばかげています。
ローリング、2014

78

私はこの質問が2年後に回答済みとマークされていないことに気付きました-最も近い回答はリチャーズだと思いますが、これはかなり簡略化できます:

list1.Concat(list2)
    .ToLookup(p => p.Name)
    .Select(g => g.Aggregate((p1, p2) => new Person 
    {
        Name = p1.Name,
        Value = p1.Value, 
        Change = p2.Value - p1.Value 
    }));

ただし、どちらのセットでも名前が重複している場合はエラーになりません。

他のいくつかの回答は、結合を使用することを提案しています-これは、結合を行わずに、明確なリストを取得するだけなので、間違いなく進むべき方法ではありません。


8
この投稿は実際に質問に回答し、うまくいきます。
philu

3
これは受け入れられる答えになるはずです。尋ねられた質問に答えない回答への賛成票が多い質問を見たことがない!
トッドメニア2017年

素敵な答え。私はそれに小さな変更を1つ加える場合があります。そのため、Valueは実際にはlist2からの値であり、重複がある場合にChangeが維持されるようにします。
Ravi Desai

70

なぜ使用しないのか Concatですか?

Concatはlinqの一部であり、 AddRange()

あなたの場合:

List<Person> list1 = ...
List<Person> list2 = ...
List<Person> total = list1.Concat(list2);

13
それがより効率的であるとどうして分かりますか?
ジェリーニクソン

@ジェリーニクソン彼/彼女はそれをテストしなかったが、説明は論理的であるようです。stackoverflow.com/questions/1337699/...
Nullius

9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange- > Gregのコメント:Actually, due to deferred execution, using Concat would likely be faster because it avoids object allocation - Concat doesn't copy anything, it just creates links between the lists so when enumerating and you reach the end of one it transparently takes you to the start of the next! これが私のポイントです。
J4N 2013

2
また、Entity Frameworkを使用すると、C#側ではなくSQL側でこれを実行できるという利点もあります。
J4N 2013

4
これが役に立たない本当の理由は、両方のリストに存在するオブジェクトを実際にはマージしないためです。
Mike Goatly、2015

15

これはLinqです

var mergedList = list1.Union(list2).ToList();

これは通常です(AddRange)

var mergedList=new List<Person>();
mergeList.AddRange(list1);
mergeList.AddRange(list2);

これは正常です(Foreach)

var mergedList=new List<Person>();

foreach(var item in list1)
{
    mergedList.Add(item);
}
foreach(var item in list2)
{
     mergedList.Add(item);
}

これは正常です(Foreach-Dublice)

var mergedList=new List<Person>();

foreach(var item in list1)
{
    mergedList.Add(item);
}
foreach(var item in list2)
{
   if(!mergedList.Contains(item))
   {
     mergedList.Add(item);
   }
}

12

これを行うにはいくつかの方法があります。各リストに重複が含まれておらず、名前は一意の識別子であり、どちらのリストも順序付けられていない場合です。

最初に、追加の拡張メソッドを作成して、単一のリストを取得します。

static class Ext {
  public static IEnumerable<T> Append(this IEnumerable<T> source,
                                      IEnumerable<T> second) {
    foreach (T t in source) { yield return t; }
    foreach (T t in second) { yield return t; }
  }
}

したがって、単一のリストを取得できます。

var oneList = list1.Append(list2);

次に名前でグループ化

var grouped = oneList.Group(p => p.Name);

次に、ヘルパーを使用して各グループを処理し、一度に1つのグループを処理できます。

public Person MergePersonGroup(IGrouping<string, Person> pGroup) {
  var l = pGroup.ToList(); // Avoid multiple enumeration.
  var first = l.First();
  var result = new Person {
    Name = first.Name,
    Value = first.Value
  };
  if (l.Count() == 1) {
    return result;
  } else if (l.Count() == 2) {
    result.Change = first.Value - l.Last().Value;
    return result;
  } else {
    throw new ApplicationException("Too many " + result.Name);
  }
}

次の各要素に適用できますgrouped

var finalResult = grouped.Select(g => MergePersonGroup(g));

(警告:テストされていません。)


2
あなたAppendは、標準のほぼ正確な複製ですConcat
ローリング、

@Rawling:なんらかの理由で欠けEnumerable.Concatていたため、再実装しました。
リチャード

2

完全外部結合のようなものが必要です。System.Linq.Enumerableには完全外部結合を実装するメソッドがないため、自分で行う必要があります。

var dict1 = list1.ToDictionary(l1 => l1.Name);
var dict2 = list2.ToDictionary(l2 => l2.Name);
    //get the full list of names.
var names = dict1.Keys.Union(dict2.Keys).ToList();
    //produce results
var result = names
.Select( name =>
{
  Person p1 = dict1.ContainsKey(name) ? dict1[name] : null;
  Person p2 = dict2.ContainsKey(name) ? dict2[name] : null;
      //left only
  if (p2 == null)
  {
    p1.Change = 0;
    return p1;
  }
      //right only
  if (p1 == null)
  {
    p2.Change = 0;
    return p2;
  }
      //both
  p2.Change = p2.Value - p1.Value;
  return p2;
}).ToList();

2

次のコードはあなたの問題に役立ちますか?リストの結合を行うために内部にlinqを少し入れたforeachを使用し、名前が一致する場合、人々は等しいと想定し、実行時に期待値を出力するようです。Resharperはforeachをlinqに変換するための提案を提供していません。そのため、これはおそらく、この方法で行うのと同じくらい良いことです。

public class Person
{
   public string Name { get; set; }
   public int Value { get; set; }
   public int Change { get; set; }

   public Person(string name, int value)
   {
      Name = name;
      Value = value;
      Change = 0;
   }
}


class Program
{
   static void Main(string[] args)
   {
      List<Person> list1 = new List<Person>
                              {
                                 new Person("a", 1),
                                 new Person("b", 2),
                                 new Person("c", 3),
                                 new Person("d", 4)
                              };
      List<Person> list2 = new List<Person>
                              {
                                 new Person("a", 4),
                                 new Person("b", 5),
                                 new Person("e", 6),
                                 new Person("f", 7)
                              };

      List<Person> list3 = list2.ToList();

      foreach (var person in list1)
      {
         var existingPerson = list3.FirstOrDefault(x => x.Name == person.Name);
         if (existingPerson != null)
         {
            existingPerson.Change = existingPerson.Value - person.Value;
         }
         else
         {
            list3.Add(person);
         }
      }

      foreach (var person in list3)
      {
         Console.WriteLine("{0} {1} {2} ", person.Name,person.Value,person.Change);
      }
      Console.Read();
   }
}

1
public void Linq95()
{
    List<Customer> customers = GetCustomerList();
    List<Product> products = GetProductList();

    var customerNames =
        from c in customers
        select c.CompanyName;
    var productNames =
        from p in products
        select p.ProductName;

    var allNames = customerNames.Concat(productNames);

    Console.WriteLine("Customer and product names:");
    foreach (var n in allNames)
    {
        Console.WriteLine(n);
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.