Анонимные внутренние классы в качестве ключей в Java, но что в C#?

В Wicket есть так называемый MetaDataKey. Они используются для хранения введенной метаинформации в компонентах Wicket. Поскольку Wicket интенсивно использует сериализацию, разработчики Wicket решили, что простая идентификация объекта не будет надежной, и поэтому сделали MetaDataKey абстрактным классом, вынуждая вас создавать подкласс для каждого ключа, а затем проверять, является ли ключ экземпляром подкласса. (из исходного кода калитки):

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# (в котором отсутствуют анонимные внутренние классы), как бы я это сделал?


person cdmckay    schedule 25.01.2011    source источник
comment
Есть ли причина, по которой класс должен быть анонимным? Почему не работает обычный статический внутренний класс С#?   -  person Gabe    schedule 25.01.2011
comment
Гейб: Что-то вроде class FooKey : MetaDataKey<string> { }?   -  person cdmckay    schedule 25.01.2011


Ответы (2)


Проблема с сериализацией и идентификацией заключается в том, что процесс сериализации фактически создает клон исходного объекта. Большую часть времени,

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

с последующим

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

не может и не обеспечивает выполнение этого x == y (хотя должно быть так, что x.equals(y))

Если вы действительно хотите использовать идентичность, вам придется написать собственный код сериализации, который обеспечивает, чтобы чтение экземпляра вашего класса из потока фактически приводило к тому же экземпляру (как в синглтоне), который был написан. Это трудно сделать правильно, и я думаю, что принуждение разработчиков к простому объявлению волшебного ключа сделает API довольно сложным в использовании.

В настоящее время можно использовать enums и полагаться на виртуальную машину для обеспечения одноэлементного символа.

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

Недостатком является то, что вы не можете объявить, что вы enum наследуете от общего базового класса (хотя вы можете реализовать интерфейсы), поэтому вам придется вручную кодировать весь код поддержки, который MetaDataKey может предоставить вам снова и снова. снова. В приведенном выше примере все эти getType должны были быть предоставлены абстрактным базовым классом, но не могли, потому что мы использовали enum.

Что касается второй части вопроса... К сожалению, я не чувствую себя достаточно опытным в С#, чтобы ответить на этот вопрос (помимо уже упомянутого использования простого решения частного класса, уже приведенного в комментариях).

Тем не менее... (Изменить, чтобы ответить на вопросы, появившиеся в комментариях к ответу Бена) Одно из возможных решений для достижения чего-то подобного (в том смысле, что оно будет столь же полезным, как и решение 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:

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

Теперь все работает, как хотелось бы, но на вас легло бремя поддержки ключевых гидов. Я думаю, это то, чего Java-решение пытается избежать с помощью этого милого трюка с анонимным подклассом.

person Dirk    schedule 25.01.2011

Что касается обоснования подтипирования по сравнению с использованием эталонной идентичности, я так же в неведении, как и вы. Если бы мне пришлось угадывать, я бы сказал, что, поскольку ссылки в основном представляют собой причудливые указатели, их значение не обязательно гарантированно будет сохранено посредством сериализации/десериализации, и разумный способ имитации уникального идентификатора объекта - это то, что выбирает компилятор Java. для вызова определенного анонимного подкласса. Я не слишком хорошо знаком со спецификацией Java, поэтому не знаю, является ли это заданным поведением или деталями реализации различных компиляторов. Я могу быть совершенно не в курсе здесь.

В C# есть анонимные типы, но они сильно ограничены — в частности, они не могут наследоваться ни от чего, кроме System.Object, и не могут реализовывать интерфейсы. Учитывая это, я не понимаю, как эту конкретную технику можно перенести с Java.

Один из способов сохранить уникальность объекта с помощью сериализации может быть таким: базовый класс в стиле Wicket теоретически может сохранять закрытый член, который уникален во время выполнения и генерируется при построении, например System.Guid или System.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
}
person Ben    schedule 25.01.2011
comment
Круто, вот о чем я думал. Я думаю, что на самом деле это намного лучше, поскольку пользователю не нужно делать ничего странного, например, создавать подкласс для создания ключа. - person cdmckay; 25.01.2011
comment
Хм. Это может почти сработать, но вы не должны генерировать новые идентификаторы GUID в конструкторе (по сути, сериализованные, а затем десериализованные экземпляры класса не будут равны главному ключу на протяжении всего времени жизни процесса). Однако, я думаю, достаточно просто передать GUID в конструктор, чтобы ваше решение заработало. - person Dirk; 25.01.2011
comment
Подумав об этом еще раз ... Если у вас нет фиксированного постоянного значения GUID, я не думаю, что эта схема вообще переживет перезапуск процесса. - person Dirk; 25.01.2011
comment
Дирк, я не думаю, что конструктор вызывается BinaryFormatter при десериализации, поэтому Guid следует назначать только один раз. На самом деле, я никогда раньше не слышал, что следует избегать создания Guid в конструкторе - можете ли вы уточнить этот момент? - person Ben; 25.01.2011
comment
@Ben: Меня беспокоит не столько вызов конструктора. Но вам нужен стабильный GUID, который выдерживает ограничения процесса. Если у вас есть унифицированный класс MetaDataKey, единственным свойством, которое можно использовать для различения разных ключей (помимо связанного типа значения), является GUID. Если вы генерируете новый GUID главного ключа каждый раз, когда приложение запускается, вы не сможете правильно восстановить данные, которые были сериализованы предыдущим вызовом приложения (значения были сохранены с использованием ключей с другим GUID) - person Dirk; 25.01.2011
comment
@Dirk: Не будет ли подход подкласса страдать от этого недостатка? - person cdmckay; 25.01.2011
comment
@Dirk: я думаю, что могу вас неправильно понять; вот что я понимаю: это руководство будет создано один раз при создании ключа. Когда ключ сериализуется, значение guid будет включено. При десериализации исходное значение будет присвоено клонированному экземпляру — даже если был вызван конструктор (что, согласно Reflector, не так), новый guid будет перезаписан исходным значением. - person Ben; 25.01.2011
comment
@ Бен, иногда ТАК кажется неподходящей средой ... Я дополню свой ответ. - person Dirk; 25.01.2011