每个班级的唯一标识符

3
我希望为每个类实现一个唯一的标识符(最好是静态的,不需要计算),但并非每个实例都需要。最明显的方法是在类中硬编码一个值,但保持值的唯一性成为了人工处理的任务,并不理想。
class Base 
{ 
    abstract int GetID();
}
class Foo: Base 
{ 
    int GetID() => 10; 
}
class Bar: Base 
{ 
    int GetID() => 20;
}

Foo foo1 = new Foo();
Foo foo2 = new Foo();
Bar bar  = new Bar();

foo1.GetID() == foo2.GetID();
foo1.GetID() != bar.GetID()

类名本身可以作为非常明显的唯一标识符,但我需要一个int(或固定长度的字节)。我将整个对象打包成字节,并使用该id在另一端解包时知道它是哪个类。
每次调用GetID()都对类名进行哈希处理似乎过于繁琐,只是为了获得一个ID号码。
我也可以制作一个enum作为查找表,但同样需要手动填充enum。
编辑:人们已经问了一些重要问题,所以我会把信息放在这里。
1.每个类都需要是独一无二的,而不是每个实例(这就是为什么已经有的重复问题没有回答这个问题的原因)。
2.ID值需要在多次运行之间持久存在。
3.值需要是固定长度的字节或int。变长字符串比如类名是不可接受的。
4.尽可能减少CPU负载(缓存结果或者使用基于程序集的元数据代替每次执行哈希操作)。
5.理想情况下,ID可以从静态函数获取。这意味着我可以制作一个静态查找函数,使其匹配ID和类别。
6.需要ID的不同类别数量并不是很大(<100),因此碰撞并不是主要问题。
编辑2:
由于人们怀疑这确实需要,我再提供一些具体信息。我正在编写一些与游戏相关的网络编码,将其分解为消息对象。每个不同的消息类型都是一个继承自MessageBase的类,并添加了自己的字段,这些字段将被发送。
MessageBase类有一个方法可以将自己打包成字节,并将消息标识符(即类ID)粘在前面。当在另一端对其进行解包时,我使用该标识符来知道如何解包字节。这导致了一些易于打包/解包的消息和非常少的开销(ID的几个字节,然后只是类属性值)。
目前,我在类中硬编码了一个ID号码,但看起来这不是最好的处理方法。
编辑3:这是我实现接受答案后的代码。
public class MessageBase
{
    public MessageID id { get { return GetID(); } }

    private MessageID cacheId;
    private MessageID GetID()
    {
        // Check if cacheID hasn't been intialised
        if (cacheId == null)
        {
            // Hash the class name
            MD5 md5 = MD5.Create();
            byte[] md5Bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(GetType().AssemblyQualifiedName));

            // Convert the first few bytes into a uint32, and create the messageID from it and store in cache
            cacheId = new MessageID(BitConverter.ToUInt32(md5Bytes, 0));
        }

        // Return the cacheId
        return cacheId;
    }
}

public class Protocol
{
    private Dictionary<Type, MessageID> messageTypeToId = new Dictionary<Type, int>();
    private Dictionary<MessageID, Type> idToMessageType = new Dictionary<int, Type>();
    private Dictionary<MessageID, Action<MessageBase>> handlers = new Dictionary<int, Action<MessageBase>>();

    public Protocol()
    {
        // Create a list of all classes that are a subclass of MessageBase this namespace
        IEnumerable<Type> messageClasses = from t in Assembly.GetExecutingAssembly().GetTypes()
                                           where t.Namespace == GetType().Namespace && t.IsSubclassOf(typeof(MessageBase))
                                           select t;

        // Iterate through the list of message classes, and store their type and id in the dicts
        foreach(Type messageClass in messageClasses)
        {
            MessageID = (MessageID)messageClass.GetField("id").GetValue(null);
            messageTypeToId[messageClass] = id;
            idToMessageType[id] = messageClass;
        }
    }
}

9
唯一浮现在脑海中的问题是:“为什么?” - Patrick Hofman
7
为什么不使用GUID? - Alex Riabov
2
为什么不使用类的 GUID 或 HashCode? - Emre Savcı
2
你可以使用类的元数据令牌。 - thehennyy
1
类名的SHA-256哈希值带有缓存?非常、非常不可能发生冲突,你可以编写一个单元测试来确保这一点……顺便问一下,你需要这个在运行之间稳定吗?如果不需要,那就意味着还有其他选项。 - Jon Skeet
显示剩余13条评论
3个回答

3
这里有一个建议。我使用了一个sha256字节数组,它保证具有固定的大小,并且几乎不可能发生碰撞。这可能太过于严谨,你可以轻松地将其替换为更小的东西。如果你需要担心版本差异或多个程序集中相同的类名,你也可以使用AssemblyQualifiedName而不是FullName。
首先,这是我所有的using语句。
using System;
using System.Collections.Concurrent;
using System.Text;
using System.Security.Cryptography;

