为什么通过序列化比较两个对象然后比较以下示例中的字符串来比较两个对象不是一个好习惯?
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
,所以比较的性能不会让我这么烦,因为我知道这可能是最大的一个-sides。此外,在Newtonsoft.Json的情况下,TypeNameHandling
属性设置为All
,可以处理无类型问题。
答案 0 :(得分:13)
主要问题是效率低下
作为一个例子想象一下这个等于函数
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是无类型,这意味着值比不以任何方式的形状或形式相等可能被误认为是相等的,并且在相反的值中,如果它们序列化,则由于格式化而相等的值可能不相等对于相同的值,这又是不安全和不稳定的
这种技术唯一的优点是程序员无需花费太多精力就可以实现
答案 1 :(得分: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型,我可以毫无问题地调用你的比较器。
答案 2 :(得分:4)
我想在开始时更正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 - &gt; B - &gt; A,它失败了。
不幸的是,这在链接在一起的列表或地图中很常见。
最糟糕的是,几乎没有一种有效的通用循环检测机制。
其次,与序列化的比较效率低下。
在成功编译结果之前,JSON需要反射和大量的类型判断。
因此,您的比较器将成为任何算法的严重瓶颈。
通常,即使在数千个记录案例中,JSON也被认为足够慢。
第三,JSON必须遍历每个属性。
如果你的对象链接到任何大对象,它将成为一场灾难。
如果您的对象链接到大文件怎么办?
因此,C#只是将实现留给用户。
在创建比较器之前,必须彻底了解他的课程。
比较需要良好的环路检测,提前终止和效率考虑。
通用解决方案根本不存在。
答案 3 :(得分:4)
你可能会继续为这个问题添加赏金,直到有人告诉你这样做是完全正常的。所以你明白了,不要犹豫利用NewtonSoft.Json库来保持代码简单。如果您的代码经过审核或其他人接管维护代码,您只需要一些好的论据来捍卫您的决定。
他们可能提出的一些反对意见及其反驳论点:
这是非常效率低下的代码!
当然,如果您在Dictionary或HashSet中使用该对象,特别是GetHashCode()会使您的代码变得非常慢。
最好的反驳论点是,在单元测试中,效率几乎不受关注。最典型的单元测试开始比实际执行需要更长的时间,是否需要1毫秒或1秒是不相关的。而且你可能很早就会发现一个问题。
您正在对未编写的库进行单元测试!
这当然是一个有效的问题,您实际上正在测试NewtonSoft.Json生成对象的一致字符串表示的能力。有理由对此感到担忧,特别是浮点值(浮点和双精度)从来都不是问题。还有some evidence图书馆作者不确定如何正确地做到这一点。
最好的反驳论点是广泛使用并维护得很好,作者多年来发布了许多更新。当您确保具有完全相同的运行时环境的完全相同的程序生成两个字符串(即不存储它)并且确保在禁用优化的情况下构建单元测试时,可以推断出浮点一致性问题。
您没有对需要测试的代码进行单元测试!
是的,如果类本身无法比较对象,则只会编写此代码。换句话说,它本身不会覆盖Equals / GetHashCode,也不会暴露比较器。因此,在单元测试中测试相等性是否为待测代码实际上不支持的功能。单元测试永远不应该做的事情,当测试失败时,你不能写错误报告。
计数器参数是为了使你需要来测试相等性来测试类的另一个特性,比如构造函数或属性setter。代码中的简单注释足以记录这一点。
答案 4 :(得分:1)
首先,我注意到你说&#34;序列化它们然后比较字符串。&#34;通常,普通的字符串比较不用于比较XML或JSON字符串,你必须比这更复杂。作为字符串比较的反例,请考虑以下XML字符串:
<abc></abc>
<abc/>
他们显然不字符串相等,但他们肯定&#34;意思是&#34;同一件事情。虽然这个例子可能看似人为,但事实证明,在很多情况下,字符串比较并不起作用。例如,空格和缩进在字符串比较中很重要,但在XML中可能不重要。
JSON的情况并没有那么好。你可以为此做类似的反例。
{ abc : "def" }
{
abc : "def"
}
同样,显然这些意思相同,但它们并不是字符串相等。
基本上,如果你正在进行字符串比较,你就会相信序列化程序总是以完全相同的方式序列化特定对象(没有任何添加的空格等),这最终会非常脆弱,尤其是鉴于我所知,大多数图书馆都没有提供任何此类保证。如果您在某个时候更新序列化库,并且序列化的方式有细微差别,那么这尤其成问题。在这种情况下,如果您尝试将已序列化的已保存对象与使用当前版本序列化的库进行比较,那么它将无法正常工作。
此外,正如您对代码本身的快速说明一样,&#34; ==&#34;运算符不是比较对象的正确方法。通常,&#34; ==&#34;测试引用相等,不对象相等。
对哈希算法的一个更快的偏离:它们作为一种平等测试手段的可靠程度取决于它们的抗冲突性。换句话说,给定两个不同的,不相等的对象,它们对相同值散列的概率是多少?相反,如果两个对象散列到相同的值,那么它们实际上相等的几率是多少?许多人理所当然地认为他们的哈希算法是100%抗冲突的(即,只有当它们相等时,两个对象将散列到相同的值),但这不一定是真的。 (一个特别着名的例子是MD5加密散列函数,其相对较差的碰撞阻力使其不适合进一步使用)。对于正确实现的散列函数,在大多数情况下,散列到相同值的两个对象实际上相等的概率足够高,不适合作为相等测试的手段,但是不能保证。
答案 5 :(得分:1)
这些是一些缺点:
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 }
答案 6 :(得分:1)
使用序列化进行对象比较,然后在以下情况下比较字符串表示无效:
当需要比较的类型中存在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
即使双值实际上彼此接近,并且在涉及计算的程序中,它将成为一个真正的问题,因为在多次除法和乘法运算之后精度损失,序列化不能提供处理这些案例的灵活性。
同时考虑到上述情况,如果一个人不想比较属性,它将面临向实际类引入序列化属性的问题,即使没有必要,也会导致代码污染或问题。必须实际使用该类型的序列化。
注意:这些是这种方法的一些实际问题,但我期待找到其他问题。
答案 7 :(得分:1)
答案 8 :(得分:1)
序列化用于存储对象或通过当前执行上下文之外的管道(网络)发送对象。不是在执行上下文中做某事。
某些序列化值可能不会被视为相等,实际上它们是:十进制“1.0”和整数“1”。
可以肯定的是,你可以像铲子一样吃东西,但不要因为你可能会伤牙!
答案 9 :(得分:0)
您可以使用System.Reflections
命名空间来获取实例的所有属性,例如this answer。使用 Reflection ,您不仅可以比较public
属性或字段(比如使用Json序列化),还可以比较一些private
,protected
等来增加计算速度。当然,很明显,如果两个对象不同,则不必比较实例的所有属性或字段(仅当对象的最后一个属性或字段不同时除外)。