2009-03-28 26 views
11

Dưới đây là một vấn đề thú vị tôi nhận thấy khi sử dụng Except điều hành: Tôi có danh sách người dùng từ mà tôi muốn loại trừ một số người sử dụng:LINQ Trừ điều hành và đối tượng bình đẳng

Các danh sách người dùng được đến từ một XML file:

Mã này đi như thế này:

interface IUser 
{ 
    int ID { get; set; } 
    string Name { get; set; } 
} 

class User: IUser 
{ 

    #region IUser Members 

    public int ID 
    { 
     get; 
     set; 
    } 

    public string Name 
    { 
     get; 
     set; 
    } 

    #endregion 

    public override string ToString() 
    { 
     return ID + ":" +Name; 
    } 


    public static IEnumerable<IUser> GetMatchingUsers(IEnumerable<IUser> users) 
    { 
     IEnumerable<IUser> localList = new List<User> 
     { 
      new User{ ID=4, Name="James"}, 
      new User{ ID=5, Name="Tom"} 

     }.OfType<IUser>(); 
     var matches = from u in users 
         join lu in localList 
          on u.ID equals lu.ID 
         select u; 
     return matches; 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     XDocument doc = XDocument.Load("Users.xml"); 
     IEnumerable<IUser> users = doc.Element("Users").Elements("User").Select 
      (u => new User 
       { ID = (int)u.Attribute("id"), 
        Name = (string)u.Attribute("name") 
       } 
      ).OfType<IUser>();  //still a query, objects have not been materialized 


     var matches = User.GetMatchingUsers(users); 
     var excludes = users.Except(matches); // excludes should contain 6 users but here it contains 8 users 

    } 
} 

Khi tôi gọi User.GetMatchingUsers(users) tôi nhận được 2 trận đấu như mong đợi. Vấn đề là khi tôi gọi users.Except(matches) Người dùng phù hợp không bị loại trừ! Tôi hy vọng 6 người dùng ut "loại trừ" chứa tất cả 8 người dùng thay thế.

Vì tất cả Tôi đang làm trong GetMatchingUsers(IEnumerable<IUser> users) được lấy IEnumerable<IUser> và chỉ trở về các IUsers có trận đấu ID (2 IUsers trong trường hợp này), sự hiểu biết của tôi là theo mặc định Except sẽ sử dụng bình đẳng tham khảo để so sánh các đối tượng để bị loại trừ. Đây có phải là cách Except hoạt động không?

Điều thú vị hơn nữa là nếu tôi hiện thực hóa đối tượng bằng cách sử dụng .ToList() và sau đó nhận được người dùng phù hợp và gọi Except, mọi thứ hoạt động như mong đợi!

Giống như vậy:

IEnumerable<IUser> users = doc.Element("Users").Elements("User").Select 
      (u => new User 
       { ID = (int)u.Attribute("id"), 
        Name = (string)u.Attribute("name") 
       } 
      ).OfType<IUser>().ToList(); //explicity materializing all objects by calling ToList() 

var matches = User.GetMatchingUsers(users); 
var excludes = users.Except(matches); // excludes now contains 6 users as expected 

Tôi không thấy lý do tại sao nên tôi cần phải thực hóa các đối tượng cho gọi Except cho rằng nó được định nghĩa trên IEnumerable<T>?

Mọi đề xuất/thông tin chi tiết sẽ được đánh giá cao.

Trả lời

10

Tôi nghĩ rằng tôi biết tại sao điều này không hoạt động như mong đợi. Vì danh sách người dùng ban đầu là biểu thức LINQ, nó được đánh giá lại mỗi khi nó được lặp lại (một lần khi được sử dụng trong GetMatchingUsers và một lần nữa khi thực hiện thao tác Except) và do đó, các đối tượng người dùng mới được tạo. Điều này sẽ dẫn đến các tham chiếu khác nhau và do đó không có kết quả phù hợp. Sử dụng ToList sửa lỗi này vì nó lặp lại truy vấn LINQ một lần duy nhất và do đó các tham chiếu được cố định.

