使用一个公共属性合并两个不同类型的列表



我有两个不同的对象列表,它们有一个共同的属性,我正试图将它们合并为一个对象,

我有

public class CustomerMRMetric
{
public int CustomerId { get; set; }
public Dictionary<int, decimal> MRMetrics { get; set; }
}
public class CustomerLRMetric
{
public int CustomerId { get; set; }
public Dictionary<int, string> LRMetrics { get; set; }
}

我得到了这些对象的两个单独列表,我应该怎么做才能得到以下输出?

public class CustomerMetrics
{
public Dictionary<int, decimal> MRMetrics { get; set; }
public int CustomerId { get; set; }
public Dictionary<int, string> LRMetrics { get; set; }
}

我不能直接进行内部联接,因为customerId可以在一个列表中有值,但在另一个列表不可以,而且两者都有,这意味着MRMetrics或LRMetrics都可以为null。

这个怎么样:

void Main()
{
var list1 = new List<CustomerMRMetric>();
var list2 = new List<CustomerLRMetric>();
var left = list1.Select(l1 => new CustomerMetrics
{
CustomerId = l1.CustomerId,
MRMetrics = l1.MRMetrics,
LRMetrics = list2.FirstOrDefault(l2 => l2.CustomerId == l1.CustomerId)?.LRMetrics
});
var right = list2.Select(l2 => new CustomerMetrics
{
CustomerId = l2.CustomerId,
LRMetrics = l2.LRMetrics,
MRMetrics = list1.FirstOrDefault(l1 => l1.CustomerId == l2.CustomerId)?.MRMetrics
});
var inner = list1.Join(list2, l1 => l1.CustomerId, l2 => l2.CustomerId, (l1, l2) => new CustomerMetrics
{
CustomerId = l1.CustomerId,
MRMetrics = l1.MRMetrics,
LRMetrics = l2.LRMetrics
});
var union = left.Concat(inner).Concat(right);
}
public class CustomerMRMetric
{
public int CustomerId { get; set; }
public Dictionary<int, decimal> MRMetrics { get; set; }
}
public class CustomerLRMetric
{
public int CustomerId { get; set; }
public Dictionary<int, string> LRMetrics { get; set; }
}
public class CustomerMetrics
{
public Dictionary<int, decimal> MRMetrics { get; set; }
public int CustomerId { get; set; }
public Dictionary<int, string> LRMetrics { get; set; }
}

请注意,此代码尚未经过测试。

ConcurrentDictionary包含一个有用的AddOrUpdate方法,您可以像这样使用:

ConcurrentDictionary<int, CustomerMetrics> combined = 
new ConcurrentDictionary<int, CustomerMetrics>();

List<CustomerMRMetric> source1 = new List<CustomerMRMetric>();
List<CustomerLRMetric> source2 = new List<CustomerLRMetric>();
foreach (var s1 in source1) 
combined.AddOrUpdate(s1.CustomerId, 
key => new CustomerMetrics(){ CustomerId = key, MRMetrics = s1.MRMetrics },
(key, v) => new CustomerMetrics() { CustomerId = key, MRMetrics = s1.MRMetrics });
foreach (var s2 in source2) 
combined.AddOrUpdate(s2.CustomerId, 
key => new CustomerMetrics(){ CustomerId = key, LRMetrics = s2.LRMetrics },
(key, v) => new CustomerMetrics() { CustomerId = key, 
MRMetrics = v.MRMetrics, LRMetrics = s2.LRMetrics });

这也为您提供了处理重复项的机会,如果它们存在于任一输入源中。

我不能直接进行内部联接,因为customerId可以在一个列表中有值,但不能在另一个中有值

你说得对。你需要的是一个full outer join:所有来自左边没有右边的元素,所有来自右边没有左边的元素,以及所有在左边和右边的元素。

我可以给你一个只适合这个问题的解决方案,如果我制作一个可重复使用的解决方案的话,编程会更有趣。

我将为具有属性选择器的两个不同序列制作一个完整外部联接的扩展方法,类似于内部联接。如果您不熟悉扩展方法,请访问扩展方法解密

