VB.NET Невозможно вернуть список на основе уникальных значений

Вопрос или проблема

Используя VB.NET, я получаю список объектов. Из этого списка объектов мне нужно удалить дублирующиеся наборы данных, чтобы список был уникальным по всем столбцам. Читая на StackOverflow, общее мнение состоит в том, чтобы использовать GroupBy, за которым следует Select, однако каждый раз, когда я пытаюсь это сделать, возвращается вся коллекция.

В данном примере я ожидаю, что количество в uniqueData будет равно 3, но оно остается равным 4. dataSet1 и dataSet2 должны рассматриваться как дубликаты друг друга.

Module Program
    Sub Main(args As String())
        Dim data As New PersonCollection With {.PersonCollection = New List(Of PersonInfo)}
        Dim dataSet1 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 10}
        Dim dataSet2 As New PersonInfo With {.FirstName = "Джон", .LastName = "Херт", .Rating = 20}
        Dim dataSet3 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 30}
        Dim dataSet4 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 10}
        data.PersonCollection.Add(dataSet1)
        data.PersonCollection.Add(dataSet2)
        data.PersonCollection.Add(dataSet3)
        data.PersonCollection.Add(dataSet4)

        Dim uniqueData = data.PersonCollection.GroupBy(Function(x) New With {x.FirstName, x.LastName, x.Rating}).Select(Function(x) x.First).ToList()

        Console.ReadLine()
    End Sub


    Private Class PersonCollection
        Property PersonCollection As List(Of PersonInfo)
    End Class

    Private Class PersonInfo
        Property FirstName As String
        Property LastName As String
        Property Rating As Integer
    End Class
End Module

Это работает с кортежами:

Module Program
    Sub Main(args As String())
        Dim data As New PersonCollection With {.PersonCollection = New List(Of PersonInfo)}
        Dim dataSet1 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 10}
        Dim dataSet2 As New PersonInfo With {.FirstName = "Джон", .LastName = "Херт", .Rating = 20}
        Dim dataSet3 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 30}
        Dim dataSet4 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 10}
        data.PersonCollection.Add(dataSet1)
        data.PersonCollection.Add(dataSet2)
        data.PersonCollection.Add(dataSet3)
        data.PersonCollection.Add(dataSet4)

        Dim uniqueData = data.PersonCollection _
            .GroupBy(Function(x) New Tuple(Of String, String, Integer)(
                x.FirstName, x.LastName, x.Rating)) _
            .Select(Function(x) x.First).ToList()
        Console.ReadLine()
    End Sub


    Private Class PersonCollection
        Property PersonCollection As List(Of PersonInfo)
    End Class

    Private Class PersonInfo
        Property FirstName As String
        Property LastName As String
        Property Rating As Integer
    End Class
End Module

Но лучше использовать Comparer:

Module Program
    Sub Main(args As String())
        Dim data As New PersonCollection With {.PersonCollection = New List(Of PersonInfo)}
        Dim dataSet1 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 10}
        Dim dataSet2 As New PersonInfo With {.FirstName = "Джон", .LastName = "Херт", .Rating = 20}
        Dim dataSet3 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 30}
        Dim dataSet4 As New PersonInfo With {.FirstName = "Боб", .LastName = "Смит", .Rating = 10}
        data.PersonCollection.Add(dataSet1)
        data.PersonCollection.Add(dataSet2)
        data.PersonCollection.Add(dataSet3)
        data.PersonCollection.Add(dataSet4)

        Dim comparer As New PersonInfoComparer
        Dim uniqueData = data.PersonCollection.Distinct(comparer).ToList

        Console.ReadLine()
    End Sub

    Private Class PersonCollection
        Property PersonCollection As List(Of PersonInfo)
    End Class

    Private Class PersonInfo
        Property FirstName As String
        Property LastName As String
        Property Rating As Integer
    End Class

    Private Class PersonInfoComparer : Inherits EqualityComparer(Of PersonInfo)

        Public Overrides Function Equals(x As PersonInfo, y As PersonInfo) As Boolean
            Return x.FirstName.Equals(y.FirstName) _
                AndAlso x.LastName.Equals(y.LastName) _
                AndAlso x.Rating.Equals(y.Rating)
        End Function

        Public Overrides Function GetHashCode(obj As PersonInfo) As Integer
#If NETSTANDARD2_0_OR_GREATER Or NETCOREAPP2_1_OR_GREATER Then
            Return HashCode.Combine(obj.FirstName, obj.LastName, obj.Rating)
