使用公共属性联合两个不同类型的列表[重复]
Posted
技术标签:
【中文标题】使用公共属性联合两个不同类型的列表[重复]【英文标题】:Union two Lists of different types using a common property [duplicate] 【发布时间】:2021-08-27 13:05:20 【问题描述】:我有两个具有共同属性的不同对象列表,我正在尝试将它们合并为一个对象,
我有
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 都可以为空。
【问题讨论】:
输入是否在内存中/是否存储在查找中?您可以先合并 ID,然后遍历它们,查找每个部分,然后形成结果对象吗?您是否保证给定 ID 的每种对象类型最多只有一个,或者您是否也需要联合字典? 合并 customerIds,然后遍历它们并进行查找工作!这样做会不会出现严重的性能问题? 您需要测试性能,它可能会更快,因为您只需分配一次结果而无需更新它。 【参考方案1】:这个怎么样:
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;
请注意,此代码尚未经过测试。
【讨论】:
【参考方案2】: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 );
如果它们存在于任一输入源中,这也为您提供了处理重复项的机会。
【讨论】:
【参考方案3】:我不能直接进行内部连接,因为 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();
这只会在左侧查找表上进行一次查找,在右侧查找表上进行一次查找。
当然,要构造第一个返回值,我还需要做很多工作:
创建两个查找表。这意味着两个输入序列都被枚举一次。 两个查找表都枚举一次以获取所有键 所有键都被枚举一次以删除重复项所以要创建第一个结果项,需要做很多工作。幸运的是,对于每个下一个结果项,我不需要在左侧或右侧进行任何枚举。我只需要做两次查找,这和在字典中查找一样快。
【讨论】:
以上是关于使用公共属性联合两个不同类型的列表[重复]的主要内容,如果未能解决你的问题,请参考以下文章