Series C# hay ho: So sánh 2 object trong C# (Deep compare)

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.

Are-You-Up-For-The-Challenge

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:

  1. Với những class khó quá, hãy tự viết hàm Equals, cũng không nhiều lắm
  2. 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.

img2

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.

12 thoughts on “Series C# hay ho: So sánh 2 object trong C# (Deep compare)”

  1. 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

    Like

  2. 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 🙂

    Like

  3. 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”

    Like

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

    Like

  5. 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

    Like

  6. 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

    Like

Leave a comment