#Else
            Return obj.FirstName.GetHashCode() _
                Xor obj.LastName.GetHashCode() _
                Xor obj.Rating.GetHashCode()
#End If
        End Function

    End Class

End Module

Попробуйте использовать кортеж вместо этого, поскольку он имеет семантику значений в отличие от типа анонимного класса, который имеет семантику ссылок. Т.е., два объекта сравниваются по ссылке, а не по их полям или свойствам.

.GroupBy(Function(x) (x.FirstName, x.LastName, x.Rating))

Обратите внимание, что синтаксис New With {x.FirstName ... } работает с О/R маппером, потому что затем это преобразуется в SQL, и различие между семантикой значений и семантикой ссылок больше не применяется.


Другим решением является объявление свойств анонимного типа как Ключевых свойств.

.GroupBy(Function(x) New With {Key x.FirstName, Key x.LastName, Key x.Rating})

Но кортежи гораздо проще.


Мы можем немного упростить запрос, используя DistinctBy вместо GroupBy, за которым следует Select:

Dim uniqueData = data.PersonCollection _
    .DistinctBy(Function(x) (x.FirstName, x.LastName, x.Rating)) _
    .ToList()

Ответ или решение

Решение проблемы с дубликатами в VB.NET

Введение

В VB.NET вы можете столкнуться с ситуацией, когда необходимо удалить дубликаты из списка объектов. В вашем случае, создание уникального списка объектов PersonInfo на основе их свойств FirstName, LastName и Rating может показаться сложной задачей. Однако правильное понимание концепций группировки и уникальности объектов поможет вам быстро обрести нужное решение. Давайте разберём подробнее, как это сделать.

Проблема с группировкой

В вашем текущем коде вы используете метод GroupBy для создания групп объектов и последующего выбора уникальных значений. Однако, когда используется анонимный тип в этой форме:

.GroupBy(Function(x) New With {x.FirstName, x.LastName, x.Rating})

Сравнение объектов происходит по ссылке, а не по значению. Поэтому ваш уникальный набор остаётся пустым, если только объекты не являются ссылками на одну и ту же память.

Решение с использованием кортежей

Одним из самых простых и эффективных способов является использование кортежей. Кортежи в VB.NET имеют семантику значений, что означает, что при сравнении они будут сравниваться по его компонентам. Вам нужно заменить сборку группировки на следующий код:

Dim uniqueData = data.PersonCollection _
    .GroupBy(Function(x) (x.FirstName, x.LastName, x.Rating)) _
    .Select(Function(x) x.First).ToList()

Это позволит вам получить уникальный список объектов.

Решение через DistinctBy

Если вы используете .NET 6 или выше, вы можете упростить свой код, используя метод DistinctBy, который позволяет легко извлекать уникальные объекты. Это значительно упрощает код:

Dim uniqueData = data.PersonCollection _
    .DistinctBy(Function(x) (x.FirstName, x.LastName, x.Rating)) _
    .ToList()

Этот метод предлагает более простую и интуитивно понятную реализацию для удаления дубликатов.

Использование сравнения через класс Comparer

Ещё один подход к решению данной задачи – заданный вами кастомный компаратор. Это необходимо, если вы используете коллекции, где предоставленные алгоритмы уже не работают должным образом. Вам нужно будет создать класс, производный от EqualityComparer(Of PersonInfo):

Private Class PersonInfoComparer : Inherits EqualityComparer(Of PersonInfo)
    Public Overrides Function Equals(x As PersonInfo, y As PersonInfo) As Boolean
        Return x.FirstName.Equals(y.FirstName) _
            AndAlso x.LastName.Equals(y.LastName) _
            AndAlso x.Rating.Equals(y.Rating)
    End Function

    Public Overrides Function GetHashCode(obj As PersonInfo) As Integer
        Return HashCode.Combine(obj.FirstName, obj.LastName, obj.Rating)
    End Function
End Class

Для использования компаратора в вашем коде:

Dim comparer As New PersonInfoComparer
Dim uniqueData = data.PersonCollection.Distinct(comparer).ToList()

Этот способ также позволяет гибко управлять логикой сравнения.

Заключение

При работе с дубликатами в коллекциях объектов VB.NET, вопросы семантики значений и ссылок могут стать серьезным препятствием. Используйте кортежи или методы DistinctBy для удобства, или напишите свой собственный класс сравнения, если требуется более сложное поведение. Каждый из этих подходов позволит вам получить желаемый результат – уникальный список объектов PersonInfo.

Если у вас остались вопросы или необходима дополнительная помощь, не стесняйтесь обращаться!

Оцените материал
Добавить комментарий

Капча загружается...