【问题标题】:C# - Getting flat many-to-many List of Objects into List of distinct combinations of two Lists of objectsC# - 将平面多对多对象列表转换为两个对象列表的不同组合列表
【发布时间】:2020-03-04 14:58:25
【问题描述】:

我似乎无法掌握如何做到这一点,也找不到简单的方法来解释它......所以我希望这个简化的例子能有意义。

得到一个对象列表:

public class FlatManyToMany
{
    public string BookTitle { get; set; }
    public int BookPages { get; set; }
    public string ReaderName { get; set; }
    public int ReaderAge { get; set; }
}

var flatManyToMany = new List<FlatManyToMany>();

flatManyToMany.Add(new FlatManyToMany { BookTitle = "How to Do This Double List", BookPages = 105, ReaderName = "Kyle", ReaderAge = 29 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "How to Do This Double List", BookPages = 105, ReaderName = "Bob", ReaderAge = 34 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "Kyle", ReaderAge = 29 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "James", ReaderAge = 45 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "Brian", ReaderAge = 15 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "Kyle", ReaderAge = 29 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "James", ReaderAge = 45 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "Brian", ReaderAge = 15 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Impostor Syndrome", BookPages = 454, ReaderName = "Kyle", ReaderAge = 29 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Self Doubt and You", BookPages = 999, ReaderName = "Kyle", ReaderAge = 29 });

我需要的结果是两个对象列表的列表:

public class ResultDoubleList
{
    public List<Book> Books { get; set; } = new List<Book>();
    public List<Reader> Readers { get; set; } = new List<Reader>();
}

public class Book
{
    public string Title { get; set; }
    public int Pages { get; set; }
}

public class Reader
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Book 在最终结果中应该只出现一次,但 Reader 可以出现多次。如果同一读者阅读了多本书,则可以将它们放在一起。

这是我需要的结果:

List<ResultDoubleList> results = new List<ResultDoubleList>();

result(1):
Books
    How to Do This Double List  105
Readers
    Kyle    29
    Bob     34

result(2):
Books
    Gone With Jon Skeet     192
    Why Is This So Hard?    56
Readers
    Kyle    29
    James   45
    Brian   15

result(3):
Books
    Impostor Syndrome   454
    Self Doubt and You  999
Readers
    Kyle    29

因此,书籍列表和读者列表的不同组合是最终结果。这本书只出现一次,但读者可以出现不止一次。具有完全相同读者列表的书籍将被归为一组。

即使有人能告诉我这种类型的最终结果叫什么,我也会很感激。

【问题讨论】:

  • 我认为ReaderAgeBookPages需要定义为int
  • @RufusL 你是对的。我的实际问题与书籍和读者无关,所以只是一个尽可能简化问题的示例。

标签: c# list linq loops


【解决方案1】:

A.为读者组提供字符串键

var booksReadByGroups = flatManyToMany.GroupBy(a => a.BookTitle)
    .Select(g => new
    {
        Book = new Book { Title = g.Key, Pages = g.Max(a => a.BookPages) },
        Readers = g.Select(a => new Reader { Name = a.ReaderName, Age = a.ReaderAge }).ToList()
    })
    .GroupBy(b => string.Join("+",b.Readers.OrderBy(r=>r.Name).ThenBy(r=>r.Age).Select(r => $"{r.Name}{r.Age}")))
    .Select(g => new
    {
        Books = g.Select(b => b.Book),
        Readers = g.First().Readers
    })
    .ToList();

Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(booksReadByGroups));

以上产生(有一些线手动中断):

[{
    "Books":[
        {"Title":"How to Do This Double List","Pages":105}
    ],
    "Readers":[
        {"Name":"Kyle","Age":29},
        {"Name":"Bob","Age":34}
    ]
},{
    "Books":[
        {"Title":"Gone With Jon Skeet","Pages":192},
        {"Title":"Why Is This So Hard?","Pages":56}
    ],
    "Readers":[
        {"Name":"Kyle","Age":29},
        {"Name":"James","Age":45},{"Name":"Brian","Age":15}
    ]
},{
    "Books":[
        {"Title":"Impostor Syndrome","Pages":454},
        {"Title":"Self Doubt and You","Pages":999}
    ],
    "Readers":[
        {"Name":"Kyle","Age":29}
    ]
}]

B.更短,但更丑

我们需要GroupBy两次,但第一次投影不是必须的,一次Select就足够了。

