【问题标题】:Compare two objects using serialization C#使用序列化 C# 比较两个对象
【发布时间】:2016-07-16 12:31:46
【问题描述】:

为什么通过序列化两个对象来比较两个对象,然后像下面的示例那样比较字符串不是一个好习惯?

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);

我已经对其进行了测试并且它有效,它是通用的,因此它可以代表各种各样的对象,但我要问的是这种比较对象的方法有哪些缺点?

我已经看到它已在 this question 中提出建议,并且收到了一些赞成票,但我不明白为什么这不是最好的方法,如果有人只想比较两个对象的属性值?

编辑:我说的是 Json 序列化,而不是 XML。

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

【问题讨论】:

  • 由于您已经在使用 Json.NET,它提供了一个 API 方法JToken.DeepEquals() 用于比较两个序列化对象。
  • 你需要用一个新的哈希器替换return obj.GetHashCode(),默认的哈希器使用与Equals相同的逻辑,因为你改变了equals的行为,现在哈希不正确,因为许多进程只在之后调用Equals检查哈希是否相等,您可能会得到非常奇怪的结果

标签: c# json unit-testing serialization comparison


【解决方案1】:

主要问题是效率低下

举个例子,想象一下这个 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 是无类型的,这意味着比任何形状或形式不相等的值可能会被误认为是相等的,并且在另一方面,如果它们序列化,则相等的值可能由于格式化而不能比较为相等到相同的值,这又是不安全和不稳定的

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

【讨论】:

  • 我只为 Newtonsoft 知道这一点,但肯定有其他序列化库:TypeNameHandling 消除了无类型问题,因此它适用于同一项目中的类。
  • @meJustAndrew 如果您不使用 JSON 并说使用 XML,那么类型问题就会消失,但字符串大小会增加,更不用说 XML 有多种方法来定义相同的对象所说的属性以不同的顺序会导致其他问题,如果您不关心使用序列化进行传输并且效率并不重要,那么确实没有任何理由不使用简单但次优的方法
  • ps 用英语写作而不是美国语不算错字;)
  • 好的,但是考虑像这样使用 Newtonsoft,唯一的缺点是性能?
  • 您可以在二档驾驶汽车时速 60 英里,因为它比使用换档更容易,只要您不关心效率和性能,那是您的选择。但你不会让任何人推荐它
【解决方案2】:

您可能会继续在问题中添加赏金,直到有人告诉您这样做就可以了。所以你明白了,不要犹豫,利用 NewtonSoft.Json 库来保持代码简单。如果您的代码曾经被审查过或者如果其他人接管了代码的维护,您只需要一些好的论据来捍卫您的决定。

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

这是非常低效的代码!

确实如此,尤其是 GetHashCode() 如果您曾经在 Dictionary 或 HashSet 中使用该对象,它会使您的代码变得非常缓慢。

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

您正在对不是您编写的库进行单元测试!

这当然是一个有效的问题,您实际上是在测试 NewtonSoft.Json 生成对象的一致字符串表示的能力。有理由对此感到担忧,特别是浮点值(float 和 double)从来都不是问题。还有some evidence库作者不确定如何正确操作。

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

您没有对需要测试的代码进行单元测试!

是的,如果类本身无法比较对象,您只会编写此代码。换句话说,它本身不会覆盖 Equals/GetHashCode 并且不会公开比较器。因此,在单元测试中测试相等性会锻炼待测试代码实际上不支持的功能。单元测试不应该做的事情,当测试失败时你不能写错误报告。

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

【讨论】:

  • 三年后读到这个答案,我觉得有必要再次投票。与此同时,支持这种方法的库像deep equal 一样发展壮大,我仍然相信在许多情况下这是正确的做法。
【解决方案3】:

我想更正开头的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# 只是将实现留给用户。

在创建比较器之前,必须彻底了解他的类。

比较需要良好的循环检测、提前终止和效率考虑。

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

【讨论】:

  • 你已经明白了,至于循环链接,存在 ReferenceLoopHandling 所以它可以与选项 Ignore 一起使用,只是不要序列化对象with 再次指向自身,所以在这种情况下它会起作用,而你第二次提到的是序列化很慢,这是真的。为什么我问这个问题是因为我想在单元测试项目中制作一个自定义比较器,所以它不会打扰我它是否会延迟第二次运行。它还为我节省了几天的编程时间来制作自定义比较器。为了澄清,我也应该将其添加到问题中。
【解决方案4】:

通过将您的对象序列化为 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 类型,我可以毫无问题地调用您的比较器。

【讨论】:

  • 正如另一个答案的评论:我知道这仅适用于 Newtonsoft,但肯定有其他序列化库:TypeNameHandling 消除了无类型问题,因此它可以正常工作对于同一项目中的课程。
【解决方案5】:

这些是一些缺点:

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 }

【讨论】:

  • 在其他答案的评论中:我知道这仅适用于 Newtonsoft,但肯定有其他序列化库:TypeNameHandling 消除了无类型问题,因此它可以正常工作对于同一项目中的类。在这种情况下,只有性能有效,但谢谢!
【解决方案6】:

首先,我注意到您说“序列化它们然后比较字符串”。一般来说,普通的字符串比较适用于比较 XML 或 JSON 字符串,你必须比这更复杂一点。作为字符串比较的反例,请考虑以下 XML 字符串:

<abc></abc>
<abc/>

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

对于 JSON,情况并没有那么好。你可以为此做类似的反例。

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

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

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

此外,作为对代码本身的快速说明,“==”运算符不是比较对象的正确方法。一般来说,"==" 测试 reference 相等,not 对象相等。

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

【讨论】:

  • 将 Newtonsoft.Json 视为库,因此 XML 问题不适用于这种情况。此外,正如我在我的问题中编写的比较器,它将仅用于一个库来比较两个对象,因此库过时的情况不是问题。在这种情况下,您的所有答案都不适用于该问题。
  • 我不同意。正如我在帖子中提到的,该问题适用于 both XML JSON。您不能只做简单的字符串相等并期望它按您期望的那样工作。要正确比较 XML 或 JSON 字符串,您必须考虑它们的含义而不是只是字符串相等。您的算法可能在一般情况下工作的唯一方法是,如果您可以保证序列化程序将始终完全 i> 格式一致,文档没有提供这样的保证。
【解决方案7】:

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

当需要比较的类型中存在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,即使双精度值非常接近,在涉及计算的程序中,这将成为一个真正的问题,因为多次除法和乘法运算后精度损失,并且序列化不提供处理这些情况的灵活性。


同样考虑到上述情况,如果不想比较一个属性,就会面临在实际类中引入序列化属性的问题,即使没有必要也会导致代码污染或出现问题将不得不实际使用该类型的序列化。

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

【讨论】:

    【解决方案8】:

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

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

    o1.ShouldBeEquivalentTo(o2);
    

    【讨论】:

      【解决方案9】:

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

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

      当然,你可以像用铲子吃饭一样,但你不能这样做,因为你可能会折断牙齿!

      【讨论】:

        【解决方案10】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-05-17
          • 2019-03-09
          • 2015-04-22
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多