使用序列化C#比较两个对象

本文关键字:两个 对象 序列化 比较 | 更新日期: 2023-09-27 17:57:26

为什么通过序列化两个对象来比较它们,然后像下面的例子中那样比较字符串不是一个好的做法?

public class Obj
{
    public int Prop1 { get; set; }
    public string Prop2 { get; set; }
}
public class Comparator<T> : IEqualityComparer<T>
{
    public bool Equals(T x, T y)
    {
        return JsonConvert.SerializeObject(x) == JsonConvert.SerializeObject(y);
    }
    public int GetHashCode(T obj)
    {
        return JsonConvert.SerializeObject(obj).GetHashCode();
    }
}
Obj o1 = new Obj { Prop1 = 1, Prop2 = "1" };
Obj o2 = new Obj { Prop1 = 1, Prop2 = "2" };
bool result = new Comparator<Obj>().Equals(o1, o2);

我已经测试过了,它很有效,它是通用的,所以它可以代表各种各样的对象,但我想问的是,这种比较对象的方法有哪些缺点?

我看到有人在这个问题中提出了这个建议,并获得了一些赞成票,但我不明白,如果有人只想比较两个对象的属性值,为什么这不被认为是最好的方法?

EDIT:我严格说的是Json序列化,而不是XML。

我之所以这么问,是因为我想为单元测试项目创建一个简单通用的Comparator,所以比较的性能不会太困扰我,因为我知道这可能是最大的缺点之一。此外,在Newtonsoft的情况下,可以使用来处理无类型问题。Json将TypeNameHandling属性设置为All

使用序列化C#比较两个对象

主要问题是效率低下

举个例子,想象一下这个Equals函数

public bool Equals(T x, T y)
{
    return x.Prop1 == y.Prop1
        && x.Prop2 == y.Prop2
        && x.Prop3 == y.Prop3
        && x.Prop4 == y.Prop4
        && x.Prop5 == y.Prop5
        && x.Prop6 == y.Prop6;
}

如果prop1不相同,那么其他5个比较就不需要检查,如果你用JSON这样做,你就必须将整个对象转换成JSON字符串,然后每次都比较字符串,这是串行化本身就是一项昂贵的任务。

接下来的问题是串行化是为通信而设计的,例如从内存到文件、通过网络等。如果你利用串行化进行比较,你可能会降低正常使用它的能力,也就是说,你不能忽略传输不需要的字段,因为忽略它们可能会破坏你的比较器。

具体来说,下一个JSON是Type less,这意味着比不在任何形状或形式下的值都相等的值可能会被误认为相等,而在另一方面,相等的值如果序列化为相同的值,则由于格式的原因,可能无法比较为相等,这再次是不安全和不稳定的

这种技术的唯一好处是,程序员只需很少的努力就可以实现

在有人告诉你做这件事没问题之前,你可能会一直给这个问题增加奖励。所以你明白了,不要犹豫,利用NewtonSoft。Json库使代码保持简单。如果你的代码被审查过,或者其他人接管了代码的维护,你只需要一些好的论据来为你的决定辩护。

他们可能提出的一些反对意见,以及他们的反驳:

这是非常低效的代码!

当然是这样,特别是如果您在Dictionary或HashSet中使用对象,GetHashCode()会使代码变得非常慢。

最好的反驳是要注意,在单元测试中,效率是无关紧要的。最典型的单元测试开始的时间比实际执行的时间长,无论是1毫秒还是1秒都无关紧要。还有一个你很可能很早就发现的问题。

你正在单元测试一个你没有写的库!

这当然是一个合理的担忧,你实际上是在测试NewtonSoft。Json生成对象的一致字符串表示的能力。有理由对此感到担忧,特别是浮点值(浮点和双精度)从来都不是问题。还有一些证据表明,图书馆的作者不确定如何正确地做到这一点。