接下来,一个静态缓存类型哈希对象,用于记住您的类型和生成的字节数组之间的映射关系。您不需要下面的Console.WriteLines,它们只是为了演示您不会一遍又一遍地计算它。
public static class TypeHasher
{
    private static ConcurrentDictionary<Type, byte[]> cache = new ConcurrentDictionary<Type, byte[]>();
    public static byte[] GetHash(Type type)
    {
        byte[] result;
        if (!cache.TryGetValue(type, out result))
        {
            Console.WriteLine("Computing Hash for {0}", type.FullName);
            SHA256Managed sha = new SHA256Managed();
            result = sha.ComputeHash(Encoding.UTF8.GetBytes(type.FullName));
            cache.TryAdd(type, result);
        }
        else
        {
            // Not actually required, but shows that hashing only done once per type
            Console.WriteLine("Using cached Hash for {0}", type.FullName);
        }

        return result;
    }
}

下一步是在对象上创建一个扩展方法,以便您可以请求任何东西的ID。当然,如果您有更合适的基类,则不需要将其放在对象本身上。
public static class IdExtension
{
    public static byte[] GetId(this object obj)
    {
        return TypeHasher.GetHash(obj.GetType());
    }
}

接下来,这是一些随机的类

public class A
{
}

public class ChildOfA : A
{
}

public class B
{
}

最后,这里是所有的东西放在一起。
public class Program
{
    public static void Main()
    {
        A a1 = new A();
        A a2 = new A();
        B b1 = new B();
        ChildOfA coa = new ChildOfA();
        Console.WriteLine("a1 hash={0}", Convert.ToBase64String(a1.GetId()));
        Console.WriteLine("b1 hash={0}", Convert.ToBase64String(b1.GetId()));
        Console.WriteLine("a2 hash={0}", Convert.ToBase64String(a2.GetId()));
        Console.WriteLine("coa hash={0}", Convert.ToBase64String(coa.GetId()));
    }
}

这是控制台输出。
Computing Hash for A
a1 hash=VZrq0IJk1XldOQlxjN0Fq9SVcuhP5VWQ7vMaiKCP3/0=
Computing Hash for B
b1 hash=335w5QIVRPSDS77mSp43if68S+gUcN9inK1t2wMyClw=
Using cached Hash for A
a2 hash=VZrq0IJk1XldOQlxjN0Fq9SVcuhP5VWQ7vMaiKCP3/0=
Computing Hash for ChildOfA
coa hash=wSEbCG22Dyp/o/j1/9mIbUZTbZ82dcRkav4olILyZs4=

另一方面,您可以使用反射来迭代库中的所有类型,并存储哈希到类型的反向字典。

不错的写作。大多数人一直建议使用哈希,这也是我预期的方向,但我很感激包含缓存以避免每次检索ID时重新计算哈希值的部分。也感谢提供示例用法和测试。最终,我编写了一些代码,通过所有MessageBase类进行迭代并创建查找字典,但美妙之处在于这也触发了ID的第一个哈希,因此所有后续的ID检索都被缓存了。 - Oliver

2

假设您可以通过在实例上调用GetType来获取Type,则可以轻松地缓存结果。这将减少问题,并解决如何为每种类型生成ID的问题。然后您会调用类似于以下内容的东西:

int id = typeIdentifierCache.GetIdentifier(foo1.GetType());

...或者使GetIdentifier接受object,并且可以调用GetType(),这样你就会得到

int id = typeIdentifierCache.GetIdentifier(foo1);

此时,所有细节都在类型标识符缓存中。

一个简单的选择是对完全限定的类型名称进行哈希(例如SHA-256,以确保稳定性并使发生碰撞的可能性非常小)。为了证明没有碰撞,您可以轻松编写一个单元测试,对程序集中的所有类型名称进行哈希,然后检查是否有重复项。(鉴于SHA-256的特性,甚至这可能过头了。)

这一切都假定类型都在单个程序集中。如果需要处理多个程序集,则可能需要对程序集限定名称进行哈希。


0

我还没有看到你回答这个问题,即是否需要在不同的运行之间保持相同的值,但如果你只需要一个类的唯一标识符,那么可以使用内置的简单GetHashCode方法:

class BaseClass
{
    public int ClassId() => typeof(this).GetHashCode();
}

如果你担心多次调用GetHashCode()的性能问题,那么首先,不要这样做,这是荒谬的微观优化,但如果你坚持,那就存储它。

GetHashCode()很快,这就是它的全部目的,作为一种快速比较哈希值的方法。

编辑: 经过一些测试,使用此方法在不同运行之间返回相同的哈希代码。我没有测试修改类之后的情况,我不知道如何对类型进行哈希处理的确切方法。


抱歉,大家回复得太快了 =P 它需要在不同的运行之间保持一致。我会把这个放到问题中。 - Oliver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接