var readerGroups = flatManyToMany.GroupBy(a => a.BookTitle)
    .GroupBy(g => string.Join("+",g.OrderBy(r=>r.ReaderName).ThenBy(r=>r.ReaderAge).Select(r => $"{r.ReaderName}{r.ReaderAge}")))
    .Select(g => new
    {
        Books = g.Select( g2 => new Book { Title = g2.Key, Pages = g2.Max(a => a.BookPages) }),
        Readers = g.First().Select(a => new Reader { Name = a.ReaderName, Age = a.ReaderAge })
    });

C.与IEquatable

这个版本是最长的,但可以说是最正确的,因为它留给Reader 类来决定哪些读者被认为是平等的。

class ReadersComparer : IEqualityComparer<List<Reader>>
{
    public bool Equals(List<Reader> a, List<Reader> b) => Enumerable.SequenceEqual(a, b); // Please note this doesn't order the lists so you either need to order them before, or order them here and implement IComparable on the Reader class
    public int GetHashCode(List<Reader> os)
    {
        int hash = 19;
        foreach (var o in os) { hash = hash * 31 + o.GetHashCode(); }
        return hash;
    }
}

public class Reader : IEquatable<Reader>
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override int GetHashCode() => (Name, Age).GetHashCode();
    public bool Equals(Reader other) => (other is null) ? false : this.Name == other.Name && this.Age == other.Age;
    public override bool Equals(object obj) => Equals(obj as Reader);
}


static void Main(string[] args)
{
var actsOfReading = new[]{
    new Reading { BookTitle = "How to Do This Double List", BookPages = 105, ReaderName = "Kyle", ReaderAge = 29},
    new Reading { BookTitle = "How to Do This Double List", BookPages = 105, ReaderName = "Bob", ReaderAge = 34},
    new Reading { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "Kyle", ReaderAge = 29},
    new Reading { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "James", ReaderAge = 45},
    new Reading { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "Brian", ReaderAge = 15},
    new Reading { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "Kyle", ReaderAge = 29},
    new Reading { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "James", ReaderAge = 45},
    new Reading { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "Brian", ReaderAge = 15},
    new Reading { BookTitle = "Impostor Syndrome", BookPages = 454, ReaderName = "Kyle", ReaderAge = 29},
    new Reading { BookTitle = "Self Doubt and You", BookPages = 999, ReaderName = "Kyle", ReaderAge = 29}
};


var booksReadByGroups = actsOfReading.GroupBy(a => a.BookTitle)
    .Select(g => new
    {
        Book = new Book { Title = g.Key, Pages = g.Max(a => a.BookPages) },
        Readers = g.Select(a => new Reader { Name = a.ReaderName, Age = a.ReaderAge }).ToList()
    })
    .GroupBy(b => b.Readers, new ReadersComparer())
    .Select(g => new
    {
        Books = g.Select(b => b.Book),
        Readers = g.First().Readers
    })
    .ToList();

    Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(booksReadByGroups));
}

输出(手动格式化)

[{
 "Books": [{
 "Title": "How to Do This Double List", "Pages": 105 }
 ],
 "Readers": [{
 "Name": "Kyle", "Age": 29 }, {
 "Name": "Bob", "Age": 34 }
 ]
}, {
 "Books": [{
 "Title": "Gone With Jon Skeet", "Pages": 192 }, {
 "Title": "Why Is This So Hard?", "Pages": 56 }
 ],
 "Readers": [{
 "Name": "Kyle", "Age": 29 }, {
 "Name": "James", "Age": 45 }, {
 "Name": "Brian", "Age": 15 }
 ]
}, {
 "Books": [{
 "Title": "Impostor Syndrome", "Pages": 454 }, {
 "Title": "Self Doubt and You", "Pages": 999 }
 ],
 "Readers": [{
 "Name": "Kyle", "Age": 29 }
 ]
}
]

