使用公共属性联合两个不同类型的列表[重复]

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();

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

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

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

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

【讨论】:

以上是关于使用公共属性联合两个不同类型的列表[重复]的主要内容,如果未能解决你的问题,请参考以下文章

公共属性列表及其值[重复]

将打字稿接口属性类型转换为联合[重复]

两个表中具有多个公共属性的自然连接[重复]

公共访问器与类的公共属性[重复]

Java从其他两个具有不同对象和公共属性的列表构建一个列表

属性与公共成员变量[重复]