我有以下情况:
class A
{
public A(string name, int age) { Name = name; Age = age; }
public string Name;
public int Age;
}
List<A> one =
new List<A>() { new A("bob", 15), new A("john", 10), new A("mary", 12) };
List<A> two =
new List<A>() { new A("bob", 15), new A("mary", 15), new A("cindy", 18) };
我想在这些列表之间做差异,并获取john仅在列表1中的信息,cindy仅在列表2中,并且mary在两个列表中但是它不是完全匹配(年龄不同) )。我的目标是以并排比较的方式提供这些信息。
有人可以建议如何有效地做到这一点(而不是做三次通过,一次是针对第一次不存在的东西,另一种是第二次不存在的东西,第三种是存在于两者中的东西但是不同)
如果我错过任何重复的问题,我很抱歉,我能找到的只能处理布尔结果,而不是实际细节。
答案 0 :(得分:2)
var result =
one.Select(a => Tuple.Create(a, "one")) // tag list one items
.Concat(two.Select(a => Tuple.Create(a, "two"))) // tag list two items
.GroupBy(t => t.Item1.Name) // group items by Name
.ToList(); // cache result
var list_one_only = result.Where(g => g.Count() == 1 && g.First().Item2 == "one");
var list_two_only = result.Where(g => g.Count() == 1 && g.First().Item2 == "two");
var both_list_diff = result.Where(g => g.Count() == 2 && g.First().Item1.Age != g.Skip(1).First().Item1.Age);
这将返回一个列表列表,其中每个内部列表将是1个项目(具有原始项目的元组以及它来自哪个列表)或者将有2个项目(相同的名称,可能相同)年龄,以及哪个年龄从哪个名单开始)。
我不确定你想要结果的结构是什么,所以我把它留在那里。否则从这里你可以做另一个选择来过滤掉两个列表中的相同记录(&#34; bob&#34;)等。
此解决方案应该只遍历两个列表一次。
答案 1 :(得分:1)
传球将是隐含的或“明确的”#34;。而通过显式我的意思是通过一些Linq扩展方法隐藏。所以你可以做到以下几点:
var results = (from item in one.Concat(two).Select(x => x.Name).Distinct()
let inFirst = one.Find(x => x.Name == item)
let inSecond = two.Find(x => x.Name == item)
let location = inFirst != null
&& inSecond != null
? 2 : inSecond != null ? 1 : 0
select new
{
Name = item,
location = location == 0 ? "First" : location == 1 ? "Second" : "Both",
ExactMatch = (location != 2 || inFirst.Age == inSecond.Age)
? "YES" : $"One: { inFirst.Age } | Two: { inSecond.Age }"
}).ToList();
结果:
{ Name = bob, location = Both, ExactMatch = YES }
{ Name = john, location = First, ExactMatch = YES }
{ Name = mary, location = Both, ExactMatch = One: 12 | Two: 15 }
{ Name = cindy, location = Second, ExactMatch = YES }
如果您担心性能问题,请使用高效的数据结构进行查找O(1)。以下33ms
项目列表的10000
完成,而上述内容在5000ms
左右结束:
var oneLookup = one.ToLookup(x => x.Name, x => x);
var twoLookup = two.ToLookup(x => x.Name, x => x);
var results = (from item in one.Concat(two).Select(x => x.Name).Distinct()
let inFirst = oneLookup[item].FirstOrDefault()
let inSecond = twoLookup[item].FirstOrDefault()
let location = inFirst != null
&& inSecond != null
? 2 : inSecond != null ? 1 : 0
select new
{
Name = item,
location = location == 0 ? "First" : location == 1 ? "Second" : "Both",
ExactMatch = (location != 2 || inFirst.Age == inSecond.Age)
? "YES" : $"One: { inFirst.Age } | Two: { inSecond.Age }"
}).ToList();
答案 2 :(得分:1)
有人可以建议如何有效地做到这一点(而不是做三次通过,一次是针对先不存在的东西,另一种针对第二种不存在的东西,第三种是东西存在于两者中但不同)
在一次传递中产生差异的唯一方法是,如果两个序列按身份键(在您的情况下为Name)排序。但是,排序会带来额外的成本,并且该过程也无法在LINQ中编码。
你真正需要的是full outer join
,它没有自然的LINQ支持。因此,经典方法需要两次传递 - left other join
用于存在于一个中并最终存在于第二个中的内容,而right antijoin
用于仅存在于第二个中的内容。这是迄今为止最有效的LINQ方式。
查询可能是这样的:
var result =
(from e1 in one
join e2 in two on e1.Name equals e2.Name into match
from e2 in match.DefaultIfEmpty()
select new
{
e1.Name,
In = e2 == null ? "A" : "A,B",
Age = e2 == null || e1.Age == e2.Age ? e1.Age.ToString() : $"A:{e1.Age} B:{e2.Age}"
})
.Concat
(from e2 in two
join e1 in one on e2.Name equals e1.Name into match
where !match.Any()
select new { e2.Name, In = "B", Age = e2.Age.ToString() })
.ToList();
从您的示例数据中生成以下内容:
{ Name = bob, In = A,B, Age = 15 }
{ Name = john, In = A, Age = 10 }
{ Name = mary, In = A,B, Age = A:12 B:15 }
{ Name = cindy, In = B, Age = 18 }
当然你可以输出你想要的任何东西。如您所见,您需要考虑具有两个匹配元素的唯一位置是查询的第一部分。