Tôi đã có thể tái tạo sự cố bạn gặp phải và đã điều tra mã, điều này có vẻ như là một lời giải thích rất hợp lý. Tôi chưa chứng minh điều đó.

Cập nhật
Tôi chỉ chạy thử nghiệm nhưng outputting users bộ sưu tập trước khi cuộc gọi đến GetMatchingUsers, trong cuộc gọi đó, và sau nó. Mỗi lần mã băm cho đối tượng là đầu ra và chúng thực sự có giá trị khác nhau mỗi lần chỉ ra các đối tượng mới, như tôi nghi ngờ.

Đây là kết quả cho mỗi cuộc gọi:

==> Start 
ID=1, Name=Jeff, HashCode=39086322 
ID=2, Name=Alastair, HashCode=36181605 
ID=3, Name=Anthony, HashCode=28068188 
ID=4, Name=James, HashCode=33163964 
ID=5, Name=Tom, HashCode=14421545 
ID=6, Name=David, HashCode=35567111 
<== End 
==> Start 
ID=1, Name=Jeff, HashCode=65066874 
ID=2, Name=Alastair, HashCode=34160229 
ID=3, Name=Anthony, HashCode=63238509 
ID=4, Name=James, HashCode=11679222 
ID=5, Name=Tom, HashCode=35410979 
ID=6, Name=David, HashCode=57416410 
<== End 
==> Start 
ID=1, Name=Jeff, HashCode=61940669 
ID=2, Name=Alastair, HashCode=15193904 
ID=3, Name=Anthony, HashCode=6303833 
ID=4, Name=James, HashCode=40452378 
ID=5, Name=Tom, HashCode=36009496 
ID=6, Name=David, HashCode=19634871 
<== End 

Và, đây là mã sửa đổi để hiển thị các vấn đề:

using System.Xml.Linq; 
using System.Collections.Generic; 
using System.Linq; 
using System; 

interface IUser 
{ 
    int ID 
    { 
     get; 
     set; 
    } 
    string Name 
    { 
     get; 
     set; 
    } 
} 

class User : IUser 
{ 

    #region IUser Members 

    public int ID 
    { 
     get; 
     set; 
    } 

    public string Name 
    { 
     get; 
     set; 
    } 

    #endregion 

    public override string ToString() 
    { 
     return ID + ":" + Name; 
    } 


    public static IEnumerable<IUser> GetMatchingUsers(IEnumerable<IUser> users) 
    { 
     IEnumerable<IUser> localList = new List<User> 
     { 
      new User{ ID=4, Name="James"}, 
      new User{ ID=5, Name="Tom"} 

     }.OfType<IUser>(); 

     OutputUsers(users); 
     var matches = from u in users 
         join lu in localList 
          on u.ID equals lu.ID 
         select u; 
     return matches; 
    } 

    public static void OutputUsers(IEnumerable<IUser> users) 
    { 
     Console.WriteLine("==> Start"); 
     foreach (IUser user in users) 
     { 
      Console.WriteLine("ID=" + user.ID.ToString() + ", Name=" + user.Name + ", HashCode=" + user.GetHashCode().ToString()); 
     } 
     Console.WriteLine("<== End"); 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     XDocument doc = new XDocument(
      new XElement(
       "Users", 
       new XElement("User", new XAttribute("id", "1"), new XAttribute("name", "Jeff")), 
       new XElement("User", new XAttribute("id", "2"), new XAttribute("name", "Alastair")), 
       new XElement("User", new XAttribute("id", "3"), new XAttribute("name", "Anthony")), 
       new XElement("User", new XAttribute("id", "4"), new XAttribute("name", "James")), 
       new XElement("User", new XAttribute("id", "5"), new XAttribute("name", "Tom")), 
       new XElement("User", new XAttribute("id", "6"), new XAttribute("name", "David")))); 
     IEnumerable<IUser> users = doc.Element("Users").Elements("User").Select 
      (u => new User 
      { 
       ID = (int)u.Attribute("id"), 
       Name = (string)u.Attribute("name") 
      } 
      ).OfType<IUser>();  //still a query, objects have not been materialized 


     User.OutputUsers(users); 
     var matches = User.GetMatchingUsers(users); 
     User.OutputUsers(users); 
     var excludes = users.Except(matches); // excludes should contain 6 users but here it contains 8 users 

    } 
} 
+0

