Lâu rồi không viết bài về technical nên phải viết 1 bài cho thiên hạ biết mình vẫn code :D. Ở bài viết này, mình sẽ nói về một chuyện khá đơn giản trong C#: So sánh 2 object. Đây là một vấn đề ai cũng tưởng là dễ, mình sẽ nâng dần vấn đề lên từ đơn giản đến phức tạp. Cách giải quyết cũng sẽ từ đơn giản trở nên phức tạp, sau đó sẽ trở lại đơn giản. Nếu chịu khó đọc bài viết này từ đầu đến cuối, các bạn sẽ ngộ ra nhiều điều, khả năng technical cũng sẽ tăng kha khá đấy.
Cấp độ 1: Class đơn giản
Chúng ta bắt đầu bài toán với một class đơn giản nhé. Class này có 3 properties, khi ta gọi hàm Equals, C# chỉ so sánh reference, do đó kết quả là False
public class Student { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } } var student1 = new Student { FirstName = "Pham", LastName = "Hoang", Age = 15, }; var student2 = new Student { FirstName = "Pham", LastName = "Hoang", Age = 15, }; student1.Equals(student2); //False
Để xử lý chuyện này, chúng ta chỉ cần override lại hàm Equals là xong, không phức tạp, chắc bạn nào cũng đã học ở trường nhỉ
public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) return false; var student = (Student) obj; return FirstName.Equals(student.FirstName) && LastName.Equals(student.LastName) && Age == student.Age && BirthDate.Equals(student.BirthDate); }
Cấp độ 2: Sử dụng extension method
Hàm Equals vẫn còn khuyết điểm, đó là student1 không được null. Ta có thể xử lý vấn đề này bằng cách sử dụng extension method như sau. Giờ ta có thể gọi student1.Equals(student2) dù student1 có null đi nữa:
public static bool DeepEquals(this Student obj, Student another) { //Nếu null hoặc giống nhau, trả true if (ReferenceEquals(obj, another)) return true; //Nếu 1 trong 2 là null, trả false if ((obj == null) || (another == null)) return false; return obj.FirstName.Equals(another.FirstName) && obj.LastName.Equals(another.LastName) && obj.Age == another.Age && obj.BirthDate.Equals(another.BirthDate); }
Cấp độ 3: Viết method để so sánh 2 object nói chung
Hãy nghĩ tới trường hợp ứng dụng của bạn có khoảng vài chục class, mỗi class có vài chục property. Bạn sẽ làm gì? Hì hục viết tay hàm Equals cho từng class? Hãy sử dụng Refection trong C# để viết 1 hàm so sánh object có thể dùng cho mọi object.
public static bool DeepEquals(this object obj, object another) { if (ReferenceEquals(obj, another)) return true; if ((obj == null) || (another == null)) return false; //So sánh class của 2 object, nếu khác nhau thì trả fail if (obj.GetType() != another.GetType()) return false; var result = true; //Lấy toàn bộ các properties của obj //sau đó so sánh giá trị của từng property foreach (var property in obj.GetType().GetProperties()) { var objValue = property.GetValue(obj); var anotherValue = property.GetValue(another); if (!objValue.Equals(anotherValue)) result = false; } return result; }
Hàm này chạy khá OK. Bạn đang tự hỏi: Ối dào, dễ thế này mà cũng viết? Chưa đâu, hãy xem tiếp phía dưới, nhiều thứ “đáng sợ” hơn đang chờ đấy.
Cấp độ 4: Trường hợp object chứa object hoặc struct như DateTime
Ở cấp độ trên, ta viết hàm so sánh từng trường. Thế nhưng giả sử trong class Student, ta có chứa DateTime hoặc 1 class khác thì sao nhỉ??
public class Student { public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Teacher Teacher { get; set; } } public class Teacher { public string Name { get; set; } public string Subject { get; set; } } var student1 = new Student { FirstName = "Pham", LastName = "Hoang", BirthDate = new DateTime(1992, 12, 5), Teacher = new Teacher { Name = "Le Minh", Subject = "Math"} }; var student2 = new Student { FirstName = "Pham", LastName = "Hoang", BirthDate = new DateTime(1992, 12, 5), Teacher = new Teacher { Name = "Le Minh", Subject = "Math" } };
Suy nghĩ 1 tí nào. Để giải quyết, ta cũng sẽ so sánh từng trường, nhưng nếu trường đó là một object thì ta sẽ so sánh bằng hàm DeepEquals đã viết. Một thuật giải đệ quy cơ bản thôi mà 😀
public static bool DeepEquals(this object obj, object another) { if (ReferenceEquals(obj, another)) return true; if ((obj == null) || (another == null)) return false; if (obj.GetType() != another.GetType()) return false; //Nếu property không phải class, chỉ là int, double, DateTime v...v //Gọi hàm equal thông thường if (!obj.GetType().IsClass) return obj.Equals(another); var result = true; foreach (var property in obj.GetType().GetProperties()) { var objValue = property.GetValue(obj); var anotherValue = property.GetValue(another); //Tiếp tục đệ quy if (!objValue.DeepEquals(anotherValue)) result = false; } return result; }
Wow, thế là đã xong, mọi vấn đề đã được giải quyết, bạn đang tự khen mình giỏi. Ồ, thế còn trường hợp không phải một object, mà là một List thì sao nhỉ, căng đây 😦
Cấp độ 5: So sánh 2 list
May thay, ta có thể viết extension method cho list như sau (Chữ <T> là generic nhé, các bạn có thể đọc bài này để xem lại).
public static bool DeepEquals<T>(this IEnumerable<T> obj, IEnumerable<T> another) { if (ReferenceEquals(obj, another)) return true; if ((obj == null) || (another == null)) return false; bool result = true; //Duyệt từng phần tử trong 2 list đưa vào using (IEnumerator<T> enumerator1 = obj.GetEnumerator()) using (IEnumerator<T> enumerator2 = another.GetEnumerator()) { while (true) { bool hasNext1 = enumerator1.MoveNext(); bool hasNext2 = enumerator2.MoveNext(); //Nếu có 1 list hết, hoặc 2 phần tử khác nhau, thoát khoải vòng lặp if (hasNext1 != hasNext2 || !enumerator1.Current.DeepEquals(enumerator2.Current)) { result = false; break; } //Dừng vòng lặp khi 2 list đều hết if (!hasNext1) break; } } return result; }
Phù, tạm xong. Chắc không còn gì nữa đâu nhỉ?
var list1 = new List<Student> { student1, student2 }; var list2 = new List<Student> { student1, student2}; list1 == list2; //True
Cấp độ 6: Một đống những thứ tả pín lù khác
Bạn chợt nhớ ra rằng, trong C# còn hằng hà sa số những thứ tương tự List như Dictionary, HashSet,… chẳng lẽ phải viết hết. Còn một vài trường hợp tréo ngoe hơn, vd như class Student sẽ chứa 1 list cái Teacher, method chúng ta viết không chạy được
var teacherA = new Teacher { Name = "Le Minh", Subject = "Math"}; var teacherB = new Teacher { Name = "Tai Phu", Subject = "Physics"}; var student1 = new Student { FirstName = "Pham", LastName = "Hoang", Age = 15, BirthDate = new DateTime(1992, 12, 5), Teacher = new List<Teacher> { teacherA, teacherB} }; var student2 = new Student { FirstName = "Pham", LastName = "Hoang", Age = 15, BirthDate = new DateTime(1992, 12, 5), Teacher = new List<Teacher> { teacherA, teacherB} };
Tới đây mình cũng bó tay rồi, con đường phía trước khá rắc rối gian nan và phức tạp :'(. Bạn được chọn 1 trong 2 lựa chọn sau:
- Với những class khó quá, hãy tự viết hàm Equals, cũng không nhiều lắm
- Theo đuổi hàm generic tới cùng, bằng cách đọc tiếp bài này.
Cấp độ cuối cùng: Cầu cứu anh JSON
Cách giải quyết vấn đề thật ra đơn giản hơn bạn nghĩ. Hãy serialize 2 object đó dưới dạng chuỗi JSON, so sánh 2 chuỗi được tạo ra là xong. (Việc serialize ra json đã giải quyết 99% vấn đề phức tạp liên quan đến các kiểu dữ liệu rồi, may quá :D).
Các bước thực hiện:
1. Add Reference Newtonsoft.JSON theo hướng dẫn, được kết quả như hình 3 là ok.
2. Viết 1 hàm so sánh gọn nhẹ đơn giản:
public static bool JSONEquals(this object obj, object another) { if (ReferenceEquals(obj, another)) return true; if ((obj == null) || (another == null)) return false; if (obj.GetType() != another.GetType()) return false; var objJson = JsonConvert.SerializeObject(obj); var anotherJson = JsonConvert.SerializeObject(another); return objJson == anotherJson; }
Có 1 vài thư viện khác, các bạn có thể search trên google với từ khóa: Deep Compare C#.
Vì bài viết khá dài, lại hơi thiên về technical nên mình đã cố gắng khiến nó hấp dẫn hơn. Chúc mừng các bạn nếu các bạn chịu khó đọc được đến cuối cùng. Phần thưởng cho các bạn kiên nhẫn đây: 5 bạn comment đầu tiên trong bài viết này có quyền request mình viết 1 bài về 1 khía cạnh trong C#, MVC hoặc javascript mà bạn muốn tìm hiểu nhé. Chúc các bạn may mắn.
Vô tình nhặt được bí cấp “Huỳnh Hoa Bảo Điển”, phải ngâm cứu mới được.
Thanks bác nhiều đã share EXP nhiều đến như vậy
LikeLike
Anh có thể dành thời gian viết một bài về vấn đề thread, tạo nhiều thread, rồi chờ thread này chạy xong hết rồi thread khác mới thực hiện. ví dụ như em có 5 ông gửi request cùng lúc, mà mỗi ông cần gửi 10 tin nhắn chẳng hạn. cảm ơn anh nhiều 🙂
LikeLike
Với case của bạn thì chỉ cần 5 thread chạy lần lượt là được, sao phải chờ nhỉ 😀
LikeLike
sao cái đệ quy “enumerator1.Current.DeepEquals(enumerator2.Current)” trong phần so sánh list mình không gọi lại được nhỉ. Nó báo ko có hàm “DeepEquals”
LikeLike
Bạn kiểm tra xem đã có viết hàm “DeepEquals(this object obj, object another)” để so sánh 2 object chưa nhé? Hàm đó nằm phía trên ấy.
LikeLike
Theo mình đoạn code trong foreach bạn sai (bắt đầu từ cấp độ 3)
var result = true;
foreach (var property in obj.GetType().GetProperties())
{
.
.
.
if (…) result = false;
}
Cần return hoặc break ngay sau khi phát hiện false. VD như:
var result = true;
foreach (var property in obj.GetType().GetProperties())
{
.
.
.
if (…) { result = false; break };
}
}
LikeLike
Cảm ơn góp ý của bạn 😀
LikeLike
mình mới băt đầu lập trình thôi. yêu cầu là so sánh 2 ảnh tìm ra điểm khác biệt và khoanh tròn chô khác biệt. Chỉ giáo mình với
LikeLike
cảm ơn anh. Chúc anh thành công
LikeLike
hổng dễ xí nào hết á @@
LikeLike
Còn 1 cách cũng khá dễ nữa đó là convert 2 object cần so sánh sang binary rồi so sánh
LikeLike
Nếu như trong class có 1 property là mảng hoặc struct thì xử lý như thế nào, bạn có thể hướng dẫn mình được không
LikeLike