【问题标题】: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,然后遍历它们并进行查找工作!这样做会不会出现严重的性能问题?
  • 您需要测试性能,它可能会更快,因为您只需分配一次结果而无需更新它。

标签: c# linq


【解决方案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();
      

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

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2022-12-18
        • 2020-07-01
        • 1970-01-01
        • 2014-02-12
        • 2011-11-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多