【问题标题】:Merge or zip different list of objects having different structures into single list将具有不同结构的不同对象列表合并或压缩到单个列表中
【发布时间】:2020-09-06 06:24:52
【问题描述】:

大家好,我有如下三个这样的课程

class A 
{
    public int number{get; set;}
    public string name{get; set;}
}
class B
{
   public string casted{get; set;}
}
class C
{
    public int Id{get; set;}
    public bool isSelect {get; set;}
}

我有这样的数据格式

var aObject = new A () { number = 11, name = "John" };
var aObject2= new A () { number = 22, name = "Steve" }; 

IList<A> ListA= new List<A>(){ aObject,aObject2 };

var bObject1 = new B () { casted = "test" };
var bObject2 = new B () { casted = "test1" };
var bObject3 = new B () { casted = "test2" };

IList<B> ListB = new List<B>(){ bObject1 , bObject2 ,bObject3 };

var cObject = new C() { Id = "1", isSelect = true };
var cObject2 = new C(){ Id = "2", isSelect = false };

IList<C> ListC = new List<C>() { cObject ,cObject2 };

所有的结构都不同,我将在上述类列表中获取数据,如List&lt;A&gt;, List&lt;B&gt; and List&lt;C&gt;

我希望在下面形成一种数据结构,将这三个列表合并为一个,如下所示

我正在寻找的结果列表如下所示。

结果

Number    name    casted    Id    isSelect
 11       john    test      1     true
 22       Steve   test1     2     false
  -        -      test2     -       -

有什么方法可以实现这种结果对象,我知道如果你有相同的结构,我们可以连接列表,但这里我有不同的结构。

任何人都可以告诉我有关如何实现这一目标的任何想法,非常感谢我提前非常感谢

【问题讨论】:

  • 什么逻辑决定这一行是有效的:1 john test 1 true 而不是这个2 Steve test 2 false?只是按订单吗?
  • 它只是按顺序,在每个列表中我们需要遍历顺序并连接项目
  • 看来,你需要类似联合类型的东西,它将添加到future versions of C#

标签: c# .net list linq generics


【解决方案1】:

评论后添加:在我的代码的 while 部分中,我忘记了 MoveNext。修复。感谢enigma state 的关注。

所以你有三个不同项目的序列,并且你想要一个方法,它返回一个包含序列相同索引元素的序列。

这是一种很难说的方式,你想要这样的序列:

A[0] / B[0] / C[0]
A[1] / B[1] / C[1]
A[2] / null / C[2] 
etc.

因此,如果其中一个序列用完元素,您希望使用 NULL 作为值继续枚举

这个方法和Enumerable.Zip很相似,只是你有三个序列,如果其中一个序列太短,你要继续枚举。

每当您认为缺少 LINQ 方法时,请考虑为 IEnumerable 编写扩展方法。特别是如果您认为可以在其他情况下重复使用它

为 IEnumerable 创建扩展方法通常相当简单。见extension methods demystified

我将编写一个通用扩展方法,它具有三个输入序列和一个结果选择器来定义返回的输出序列。这个结果选择器类似于 Enumerable.Select 中的选择器参数。

public static IEnumerable<TResult> ZipWithNull<T1, T2, T2, TResult>(
    this IEnumerable<T1> source1,
    IEnumerable<T2> source2,
    IEnumerable<T3> source3,
    Func<T1, T2, T3, TResult> resultSelector)
{
    // get the enumerators and start enumerating until there are no more elements
    IEnumerator<T1> enumerator1 = source1.GetEnumerator();
    IEnumerator<T2> enumerator2 = source2.GetEnumerator();
    IEnumerator<T3> enumerator3 = source3.GetEnumerator();

    // check if there is at least one item available
    bool t1Available = enumerator1.MoveNext();
    bool t2Available = enumerator2.MoveNext();
    bool t3Available = enumerator3.MoveNext();
    bool anyAvailabe = t1Available || t2Available || t3Available;

    while (anyAvailable)
    {
        // if available use the Current, else use default (= null for classes)
        T1 t1 = t1Available ? enumerator1.Current ?? default(T1);
        T2 t2 = t2Available ? enumerator2.Current ?? default(T2);
        T3 t3 = t3Available ? enumerator3.Current ?? default(T3);

        TResult result = resultSelector(t1, t2, t3);
        yield return result;

        t1Available = enumerator1.MoveNext();
        t2Available = enumerator2.MoveNext();
        t3Available = enumerator3.MoveNext();
        anyAvailabe = t1Available || t2Available || t3Available;
    }
}

用法:

List<A> listA = ...
List<B> listA = ...
List<C> listA = ...

// you want a dash if the value is null
const string dash = "-";

var result = listA.ZipWithNull(listB, listC,

    // parameter ResultSelector: taks one a, b, c and create the output:
    (a, b, c) => new
    {
        Number = a?.Number.ToString() ?? dash,
        Name = a?.Name ?? dash,
        Casted = b?.Casted ?? dash,
        Id = c?.Id.ToString() ?? dash,
        IsSelect = c?.IsSelect.ToString() ?? dash,
    });

请注意,此结果选择器可以优化。例如,参数a 是否等于 null 会检查两次。稍加努力,您就可以确保 resultSelector 的每个输入只检查一次。对于这个例子,我选择了简单而不是效率。

