使用 "ObjectList.Contains(foo)" 时对象列表无法正确比较

List of objects wont compare correctly when using "ObjectList.Contains(foo)"

从 HelperLibrary.Models.Book.cs

预订 Class
public class Book
{
    public string Title;
    public string Author;
    public string ISBN;

    public Book(string title, string author, string iSBN)
    {
        Title = title;
        Author = author;
        ISBN = iSBN;
    }

}

通话

private void SaveChanges_btn_Click(object sender, RoutedEventArgs e)
    {
        List<HelperLibrary.Models.Book> NewUsersBooks = new List<HelperLibrary.Models.Book>();

        foreach (var x in UserBooks_List.Items)
        {

            foreach(HelperLibrary.Models.Book y in App.GlobalBookList)
            {

                if (y.ISBN == x.ToString())
                {
                    NewUsersBooks.Add(y);
                }
            }

        }


        HelperLibrary.Helpers.SQLHelper.AddBookToUser(App.GlobalUserList[UserList_List.SelectedIndex], NewUsersBooks);


}

Sql 来自 HelperLibrary.SqlHelper.cs

的电话
    public static void AddBookToUser(Models.User user, List<Models.Book> NewBooks)

    {
        List<Models.Book> OnlineUsersBooks = new List<Models.Book>();

        OnlineUsersBooks = GetUsersBooks(user);

        Debug.WriteLine("Online Count: " + OnlineUsersBooks.Count.ToString());

        if (OnlineUsersBooks.Count > 0)
        {


                foreach (Models.Book y in NewBooks)
                {

                    if (!(OnlineUsersBooks.Contains(y)))
                    {

                        using (SqlConnection connection = new SqlConnection(connectionString))
                        {
                            SqlCommand command = new SqlCommand("INSERT INTO Bookings VALUES (@UserId, @Title, @Author, @ISBN)", connection);
                            command.Parameters.AddWithValue("@UserId", user.GetUserID);
                            command.Parameters.AddWithValue("@Title", y.Title);
                            command.Parameters.AddWithValue("@Author", y.Author);
                            command.Parameters.AddWithValue("@ISBN", y.ISBN);

                            Debug.WriteLine(command.ToString());

                            command.Connection.Open();
                            command.ExecuteNonQuery();
                        }
                    }



            }

        }
        else
        {
            foreach (Models.Book y in NewBooks)
            {
                using (SqlConnection connection = new SqlConnection(connectionString))
                {
                    SqlCommand command = new SqlCommand("INSERT INTO Bookings VALUES (@UserId, @Title, @Author, @ISBN)", connection);
                    command.Parameters.AddWithValue("@UserId", user.GetUserID);
                    command.Parameters.AddWithValue("@Title", y.Title);
                    command.Parameters.AddWithValue("@Author", y.Author);
                    command.Parameters.AddWithValue("@ISBN", y.ISBN);

                    Debug.WriteLine(command.ToString());

                    command.Connection.Open();
                    command.ExecuteNonQuery();
                }
            }
        }

    }

GetUserBooks 方法经过测试并且工作正常,返回图书列表。 我需要某种额外的覆盖来获得

    if (!(OnlineUsersBooks.Contains(y)))

比较正确? 这是一个相当粗鲁的早期代码,客气一点,还有很多指标需要改进。

有几种方法可以处理这个问题。 一种方法,正如 David Grilach 在他的回答中所写(现已删除,但 Rufus L 添加了另一个显示如何做的答案),是覆盖 Equals 方法 - 但我不建议这样做,除非你真的知道你是什么是做。当您覆盖 Equals 方法时,建议同时覆盖 GetHashCode 方法 - 这样做很容易出错。

另一种方法是将 Contains 更改为 Find- 这可能是最简单的方法:

if (OnlineUsersBooks.Find(b=> b.ISBN == y.ISBN)==null)

使用 Find 方法允许您使用 lambda 表达式作为谓词,因此您根本不必覆盖 EqualsGetHashCode

还有一种方法是使用 linq。它非常强大而且不难学习,而且它可以帮助您编写比现在少得多的代码。

这是一个未经测试的示例,说明我如何使用 linq 获取您需要插入数据库的书籍:

var booksToAdd = NewBooks
    .Where(nb => !OnlineUsersBooks
        .Any(ob => ob.ISBN == nb.ISBN));

它会 return 一个包含 NewBooks 中所有在 OnlineUsersBooks 中没有 ISBN 匹配的书的 IEnumerable<Book>,而您不必编写循环获取它。
这种方法的另一个好处是它消除了对 if(OblineUsersBooks.Count>0) 的需要——它对空列表也同样有效。

此外,作为旁注,您不应使用 public 字段。相反,使用 public 属性 (Bonus reading: Why?):

public class Book
{
    public string Title {get; set;} // Note the {get;set;} here.
    public string Author {get; set;}
    public string ISBN {get; set;}

    public Book(string title, string author, string iSBN)
    {
        Title = title;
        Author = author;
        ISBN = iSBN;
    }

}

为了使 Contains return 有用,您需要覆盖 class 上的 Equals 方法。否则,比较是使用参考比较完成的(这意味着如果集合中的一本书指向与您要查找的书相同的内存位置,它只会 return 为真)。

执行此操作的最简单方法可能是使用 ISBN,因为对于书籍,我认为这应该是一个唯一标识符。但如果您愿意,也可以使用其他字段进行比较。

请注意,当您覆盖 Equals 时,您也应该覆盖 GetHashCode。这是一个简单的例子:

public class Book
{
    public string Title;
    public string Author;
    public string ISBN;

    public Book(string title, string author, string iSBN)
    {
        Title = title;
        Author = author;
        ISBN = iSBN;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Book);
    }

    protected bool Equals(Book other)
    {
        return string.Equals(ISBN, other?.ISBN);
    }

    public override int GetHashCode()
    {
        return ISBN?.GetHashCode() ?? 0;
    }
}

默认情况下,Collection.Contains 执行引用比较,因此只有当 ab 是同一对象时,a == b 才为真。具有相同的字段值是不够的。

有几种不同的方法可以解决这个问题,但是如果您希望实例在它们的字段值相等时比较相等,您需要实现 IEquatable:

public class Book : IEquatable<Book>
{
    public readonly string Title;
    public readonly string Author;
    public readonly string ISBN;

    public Book(string title, string author, string iSBN)
    {
        if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
        if (string.IsNullOrEmpty(author)) throw new ArgumentNullException(nameof(author));
        if (string.IsNullOrEmpty(iSBN)) throw new ArgumentNullException(nameof(iSBN));

        Title = title;
        Author = author;
        ISBN = iSBN;
    }

    public bool Equals(Book other)
    {
        return other != null
            && other.Title == Title && other.Author == Author && other.ISBN == ISBN;
    }

    protected override bool Equals(object other)
    {
        return Equals((Book)other);
    }

    public override int GetHashCode()
    {
        return Title.GetHashCode() ^ Author.GetHashCode() ^ ISBN.GetHashCode();
    }
}

根据您使用 Book 的方式,您可能希望将其字段变成属性,您可能希望覆盖 ==!=,并且可能希望更改您的 GetHashCode 实施,但我所展示的是一个良好的开端。