每个班级的唯一ID

时间:2018-06-25 09:50:51

标签: c#

我想要每个类实现的唯一ID(最好是静态的,无需计算),而不是实例。最明显的方法是在类中对值进行硬编码,但是保持值唯一对人类来说是一项任务,并不理想。

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号而进行的不必要的处理。

我还可以将枚举作为查找,但是再次需要手动填充枚举。

编辑:人们一直在问重要问题,所以我将信息放在这里。

  1. 需要在每个班级而不是在每个实例上唯一(这就是为什么所标识的重复问题不能回答这一问题)。

  2. ID值必须在两次运行之间保持不变。

  3. 值必须为固定长度的字节或int。不能接受长度可变的字符串,例如类名。

  4. 需要尽可能减少CPU负载(缓存结果或使用基于程序集的元数据而不是每次都进行哈希处理)。

  5. 理想情况下,可以从静态函数中检索ID。这意味着我可以创建一个将ID与类匹配的静态查找函数。

  6. 需要ID的不同类的数目不是很大(<100),因此冲突不是主要问题。

EDIT2:

更多颜色,因为人们怀疑这确实是必需的。我愿意接受另一种方法。

我正在为游戏编写一些网络代码,并将其分解为消息对象。每种不同的消息类型都是一个从MessageBase继承的类,并添加了将发送的自己的字段。

MessageBase类具有一种将自身打包为字节的方法,并且在其前面粘贴了一个消息标识符(类ID)。在另一端解压缩时,我使用标识符来知道如何解压缩字节。这样会导致一些易于打包/解压缩的消息和很少的开销(用于ID的字节数少,然后是类属性值)。

当前,我在类中对ID号进行了硬编码,但这似乎并不是最好的处理方式。

EDIT3:这是实现接受的答案后的代码。

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

3 个答案:

答案 0 :(得分:1)

鉴于您可以通过在实例上调用Type来获得GetType,因此可以轻松地缓存结果。这就减少了解决如何为每种类型生成ID的问题。然后,您会拨打类似的电话:

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

...或让GetIdentifier接受object,并且 it 可以呼叫GetType(),让您离开

int id = typeIdentifierCache.GetIdentifier(foo1);

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

一个简单的选择是对完全限定的类型名称进行哈希处理(例如SHA-256,以确保稳定性并避免碰到冲突)。为了证明您没有冲突,您可以轻松地编写一个运行于程序集中所有类型名称并对其进行哈希处理的单元测试,然后检查是否存在重复项。 (即使考虑到SHA-256的性质,这可能也算是过大了。)

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

答案 1 :(得分:1)

这是一个建议。我使用了sha256字节数组,该数组保证为固定大小,从天文学角度讲不可能发生冲突。那可能太过分了,您可以轻松地用较小的东西代替它。如果您需要担心版本差异或多个程序集中的相同类名,也可以使用AssemblyQualifiedName而不是FullName

首先,这是我的全部用处

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=

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

答案 2 :(得分:0)

没有看到您回答以下问题:是否需要在不同的运行之间保留相同的值,但是如果您所需的只是一个类的唯一ID,则使用内置的简单GetHashCode方法:

regex

如果您担心多次调用((\d[., \d]+)(Kč|Eur)) 会导致性能下降,那么首先不要这样做,这是荒谬的微优化,但是如果您坚持要进行存储,就可以存储它。

re.findall("""((\d[., \d]+)(Kč|Eur))""","Letenky od 12 932 Kč",flags=re.IGNORECASE) 是一种快速的方法,即它的全部用途,是一种比较哈希值的快速方法。

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