最好的反驳是,该库被广泛使用和维护良好,作者多年来发布了许多更新。当您确保具有完全相同的运行时环境的完全相同的程序生成两个字符串时(即,不要存储它),并且您确保单元测试是在禁用优化的情况下构建的,则可以推断出浮点一致性问题。

您不是在对需要测试的代码进行单元测试!

是的,只有当类本身没有提供比较对象的方法时,您才会编写此代码。换句话说,它本身并不覆盖Equals/GetHashCode,也不公开比较器。因此,在单元测试中测试相等性是要测试的代码实际上并不支持的一个功能。单元测试永远不应该做的事情是,当测试失败时,你不能写错误报告。

反参数是为了说明需要来测试相等性,以测试类的另一个功能,如构造函数或属性设置器。代码中的一个简单注释就足以记录这一点。

通过将对象序列化为JSON,基本上将所有对象更改为另一种数据类型,因此应用于JSON库的所有内容都会对结果产生影响。

因此,如果其中一个对象中有一个类似[ScriptIgnore]的标记,您的代码将忽略它,因为它已从数据中省略。

此外,对于不相同的对象,字符串结果可能是相同的。就像这个例子一样。

static void Main(string[] args)
{
    Xb x1 = new X1()
    {
        y1 = 1,
        y2 = 2
    };
    Xb x2 = new X2()
    {
        y1 = 1,
        y2= 2
    };
   bool result = new Comparator<Xb>().Equals(x1, x2);
}
}
class Xb
{
    public int y1 { get; set; }
}
class X1 : Xb
{
    public short y2 { get; set; }
}
class X2 : Xb
{
    public long y2 { get; set; }
}

因此,正如您所看到的,x1的类型与x2不同,甚至y2的数据类型也不同,但json的结果是相同的。

除此之外,由于x1和x2都来自Xb类型,我可以毫无问题地调用您的比较器。

我想在开始时更正GetHashCode

public class Comparator<T> : IEqualityComparer<T>
{
    public bool Equals(T x, T y)
    {
        return JsonConvert.SerializeObject(x) == JsonConvert.SerializeObject(y);
    }
    public int GetHashCode(T obj)
    {
        return JsonConvert.SerializeObject(obj).GetHashCode();
    }
}

好的,接下来我们讨论这个方法的问题。


首先,它不适用于具有环形链接的类型。

如果您有一个像a->B->a这样简单的属性链接,它就会失败。

不幸的是,这在相互关联的列表或地图中非常常见。

最糟糕的是,几乎没有有效的通用循环检测机制。


其次,与序列化相比效率低下。

JSON在成功编译结果之前需要进行反射和大量类型判断。

因此,您的比较器将成为任何算法中的严重瓶颈。

通常,即使在数千条记录的情况下,JSON也被认为足够慢。


第三,JSON必须遍历每个属性。

如果你的对象链接到任何一个大对象,这将成为一场灾难。

如果你的对象链接到一个大文件怎么办?


因此,C#只是将实现留给用户。

在创建一个比较器之前,必须彻底了解自己的班级。

比较需要良好的环路检测、早期终止和效率考虑。

通用解决方案根本不存在。

以下是一些缺点:

a) 对象树越深,性能就越差。

b) new Obj { Prop1 = 1 } Equals new Obj { Prop1 = "1" } Equals new Obj { Prop1 = 1.0 }

c) new Obj { Prop1 = 1.0, Prop2 = 2.0 } Not Equals new Obj { Prop2 = 2.0, Prop1 = 1.0 }

首先,我注意到你说的是"序列化它们,然后比较字符串。"通常,普通的字符串比较不能用于比较XML或JSON字符串,你必须比这更复杂一点。作为字符串比较的反例,考虑以下XML字符串:

<abc></abc>
<abc/>

它们显然是而不是字符串相等,但它们绝对"意味着"相同的东西。虽然这个例子看起来有些做作,但事实证明,在相当多的情况下,字符串比较不起作用。例如,空白和缩进在字符串比较中很重要,但在XML中可能不重要。

