我不能直接进行内部连接,因为 customerId 可以在一个列表中具有值,但在另一个列表中没有
你是对的。你需要的是一个full outer join:左边所有没有右边的元素,右边所有没有左边的元素以及左右两边的所有元素。
我可以给你一个只适合这个问题的解决方案,如果我提出一个可重用的解决方案,编程会更有趣。
我将为完全外连接创建一个扩展方法,用于具有属性选择器的两个不同序列,类似于内连接。如果您不熟悉扩展方法,请访问Extension Methods Demystified
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 右侧加入。当然,即使键的 names 不必相同,但两个属性的 type 也必须相同,否则不能比较是否相等。
好吧,如果这有点你想要的,让我们实现扩展方法!
实现
您没有说每个 CustomerId 都是唯一的。在您的情况下,它可能是,但如果您想在例如 Customer.City 上执行 FullOuterJoin,则密钥可能不是唯一的。
首先我们创建两个LookupTables:从Left 中的所有元素中,我们使用leftKeySelector 中的Tkey 作为键创建一个LookupTable。从右边的所有元素中,我们用 rightKeySelector 中的 Tkey 作为键创建一个 LookupTable。
然后我们从两个查找表中获取所有使用的键。我们需要一个 Distinct 来删除左右两边的重复键。
然后我们枚举所有这些唯一键。我们在左侧查找和右侧查找中进行搜索。
注意:如果Left序列中没有用到某个key,那么搜索会返回一个空集合,也就是说left没有这个key的元素。这样做的好处是我们确定不用担心关于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();
这只会在左侧查找表上进行一次查找,在右侧查找表上进行一次查找。
当然,要构造第一个返回值,我还需要做很多工作:
- 创建两个查找表。这意味着两个输入序列都被枚举一次。
- 两个查找表都枚举一次以获取所有键
- 所有键都被枚举一次以删除重复项
所以要创建第一个结果项,需要做很多工作。幸运的是,对于每个下一个结果项,我不需要在左侧或右侧进行任何枚举。我只需要做两次查找,这和在字典中查找一样快。