好消息是您可以将ZipWithNull 用于任何您想用空值作为替代压缩的序列。因为我将 ZipWithNull 与任何其他 LINQ 方法一样创建,所以我可以将它与其他 LINQ 方法交织在一起:

var result = customers
    .Where(customer => customer.BirthDay.Year >= 2000)
    .ZipWithNull(
         addresses.Where(address => address.City == "Amsterdam"),
         orders.Where(order => order.Total > 1000,
         (customer, address, order) => new {...});
    .GroupBy(...)
    .OrderByDescending(...)
    // Etc();

【讨论】:

  • 当我尝试使用foreach 像这样foreach (var item in result) {} 遍历结果时,它正在经历无限循环。我需要处理结果以进行进一步的数据操作,但它正在经历无限循环
【解决方案2】:

目前还不是很清楚您要做什么,但我看到了 3 种方法可以实现这一点。

最简单的是,您可以创建一个List&lt;object&gt;,其中包含您的不同类别的数据。 然后,您将丢失有关每个对象是什么类的信息(您可以将这些对象存储在 List 中,如 TupleType)。

List<object> joinedList = new List<object>();
joinedList.AddRange(ListA.Cast<object>());
joinedList.AddRange(ListB.Cast<object>());
joinedList.AddRange(ListC.Cast<object>());

另一种解决方案是让所有这些类共享一个基类或接口并构造该类型的List

List 读取时,您可以检查listItem is A 之类的类型

class z
{
}
class a : z
{
    public int number{get; set;}
    public string name{get; set;}
}
class B : z
{
   public string casted{get; set;}
}
class c : z
{
    public int Id{get; set;}
    public bool isSelect {get; set;}
}

List<z> joinedList = new List<z>();
joinedList.AddRange(ListA);
joinedList.AddRange(ListB);
joinedList.AddRange(ListC);

您还可以创建ABC 的“联合”版本,这可能适合您的用例(因为看起来您可能正在根据数据创建表)。

class Aggregate
{
   A aData {get;set;}
   B bData {get;set;}
   C cData {get;set;}
}

List<Aggregate> aggregatedData = new List<Aggregate>();
// join your data together into aggregate and add to list

【讨论】:

  • 感谢输入,既然要加入的三个类之间没有关系,我们如何使用聚合
  • 您需要在每个对象上都有一个标识符,表明它们是相关的(例如 Id 上的 C 或其他一些描述关系的数据(例如 C.Id 为 1 是等效的) to A.Name of John)。你是如何在你的问题中生成示例结果行的?我想你会使用相同的定义关系
  • 我正在尝试用 word 导出文档,并希望生成一个包含我提到的示例数据的表格,但它没有任何关系
  • 我无法建立这些类之间的关系,还有其他方法可以解决这个问题
【解决方案3】:

因此,您面临一些挑战,因为您提供的模型与您提供的列表不匹配(例如,类是 @987654326 上的 AC 不是 acId @ 应该是 string 而不是 int),但是在改变了所有这些之后我们可以这样做:

private static void AddAllValuesToDictionary<T>(List<Dictionary<string, object>> target, IList<T> source) {
        var properties = typeof(T).GetProperties();
        for (var i = 0; i < source.Count(); i++) 
        {
            if (target.Count() <= i) {
               target.Add(new Dictionary<string, object>()); 
            }

            foreach(var prop in properties) {
                target[i][prop.Name] = prop.GetValue(source[i]);
            }
        }
    }

然后

var result = new List<Dictionary<string, object>>();

AddAllValuesToDictionary(result, ListA);
AddAllValuesToDictionary(result, ListB);
AddAllValuesToDictionary(result, ListC);

如果你真的想要一个对象而不是字典,你可以选择ExpandoObject,但基本上你会得到与上述相反的体验。

在我的列表中,如果两个对象(例如“A”和“C”)具有重复的属性名称,则后者将覆盖前者。使用ExpandoObject,您设置的第一个值将是结果中出现的值。

更新这是Expando的示例

using System.Dynamic; 添加到您的文件中。

然后

private static void AddAllValuesToListOfExpandos<T>(List<ExpandoObject> target, IList<T> source) {
        var properties = typeof(T).GetProperties();
        for (var i = 0; i < source.Count(); i++) 
        {
            if (target.Count() <= i) {
               target.Add(new ExpandoObject()); 
            }

            foreach(var prop in properties) {
                target[i].TryAdd(prop.Name, prop.GetValue(source[i]));
            }
        }
    }
}

        var result2 = new List<ExpandoObject>();
        AddAllValuesToListOfExpandos(result2, ListA);
        AddAllValuesToListOfExpandos(result2, ListB);
        AddAllValuesToListOfExpandos(result2, ListC);
// Since you're using ExpandoObject you can now use the properties
// the same way you do with dynamic
        Console.WriteLine(((dynamic)result2.First()).casted);
        Console.WriteLine(((dynamic)result2.First()).number);
        Console.WriteLine(((dynamic)result2.Last()).casted);
// note: Next line will fail with RuntimeBinderException
        Console.WriteLine(((dynamic)result2.Last()).number); 

【讨论】:

  • 这些都是创建列表的列表,因为我只需要一个列表,其中的项目来自这三个列表。
猜你喜欢
  • 2020-05-01
  • 2014-05-28
  • 2021-09-10
  • 2019-12-15
  • 1970-01-01
  • 2018-03-11
  • 2022-01-19
  • 1970-01-01
  • 2020-07-11
相关资源
最近更新 更多