public IEnumerable<TResult> FullOuterJoin<T1, T2, TKey, TResult>(
this IEnumerable<T1> leftSequence,
IEnumerable<T2> rightSequence,
Func<T1, TKey> leftKeySelector,
Func<T2, TKey> rightKeySelector,
Func<TKey, IEnumerable<T1>, IEnumerable<T2>, TResult> resultSelector)
{
return FullOuterJoin(leftSequence, rightSequence,
leftKeySelector, rightKeySelector,
resultSelector, null);
}
public IEnumerable<TResult> FullOuterJoin<T1, T2, TKey, TResult>(
this IEnumerable<T1> leftSequence,
IEnumerable<T2> rightSequence,
Func<T1, TKey> leftKeySelector,
Func<T2, TKey> rightKeySelector,
Func<TKey, IEnumerable<T1>, IEnumerable<T2>, TResult> resultSelector,
IEqualityComparer<TKey> comparer)
{
// TODO: check inputs not null
if (comparer == null) comparer = EqualityComparer<TKey>.Default;
// TODO: implement
}

用途为:

IEnumerable<CustomerMRMetric> customerMRMetrics = ...
IEnumerable<CustomerLRMetric> customerLRMetrics = ...
IEnumerable<CustomerMetric> customerMetrics = customerMRMetrics.LeftOuterJoin(
customerLRMetrics,
mrMetric => mrMetric.CustomerId,   // from every mrMetric take the CustomerId
lrMetric => lrMetric.CustomerId,   // from every lrMetric take the CustomerId
// parameter resultSelector: from every used CustomerIds,
// and all zero or more mrMetrics with this CustomerId,
// and all zero or more lrMetrics with this CustomerId,
// construct one new CustomerMetric:
(customerId, mrMetricsWithThisCustomerId, lrMetricsWithThisCustomerId) => new CustomerMetric
{
CustomerId = customerId,
MrMetrics = mrMetricsWithThisCustomerId.MrMetrics,
LRMetrics = lrMetricsWithThisCustomerId.LrMetrics,
});

注意:就像在完全内部联接中一样,您联接的项不必是相同的属性。例如,如果您想为在生日当天订购的客户提供服务,您可以在Customer.BirthDate上加入左侧,在Order.OrderDate上加入右侧。当然,即使键的名称不必相同,两个属性的type也必须相同,否则无法进行相等性比较。

好吧,如果这是你想要的,让我们实现扩展方法吧!

实施

您并没有说每个CustomerId都是唯一的。在您的情况下,可能会这样,但如果您想在例如Customer.City上执行FullOuterJoin,则密钥可能不是唯一的。

首先,我们制作两个LookupTables:从Left中的所有元素中,我们制作一个LookupTable,并将leftKeySelector中的Tkey作为键。从右边的所有元素中,我们用rightKeySelector中的Tkey作为键创建一个LookupTable。

然后我们从两个查找表中获得所有使用过的键。我们需要一个Distinct来删除左键和右键中的重复键。

然后我们列举所有这些唯一的密钥。我们在左查找和右查找中进行搜索。

注意:如果Left序列中没有使用键,那么搜索将返回一个空集合,这意味着Left没有包含此键的元素。这样做的优点是,我们确信不必担心NULL。当然,对权利也是如此。如果键同时位于左侧和右侧,则两次搜索都将返回非空序列。

完全外部联接的实现:

// TODO: check inputs not null
if (comparer == null) comparer = EqualityComparer<TKey>.Default;
var leftLookup = leftSequence.ToLookup(leftKeySelector, comparer);
var rightLookup = rightSequence.ToLookup(rightKeySelector, comparer);
var leftKeys = leftLookup.Select(left => left.Key);
var rightKeys = rightLookup.Select(right => right.Key);
var allUsedKeys = leftKeys.Concat(rightKeys).Distinct(comparer);
// enumerate all keys, fetch all items with this key from left, do the same from right
foreach (TKey key in allUsedKeys)
{
IEnumerable<T1> leftItemsWithThisKey = leftLookup[key];
IEnumerable<T2> rightItemsWithThisKey = rightLookup[key];
TResult result = resultSelector(key, leftItemsWithThisKey, rightItemsWithThisKey);
yield return result;
}

当然,你可以把几个语句放在一个语句中。这不会大大加快进程。然而,它会降低可读性。

因为我使用yield return,所以该方法使用延迟执行,就像大多数LINQ方法一样:只需执行所需的查找即可。

var result = customerMRMetrics.LeftOuterJoin(customerLRMetrics, ...)
.FirstOrDefault();

这将只在左侧查找表上执行一次查找,在右侧查找表上进行一次查找。

当然,要构建第一个返回值,我需要做很多工作:

  • 创建两个查找表。这意味着两个输入序列都被枚举一次
  • 两个查找表都枚举一次以获取所有键
  • 所有键都枚举一次以删除重复项

因此,要创建第一个结果项,需要做大量工作。幸运的是,对于下一个结果项,我不需要在左侧或右侧进行任何枚举。我只需要做两次查找,这和在字典中查找一样快。

最新更新