【讨论】:

    【解决方案2】:

    您可以使用这个冗长的 LINQ 查询来做到这一点:

    var result = flatManyToMany
        .GroupBy(f1 => (f1.BookTitle, f1.BookPages))
        .Select(g1 => (bookInfo: g1.Key,
                        readers:
                            g1.Select(f2 => new Reader { Name= f2.ReaderName, Age= f2.ReaderAge }),
                        readerKey:
                            String.Join("|", g1.Select(f3 => $"{f3.ReaderName}{f3.ReaderAge}"))))
        .GroupBy(a1 => a1.readerKey)
        .Select(g2 => new ResultDoubleList {
            Books = g2.Select(a2 => new Book {
                        Title = a2.bookInfo.BookTitle,
                        Pages = a2.bookInfo.BookPages
                    }
                ).ToList(),
            Readers = g2.First().readers.ToList() // Any will do, since they have the same readers
        })
        .ToList();
    

    这个想法是分组两次。每本书一次,每个读者组一次。

    首先,我们按ValueTuple (f1.BookTitle, f1.BookPages) 分组。与创建Book 对象相比的优势在于ValueTuple 会自动覆盖EqualsGetHashCode。这对于用作字典中的键的类型或 GroupBy 所做的查找是必需的。或者,您可以在 Book 类中覆盖这些方法,并按 Book 对象分组。如果您有唯一的图书 ID,请改用此 ID。

    然后我们使用Select 创建一个临时结果。我们再次创建一个具有 3 个字段的元组。包含书籍信息的元组,Reader 对象的可枚举,最后,我们创建一个包含所有读者作为键的字符串,稍后我们将使用它来按唯一读者组进行分组。如果您有唯一的读者 ID,则使用此 ID 而不是姓名和年龄。

    到目前为止,我们有一个

    IEnumerable<(
        (string BookTitle, int BookPages) bookInfo,
        IEnumerable<Reader> readers,
        string readerKey
    )>
    

    现在我们按readerKey 分组,然后创建ResultDoubleList 对象列表。

    如果您难以理解细节,请将 LINQ 查询分解为多个查询。通过使用“显式”重构,您可以查看得到的结果类型。 (这就是我从上面得到复杂的IEnumerable&lt;T&gt; 的方法。)这也允许您在调试器中检查中间结果。


    这个测试...

    int resultNo = 1;
    foreach (ResultDoubleList item in result) {
        Console.WriteLine($"\r\nresult({resultNo++}):");
        Console.WriteLine("Books");
        foreach (var book in item.Books) {
            Console.WriteLine($"    {book.Title,-28} {book.Pages,3}");
        }
        Console.WriteLine("Readers");
        foreach (var reader in item.Readers) {
            Console.WriteLine($"    {reader.Name,-8} {reader.Age,2}");
        }
    }
    Console.ReadKey();
    

    ...产量:

    result(1):
    Books
        How to Do This Double List   105
    Readers
        Kyle     29
        Bob      34
    
    result(2):
    Books
        Gone With Jon Skeet          192
        Why Is This So Hard?          56
    Readers
        Kyle     29
        James    45
        Brian    15
    
    result(3):
    Books
        Impostor Syndrome            454
        Self Doubt and You           999
    Readers
        Kyle     29
    

    【讨论】:

    • 我喜欢这个。
    • 您应该对部分 readerKey 进行排序,否则结果将无法预测。
    • 天哪,就是这样。当您应该看到我正在尝试的内容时,您会说“冗长的 LINQ 查询”……它已经失控了。真正的问题有大约 5 个“书”属性和 26 个“读者”属性,这个答案/逻辑按预期扩展并解决了我的真正问题。谢谢!!!
    • @AntonínLejsek,好点子!阅读器在示例数据中具有相同的顺序,但在实际数据集中可能会有所不同。
    【解决方案3】:

    假设书名和读者名是 ID。

    var results = flatManyToMany
    .GroupBy(f => new { f.BookTitle, f.BookPages })
    .Select(g => new
    {
        Book = new Book() { Title = g.Key.BookTitle, Pages = g.Key.BookPages },
        Readers = g.Select(i => new Reader() { Name = i.ReaderName, Age = i.ReaderAge })
    })
    .GroupBy(i => string.Concat(i.Readers.Select(r => r.Name).Distinct()))
    .Select(g => new ResultDoubleList()
    {
        Books = g.Select(i => i.Book).ToList(),
        Readers = g.SelectMany(i => i.Readers).GroupBy(r => r.Name).Select(r => r.First()).ToList()
    })
    ;
    
        foreach(var result in results)
        {
            Console.WriteLine("Result:");
            Console.WriteLine("\tBooks:");
            foreach(var b in result.Books)
            {
                Console.WriteLine($"\t\t{b.Title}");
            }
            Console.WriteLine("\tReaders:");
            foreach (var reader in result.Readers)
            {
                Console.WriteLine($"\t\t{reader.Name}");
            }
        }
    
    
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-14
      • 2021-02-15
      • 1970-01-01
      • 1970-01-01
      • 2018-04-21
      • 2013-12-02
      相关资源
      最近更新 更多