JSON的情况并没有好到哪里去。你可以做类似的反例。

{ abc : "def" }
{
   abc : "def"
}

同样,很明显,这些意思是一样的,但它们不是字符串相等的。

从本质上讲,如果你在进行字符串比较,你就相信序列化程序总是以完全相同的方式序列化特定的对象(没有添加任何空格等),这最终会非常脆弱,尤其是据我所知,大多数库都没有提供任何这样的保证。如果您在某个时刻更新序列化库,并且它们执行序列化的方式存在细微差异,那么这将是特别有问题的;在这种情况下,如果您尝试将使用库的前一版本序列化的已保存对象与使用当前版本序列化的对象进行比较,那么它将不起作用。

此外,作为对代码本身的快速说明,"=="运算符不是比较对象的正确方法。通常,"=="测试引用相等性,而不是对象相等性。

关于哈希算法还有一个快速的话题:它们作为等式测试的一种手段的可靠性取决于它们的抗冲突性。换句话说,给定两个不同的、不相等的对象,它们散列到相同值的概率是多少?相反,如果两个对象散列到相同的值,那么它们实际上相等的几率是多少?很多人想当然地认为他们的哈希算法是100%抗冲突的(即,如果并且只有当两个对象相等时,它们才会哈希到相同的值),但这并不一定是真的。(一个特别著名的例子是MD5密码散列函数,其相对较差的抗冲突性使其不适合进一步使用)。对于一个正确实现的散列函数,在大多数情况下,散列到相同值的两个对象实际上相等的概率足够高,可以作为相等性测试的手段,但这并不能保证。

使用序列化然后比较字符串表示的对象比较在以下情况下无效:

当需要比较的类型中存在DateTime类型的属性时,

public class Obj
{
    public DateTime Date { get; set; }
}
Obj o1 = new Obj { Date = DateTime.Now };
Obj o2 = new Obj { Date = DateTime.Now };
bool result = new Comparator<Obj>().Equals(o1, o2);

即使对于在时间上创建的非常接近的对象,也会产生false,除非它们不共享完全相同的属性。


对于具有双倍或十进制值的对象,需要与Epsilon进行比较,以验证它们最终是否非常接近

public class Obj
{
    public double Double { get; set; }
}
Obj o1 = new Obj { Double = 22222222222222.22222222222 };
Obj o2 = new Obj { Double = 22222222222222.22222222221 };
bool result = new Comparator<Obj>().Equals(o1, o2);

即使双值非常接近,这也会返回false,在涉及计算的程序中,这将成为一个真正的问题,因为在多次除法和乘法运算后会失去精度,并且串行化无法提供处理这些情况的灵活性。


同样考虑到以上情况,如果不想比较一个属性,它将面临向实际类引入序列化属性的问题,即使这不是必要的,也会导致代码污染或问题,因此它将不得不对该类型实际使用序列化。

注意:这些是这种方法的一些实际问题,但我期待着找到其他问题。

对于单元测试,您不需要编写自己的比较器。:)

只需使用现代框架。例如,尝试FluentAssertions库

o1.ShouldBeEquivalentTo(o2);

序列化是为了存储对象或通过当前执行上下文之外的管道(网络)发送对象。不是为了在执行上下文中做某事。

一些序列化的值可能不被认为是相等的,事实上它们是:例如,十进制"1.0"和整数"1"。

当然,你可以喜欢用铲子吃饭,但你不能,因为你可能会摔坏你的牙齿!

您可以使用System.Reflections名称空间来获取实例的所有属性,如下所示。使用反射,您不仅可以比较public属性或字段(如使用Json序列化),还可以比较一些privateprotected等,以提高计算速度。当然,很明显,如果两个对象不同(不包括只有对象的最后一个属性或字段不同的示例),则不必比较实例的所有属性或字段。