简体   繁体   English

C# - 将平面多对多对象列表转换为两个对象列表的不同组合列表

[英]C# - Getting flat many-to-many List of Objects into List of distinct combinations of two Lists of objects

I cannot seem to grasp how to do this, nor find an easy way to explain it... so I hope this simplified example will make sense.我似乎无法掌握如何做到这一点,也找不到简单的方法来解释它......所以我希望这个简化的例子是有意义的。

Being given a List<> of objects as such:给定一个 List<> 对象,如下所示:

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

The result I need is a List of two Lists of objects as such:我需要的结果是两个对象列表的列表,如下所示:

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; }
}

The Book should only show up once in the end result, but the Reader can show up many times. Book 应该只在最终结果中出现一次,但 Reader 可以出现多次。 Multiple books can be put together if they were read by the same readers.如果同一读者阅读了多本书,则可以将它们放在一起。

Here's how I need the result:这是我需要的结果:

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

So distinct combinations of Lists of Books and Lists of Readers is the end result.因此,最终结果是书籍列表和读者列表的不同组合。 The Book only shows up once, but the Readers can show up more than once.这本书只出现一次,但读者可以出现不止一次。 Books with the exact same list of readers would be grouped together.具有完全相同读者列表的书籍将被分组在一起。

Even if someone can tell me what this type of final result is called, I would appreciate it.即使有人能告诉我这种类型的最终结果叫什么,我也会很感激。

You can do it with this lengthy LINQ query:您可以使用这个冗长的 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();

The idea is to group twice.这个想法是分组两次。 Once per book and once per reader group.每本书一次,每个读者组一次。

First, we group by the ValueTuple (f1.BookTitle, f1.BookPages) .首先,我们按ValueTuple (f1.BookTitle, f1.BookPages) The advantage over creating a Book object is that the ValueTuple automatically overrides Equals and GetHashCode .创建Book object 的优势在于ValueTuple自动覆盖EqualsGetHashCode This is required for types used as key in a dictionary or lookup as GroupBy does.这对于用作字典中的键的类型或像GroupBy一样的查找是必需的。 Alternatively, you could override these methods in the Book class and group by Book objects.或者,您可以在Book class 中覆盖这些方法并按Book对象分组。 If you have a unique book id, use this one instead.如果您有唯一的图书 ID,请改用此 ID。

Then we create a temporary result with Select .然后我们使用Select创建一个临时结果。 We create a tuple again having 3 fields.我们再次创建一个具有 3 个字段的元组。 The tuple containing the book information, an enumerable of Reader objects and, finally, we create a string containing all the readers as a key that we will use later to group by unique reader groups.包含书籍信息的元组,一个可枚举的Reader对象,最后,我们创建一个包含所有读者作为键的字符串,稍后我们将使用它来按唯一的读者组进行分组。 If you have a unique reader id, this one instead of name and age.如果您有唯一的读者 ID,则使用此 ID 而不是姓名和年龄。

So far, we have an到目前为止,我们有一个

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

Now we group by readerKey and then create the list of ResultDoubleList objects.现在我们按readerKey ,然后创建ResultDoubleList对象列表。

If you have difficulties understanding the details, break up the LINQ query into several queries.如果您难以理解详细信息,请将 LINQ 查询分解为多个查询。 By using the "Make explicit" refactoring, you can then see what type of result you got.通过使用“显式”重构,您可以查看得到的结果类型。 (That's how I got the complex IEnumerable<T> from above.) This also allows you to inspect the intermediate results in the debugger. (这就是我从上面得到复杂的IEnumerable<T>的方式。)这也允许您在调试器中检查中间结果。


This test...这个测试...

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

... yields: ...产量:

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

A. With a string key for readers groups 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));

The above produces (with some line manual breaking):以上产生(有一些线手动中断):

[{
    "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. Shorter, but uglier B. 更短,但更丑

We need to GroupBy twice, but the first projection is not necessary, and one Select is enough.我们需要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. C。 With IEquatable使用IEquatable

This version is the longest, but arguably the most correct, as it leaves to the Reader class to decide what readers are considered equal.这个版本是最长的,但可以说是最正确的,因为它留给Reader class 来决定哪些读者被认为是平等的。

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

Output (manually formatted) Output(手动格式化)

[{
 "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 }
 ]
}
]

Assume the Book name and Reader names are IDs.假设书名和读者名是 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}");
        }
    }


声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM