Anonymous inner classes as keys in Java, but what in C#?
In Wicket, they have something called a MetaDataKey. These are used to store typed meta information in Wicket components. Since Wicket makes heavy use of serialization, the Wicket designers decided that simple object identity would not be reliable and so made MetaDataKey an abstract class, forcing you to create a subclass for each key a开发者_如何学编程nd then check to see if the key is an instance of subclass (from the Wicket source code):
public boolean equals(Object obj) {
return obj != null && getClass().isInstance(obj);
}
Thus, to create a key and store something I would do something like this:
private final static MetaDataKey<String> foo = new MetaDataKey<String>() {};
...
component.setMetaData(foo, "bar");
First, why would making a subtype work better than using object identity under serialization?
Second, if I wanted to create a similar facility in C# (which lacks anonymous inner classes), how would I go about it?
The problem with serialization and identity is, that the serialization process actually creates a clone of the original object. Most of the time,
SomeThing x = ...;
ObjectOutputStream oos = ...;
oos.writeObject(x);
followed by
ObjectInputStream ois = ...;
SomeThing y = (SomeThing)ois.readObject()
cannot and does not enforce that x == y
(though it should be the case, that x.equals(y)
)
If you really wanted to go with identity, you'd have to write custom serialization code, which enforces, that reading an instance of your class from the stream yields actually the same (as in singleton) instance that was written. This is hard to get right, and I think, forcing developers to do that simply to declare a magic key would make the API quite hard to use.
Nowadays, one could use enum
s, and rely on the VM to enforce the singleton character.
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; }
}
The disadvantage is, that you cannot declare you enum to inherit from the common base class (you can have it implement interfaces, though), so you would have to manually code the entire support code, which MetaDataKey
might provide for you, over and over again. In the example above, all this getType
should have been provided by an abstract base class, but it couldn't because we used enum
.
As to the second part of the question... Unfortunately, I don't feel proficient enough in C# to answer that (besides the already mentioned use a plain private class solution already given in the comments).
That said... (Edit to answer the questions which appeared in the comments on Ben's answer) One possible solution to achieve something similar (in the sense, that it would be just as usable as the Java solution):
[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;
}
}
which may be used as in
class MyStuff {
private static MetaDataKey<String> key = new MetaDataKey<String>(new Guid(), typeof(String));
}
Please ignore any violations of the C#-language. It's too long since I used it.
This may look like a valid solution. The problem, however, lies in initialization of the key
constant. If it is done like in the example above, each time, the application is started, a new Guid value is created and used as the identifier for MyStuff
's meta-data value. So if, say, you have some serialized data from a previous invocation of the program (say, stored in a file), it will have keys with a different Guid value of MyStuff
's meta-data key. An effectively, after deserialization, any request
String myData = magicDeserializedMetaDataMap.Get(MyStuff.key);
will fail -- simply because the Guids differ. So, in order to make the example work, you have to have persistent pre-defined Guids:
class MyStuff {
private static Guid keyId = new Guid("{pre-define-xyz}");
private static MetaDataKey<String> key = new MetaDataKey<String>(keyId, typeof(String));
}
Now, things work as desired, but the burden of maintaining the key Guids has come upon you. This is, I think, what the Java solution tries to avoid with this cute anonymous subclass trick.
As far as the rationale for subtyping vs using reference identity, I'm as much in the dark as you. If I had to guess, I'd say that as references are basically fancy pointers, their value isn't necessarily guaranteed to be retained through serialization/deserialization, and a clever way to simulate a unique object ID is with whatever the Java compiler chooses to call a particular anonymous subclass. I'm not too conversant with the Java spec, so I don't know if this is specified behavior or an implementation detail of the various compilers. I may be entirely off track here.
C# has anonymous types, but they are severely limited - in particular, they cannot inherit from anything other than System.Object and can implement no interfaces. Given that, I don't see how this particular technique can be ported from Java.
One way to go about retaining object uniqueness through serialization could go thusly: The Wicket-style base class could, in theory, retain a private member that is runtime-unique and generated on construction, like a System.Guid
or a System.IntPtr
that gets the value of the object's handle. This value could be (de)serialized and used as a stand-in for reference equality.
[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;
}
}
EDIT Here's how to do it by saving the object's actual reference value; this takes a bit less memory, and is slightly more true to the notion of reference-equality.
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
}
精彩评论