Nếu đúng như vậy, thì các đối tượng "mới" sẽ không được chuyển vào GetMatchingUsers mỗi lần? Ngoài ra, phương thức đó trả về một truy vấn dưới dạng kết quả và không phải là các đối tượng. Chỉ 2 xu của tôi ... –

+0

Không, bởi vì biểu thức được đánh giá mỗi khi nó được sử dụng. Trong mã của tôi, trong đó cho thấy điều này, nó được đánh giá bởi đầu ra của tôi trước khi cuộc gọi đến GetMatchingUsers, sau đó một lần nữa khi gọi GetMatchingUSers, và quan trọng, một lần nữa trong các ngoại trừ. –

+0

Bởi vì đánh giá cho GetMatchingUsers và ngoại trừ cả hai tạo ra trường hợp riêng của họ, ngoại trừ không làm việc như bạn mong đợi. –

2

Tôi nghĩ rằng bạn nên thực hiện IEquatable<T> để cung cấp riêng của bạn Bằng và phương thức GetHashCode.

Từ MSDN (Enumerable.Except):

Nếu bạn muốn so sánh trình tự của đối tượng của một số kiểu dữ liệu tùy chỉnh, bạn phải thực hiện IEqualityComparer < (Tất < (T>)>) generic giao diện trong lớp học của bạn. Ví dụ mã sau đây cho biết cách triển khai giao diện này trong loại dữ liệu tùy chỉnh và cung cấp phương thức GetHashCode và Equals .

+0

Nhưng mã anh ta phải làm việc. Tại sao nó không hoạt động? –

+0

CMS: Tôi đã triển khai IEqualtable trong mã sản xuất của mình và DOES hoạt động. Những gì tôi không hiểu là tại sao nó gọi ToList() trên truy vấn TRƯỚC KHI gọi GetMatching Users tạo ra hiệu ứng mong muốn thay vì để biến người dùng làm truy vấn –

+0

Jeff: Tôi không trả lại IUsers từ danh sách cục bộ I ' đã tạo bên trong GetMatchingUser, Phương thức trả về IUsers từ IEnumerable số gốc, do đó, các tham chiếu vẫn phải là đối tượng IUser gốc đằng sau hậu trường để bình đẳng tham chiếu đã hoạt động như mong đợi! –

12

a) Bạn cần ghi đè hàm GetHashCode. PHẢI trả về giá trị bằng nhau cho các đối tượng IUser bằng nhau. Ví dụ:

public override int GetHashCode() 
{ 
    return ID.GetHashCode()^Name.GetHashCode(); 
} 

b) Bạn cần ghi đè đối tượng.Equals (đối tượng obj) trong các lớp triển khai IUser.

public override bool Equals(object obj) 
{ 
    IUser other = obj as IUser; 
    if (object.ReferenceEquals(obj, null)) // return false if obj is null OR if obj doesn't implement IUser 
     return false; 
    return (this.ID == other.ID) && (this.Name == other.Name); 
} 

c) Là một thay thế cho (b) IUser có thể kế thừa IEquatable:

interface IUser : IEquatable<IUser> 
... 

lớp người dùng sẽ cần phải cung cấp bool Equals phương pháp trong trường hợp đó (IUser khác).

Đó là tất cả. Bây giờ nó hoạt động mà không cần gọi phương thức .ToList().

Các vấn đề liên quan