Информация о сериализации типов кеша Json.NET?

В .NET-мире, когда дело доходит до сериализации объектов, оно обычно идет на проверку полей и свойств объекта во время выполнения. Использование отражения для этого задания обычно медленное и нежелательно при работе с большими наборами объектов. Другим способом является использование IL emit или построение деревьев выражений, которые обеспечивают значительное увеличение производительности за счет отражения. И последнее - это самые современные библиотеки, которые выбирают при работе с сериализацией. Однако для создания и испускания ИЛ во время выполнения требуется время, и инвестиции выплачиваются только тогда, когда эта информация кэшируется и повторно используется для объектов того же типа.

При использовании Json.NET мне непонятно, какой метод, описанный выше, используется, и если последний действительно используется, используется ли кеширование.

Например, когда я делаю:

JsonConvert.SerializeObject(new Foo { value = 1 });

Создает ли Json.NET информацию о доступе к члену Foo и кэш для повторного использования позже?

Ответ 1

Json.NET кэширует информацию о сериализации типов внутри своих IContractResolver классов DefaultContractResolver и CamelCasePropertyNamesContractResolver. Если вы не укажете пользовательский обработчик контракта, эта информация кэшируется и используется повторно.

Для DefaultContractResolver поддерживается глобальный статический экземпляр, который Json.NET использует всякий раз, когда приложение не указывает свой собственный преобразователь контрактов. CamelCasePropertyNamesContractResolver, с другой стороны, поддерживает статические таблицы, которые являются общими для всех экземпляров. (Я полагаю, что несоответствие обусловлено устаревшими проблемами; подробности см. в здесь.)

Оба эти типа предназначены для обеспечения полной безопасности потоков, поэтому совместное использование потоков не должно быть проблемой.

Если вы решите создать собственный обработчик контрактов, то информация о типах будет кэшироваться и использоваться повторно только в том случае, если вы кэшируете и повторно используете сам экземпляр распознавателя контрактов. Таким образом, Newtonsoft рекомендует:

Для производительности вы должны создать определитель контракта один раз и повторно использовать экземпляры, когда это возможно. Разрешение контрактов выполняется медленно, и реализации IContractResolver обычно кэшируют контракты.

Одна из стратегий обеспечения кэширования в подклассе DefaultContractResolver заключается в том, чтобы сделать его конструктор защищенным или закрытым и предоставить глобальный статический экземпляр. (Конечно, это уместно только в том случае, если распознаватель "не имеет состояния" и всегда будет возвращать одинаковые результаты.) Например, вдохновленный этим вопросом, здесь приведен случай, когда необходимо подчеркнуть решатель контрактов:

public class PascalCaseToUnderscoreContractResolver : DefaultContractResolver
{
    protected PascalCaseToUnderscoreContractResolver() : base() { }

    // As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
    // http://www.newtonsoft.com/json/help/html/ContractResolver.htm
    // http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
    // "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
    static PascalCaseToUnderscoreContractResolver instance;

    // Using an explicit static constructor enables lazy initialization.
    static PascalCaseToUnderscoreContractResolver() { instance = new PascalCaseToUnderscoreContractResolver(); }

    public static PascalCaseToUnderscoreContractResolver Instance { get { return instance; } }

    static string PascalCaseToUnderscore(string name)
    {
        if (name == null || name.Length < 1)
            return name;
        var sb = new StringBuilder(name);
        for (int i = 0; i < sb.Length; i++)
        {
            var ch = char.ToLowerInvariant(sb[i]);
            if (ch != sb[i])
            {
                if (i > 0) // Handle flag delimiters
                {
                    sb.Insert(i, '_');
                    i++;
                }
                sb[i] = ch;
            }
        }
        return sb.ToString();
    }

    protected override string ResolvePropertyName(string propertyName)
    {
        return PascalCaseToUnderscore(propertyName);
    }
}

Что бы вы использовали, как:

var json = JsonConvert.SerializeObject(someObject, new JsonSerializerSettings { ContractResolver = PascalCaseToUnderscoreContractResolver.Instance });

(N.B. - полезность этого конкретного преобразователя была уменьшена с введением SnakeCaseNamingStrategy. Он был оставлен только в качестве иллюстративного примера.)

Если потребление памяти является проблемой и по какой-либо причине вам необходимо минимизировать объем памяти, постоянно занимаемый кэшированными контрактами, вы можете создать свой собственный локальный экземпляр DefaultContractResolver (или некоторый пользовательский подкласс), сериализовать его, а затем немедленно удалите все ссылки на него, например:

public class JsonExtensions
{
    public static string SerializeObjectNoCache<T>(T obj, JsonSerializerSettings settings = null)
    {
        settings = settings ?? new JsonSerializerSettings();
        if (settings.ContractResolver == null)
            // To reduce memory footprint, do not cache contract information in the global contract resolver.
            settings.ContractResolver = new DefaultContractResolver();
        return JsonConvert.SerializeObject(obj, settings);
    }
}

Большая часть кэшированной памяти контрактов со временем будет собираться мусором. Конечно, при этом производительность сериализации может существенно снизиться.

Для получения дополнительной информации см. раздел Советы по повышению производительности: повторное использование контрактного резолвера.