1

在 Wicket 中,他们有一个叫做MetaDataKey的东西。这些用于在 Wicket 组件中存储类型化的元信息。由于 Wicket 大量使用序列化,Wicket 设计者认为简单的对象标识不可靠,因此将 MetaDataKey 设为抽象类,迫使您为每个键创建一个子类,然后检查该键是否是子类的实例(来自 Wicket 源代码):

public boolean equals(Object obj) {
    return obj != null && getClass().isInstance(obj);
}

因此,要创建一个密钥并存储一些东西,我会做这样的事情:

private final static MetaDataKey<String> foo = new MetaDataKey<String>() {};

...

component.setMetaData(foo, "bar");

首先,为什么使子类型比在序列化下使用对象标识更好?

其次,如果我想在 C# 中创建一个类似的工具(缺少匿名内部类),我该怎么做?

4

2 回答 2

2

序列化和身份的问题在于,序列化过程实际上创建了原始对象的克隆。大多数时候,

SomeThing x = ...;
ObjectOutputStream oos = ...;
oos.writeObject(x);

其次是

ObjectInputStream ois = ...;
SomeThing y = (SomeThing)ois.readObject()

不能也不会强制执行x == y(尽管应该是这样,但x.equals(y)

如果您真的想使用身份,则必须编写自定义序列化代码,这会强制从流中读取您的类的实例实际上会产生与所编写的实例相同的(如在单例中)。这很难做到,而且我认为,强迫开发人员这样做只是为了声明一个魔法密钥会使 API 很难使用。

如今,可以使用enums,并依靠 VM 来强制执行单例字符。

enum MyMetaDataKey implements HyptheticalMetaDataKeyInterface {

    TITLE(String.class),
    WIDTH(Integer.class);

    private final Class<?> type;

    private MyMetaDataKey(Class<?> t) { type = t; }
    public Class<?> getType() { return type; }
}

缺点是,你不能声明你的枚举继承自公共基类(你可以让它实现接口),所以你必须手动编写整个支持代码,这MetaDataKey可能会一遍又一遍地为你提供. 在上面的示例中,所有这些getType都应该由抽象基类提供,但它不能,因为我们使用了enum.

至于问题的第二部分......不幸的是,我觉得 C# 不够熟练来回答这个问题(除了已经提到的使用评论中已经给出的普通私有类解决方案)。

也就是说......(编辑以回答出现在 Ben 的回答的评论中的问题)实现类似事情的一种可能的解决方案(从某种意义上说,它与 Java 解决方案一样可用):

[Serializable]
public class MetaDataKey<T> {

    private Guid uniqueId;
    private Type type;

    public MetaDataKey(Guid key, Type type) {
        this.uniqueId;
        this.type = type;
    }

    public override boolean Equals(object other) {
        return other is MetaDataKey && uniqueId == ((MetaDataKey)other).uniqueId;
    }
}

这可以用于

class MyStuff {
    private static MetaDataKey<String> key = new MetaDataKey<String>(new Guid(), typeof(String));
}

请忽略任何违反 C# 语言的行为。我用它太久了。

这看起来像是一个有效的解决方案。然而,问题在于key常量的初始化。如果像上面的示例那样完成,每次启动应用程序时,都会创建一个新的 Guid 值并将其用作MyStuff的元数据值的标识符。因此,例如,如果您有一些来自程序先前调用的序列化数据(例如,存储在文件中),它将具有具有不同 Guid 值MyStuff的元数据键的键。一个有效的,在反序列化之后,任何请求

 String myData = magicDeserializedMetaDataMap.Get(MyStuff.key);

会失败——仅仅因为Guids不同。因此,为了使示例工作,您必须具有持久的预定义 Guid:

 class MyStuff {
     private static Guid keyId = new Guid("{pre-define-xyz}");
     private static MetaDataKey<String> key = new MetaDataKey<String>(keyId, typeof(String));
 }

现在,一切都如您所愿,但维护关键 Guid 的重担已经落在了您身上。我认为,这是 Java 解决方案试图通过这个可爱的匿名子类技巧来避免的。

于 2011-01-25T01:42:07.160 回答
1

至于子类型化与使用参考身份的基本原理,我和你一样在黑暗中。如果我不得不猜测,我会说由于引用基本上是花哨的指针,它们的值不一定能通过序列化/反序列化来保证保留,而模拟唯一对象 ID 的一种聪明方法是使用 Java 编译器选择的任何内容调用特定的匿名子类。我不太熟悉 Java 规范,所以我不知道这是指定的行为还是各种编译器的实现细节。我可能在这里完全偏离轨道。

C# 具有匿名类型,但它们受到严格限制——特别是,它们不能从 System.Object 以外的任何东西继承,并且不能实现任何接口。鉴于此,我看不出如何从 Java 移植这种特殊技术。

一种通过序列化来保持对象唯一性的方法是这样的:理论上,Wicket 风格的基类可以保留一个运行时唯一且在构造时生成的私有成员,例如获取对象的值的aSystem.Guid或 aSystem.IntPtr处理。该值可以(反)序列化并用作引用相等的替代。

[Serializable]
public class MetaDataKey<T>
{
    private Guid id;

    public MetaDataKey(...)
    {
        this.id = Guid.NewGuid();
        ....
    }

    public override bool Equals(object obj)
    {
        var that = obj as MetaDataKey<T>;
        return that != null && this.id == that.id;
    }
}

编辑 这里是如何通过保存对象的实际参考值来做到这一点;这需要更少的内存,并且更符合引用相等的概念。

using System.Runtime.InteropServices;

[Serializable]
public class AltDataKey<T>
{
    private long id;  // IntPtr is either 32- or 64-bit, so to be safe store as a long.

    public AltDataKey(...)
    {
        var handle = GCHandle.Alloc(this);
        var ptr = GCHandle.ToIntPtr(handle);

        id = (long)ptr;

        handle.Free();
    }

    // as above
}
于 2011-01-25T01:42:59.727 回答