与其使用二进制序列化,你可以使用
http://code.google.com/p/protobuf-net/,然后计算它的加密哈希值。据说protobuf比二进制序列化更紧凑(例如,参见
http://code.google.com/p/protobuf-net/wiki/Performance)。
我想补充说明的是,考虑到你实际上不需要序列化,最好使用反射并“浏览”对象来计算哈希值(就像各种序列化器“遍历”对象一样)。例如,请参见
Using reflection in C# to get properties of a nested object
经过深思熟虑,并听取@Jon的意见,我可以告诉你,我的“次要”想法(使用反射)非常非常困难,除非你愿意花一周时间编写一个对象解析器。是的,这是可行的……但在计算哈希之前,你会给数据什么表示?明确一点:
two strings
"A"
"B"
显然,"A","B" != "AB",""。但是MD5("A")与MD5("B")相结合等于MD5("AB")与MD5("")相结合。可能最好的方法是在前面添加长度(因此使用Pascal / BSTR表示法)。
而null值呢?它们有什么“序列化”值?另一个困难问题。显然,如果将字符串序列化为长度+字符串(以解决前一个问题),则可以将null简单地序列化为“null”(没有长度)...那对象呢?您会在前面加上对象类型ID吗?这肯定更好。否则,可变长度对象可能会像字符串一样造成混乱。
使用BinaryFormatter(甚至是protobuf-net)时,您不必真正将序列化的对象保存在某个地方,因为它们都支持流式处理...以下是示例。
public class Hasher : Stream
{
protected readonly HashAlgorithm HashAlgorithm;
protected Hasher(HashAlgorithm hash)
{
HashAlgorithm = hash;
}
public static byte[] GetHash(object obj, HashAlgorithm hash)
{
var hasher = new Hasher(hash);
if (obj != null)
{
var bf = new BinaryFormatter();
bf.Serialize(hasher, obj);
}
else
{
hasher.Flush();
}
return hasher.HashAlgorithm.Hash;
}
public override bool CanRead
{
get { throw new NotImplementedException(); }
}
public override bool CanSeek
{
get { throw new NotImplementedException(); }
}
public override bool CanWrite
{
get { return true; }
}
public override void Flush()
{
HashAlgorithm.TransformFinalBlock(new byte[0], 0, 0);
}
public override long Length
{
get { throw new NotImplementedException(); }
}
public override long Position
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
HashAlgorithm.TransformBlock(buffer, offset, count, buffer, offset);
}
}
static void Main(string[] args)
{
var list = new List<int>(100000000);
for (int i = 0; i < list.Capacity; i++)
{
list.Add(0);
}
Stopwatch sw = Stopwatch.StartNew();
var hash = Hasher.GetHash(list, new MD5CryptoServiceProvider());
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
我定义了一个
Hasher
类,该类接收对象的序列化数据(逐个部分)并以“流模式”计算哈希值。内存使用为O(1)。时间复杂度显然为O(n)(其中n是序列化对象的“大小”)。
如果您想使用protobuf(但请注意,对于复杂对象,它需要使用其属性标记(或WCF属性等)),
public static byte[] GetHash<T>(T obj, HashAlgorithm hash)
{
var hasher = new Hasher(hash);
if (obj != null)
{
ProtoBuf.Serializer.Serialize(hasher, obj);
hasher.Flush();
}
else
{
hasher.Flush();
}
return hasher.HashAlgorithm.Hash;
}
唯一的“大”区别在于protobuf不会“Flush”流,所以我们必须这样做,并且它确实希望根对象是有类型的,而不是简单的“对象”。
哦...关于你的问题:
如何序列化对象?它必须快速,不消耗太多内存。同时它必须可靠地总是以相同的方式序列化。如果我使用.NET默认序列化,我真的能确定如果实际数据相同,创建的二进制流总是相同的吗?我怀疑。
List<int> l1 = new List<int>();
byte[] bytes1, bytes2;
using (MemoryStream ms = new MemoryStream())
{
new BinaryFormatter().Serialize(ms, l1);
bytes1 = ms.ToArray();
}
l1.Add(0);
l1.RemoveAt(0);
using (MemoryStream ms = new MemoryStream())
{
new BinaryFormatter().Serialize(ms, l1);
bytes2 = ms.ToArray();
}
Debug.Assert(bytes1.Length == bytes2.Length);
假设这样说:调试断言会失败。这是因为List“保存”一些内部状态(例如版本),这使得二进制序列化和比较变得非常困难。最好使用“可编程”序列化器(如proto-buf)。您告诉它要序列化哪些属性/字段,它就将它们序列化。
那么有没有一种不需要花费太长时间实现的替代序列化方式呢?
Proto-buf...或DataContractSerializer(但速度相对较慢)。正如您所想象的那样,数据序列化并不存在万能的解决方案。