Я использую библиотеку реактивного программирования R3 и Newtonsoft
. Я пытаюсь сохранить данные объекта в файл, чтобы позже можно было их прочитать. Запись просто работает без проблем. Но при чтении файла выдает ошибку
Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: {. Path 'resourceList[0].Count', line 5, position 16.
{
"resourceList": [
{
"Name": "banana",
"Count": {
"EqualityComparer": {},
"CurrentValue": 10,
"HasObservers": false,
"IsCompleted": false,
"IsDisposed": false,
"IsCompletedOrDisposed": false,
"Value": 10
}
},
{
"Name": "apple",
"Count": {
"EqualityComparer": {},
"CurrentValue": 3,
"HasObservers": false,
"IsCompleted": false,
"IsDisposed": false,
"IsCompletedOrDisposed": false,
"Value": 3
}
}
],
"observableResourceList": [
{
"Name": "oBanana",
"Count": {
"EqualityComparer": {},
"CurrentValue": 5,
"HasObservers": false,
"IsCompleted": false,
"IsDisposed": false,
"IsCompletedOrDisposed": false,
"Value": 5
}
},
{
"Name": "oApple",
"Count": {
"EqualityComparer": {},
"CurrentValue": 2,
"HasObservers": false,
"IsCompleted": false,
"IsDisposed": false,
"IsCompletedOrDisposed": false,
"Value": 2
}
}
],
"number": {
"EqualityComparer": {},
"CurrentValue": 9,
"HasObservers": false,
"IsCompleted": false,
"IsDisposed": false,
"IsCompletedOrDisposed": false,
"Value": 9
},
"word": {
"EqualityComparer": {},
"CurrentValue": "word string",
"HasObservers": false,
"IsCompleted": false,
"IsDisposed": false,
"IsCompletedOrDisposed": false,
"Value": "word string"
}
}
Также в файл попадает много ненужной информации из ReactiveProperty
, я стараюсь сохранять только ту часть, которая мне нужна. Мне удалось записать часть файла с помощью JsonConverter
, но я все еще не могу его правильно прочитать. Я получаю ту же ошибку
{
"resourceList": [
{
"Name": "banana",
"Count": {
"Value": 10
}
},
{
"Name": "apple",
"Count": {
"Value": 3
}
}
],
"observableResourceList": [
{
"Name": "oBanana",
"Count": {
"Value": 5
}
},
{
"Name": "oApple",
"Count": {
"Value": 2
}
}
],
"number": {
"Value": 9
},
"word": {
"Value": "word string"
}
}
Кроме того, я не понимаю, как описать определение обобщенного типа типа ReactiveProperty<>
в Json.NET
, неясно, как интерпретировать JsonConverter
, ContractResolver
или что-то в этом роде.
public class Test : MonoBehaviour
{
private void Start()
{
TestData testData = new TestData();
DataStorageAgent dataStorageAgent = new DataStorageAgent();
string savePath = Application.dataPath + "/Saves/Test.json";
dataStorageAgent.Save(testData, savePath);
testData = dataStorageAgent.Load<TestData>(savePath);
}
}
public class TestData
{
public List<Resource> resourceList;
public ObservableList<Resource> observableResourceList;
public ReactiveProperty<int> number;
public ReactiveProperty<string> word;
public TestData()
{
resourceList = new List<Resource>();
resourceList.Add(new Resource("banana", 10));
resourceList.Add(new Resource("apple", 3));
observableResourceList = new ObservableList<Resource>();
observableResourceList.Add(new Resource("oBanana", 5));
observableResourceList.Add(new Resource("oApple", 2));
number = new ReactiveProperty<int>(9);
word = new ReactiveProperty<string>("word string");
}
}
public class Resource
{
public readonly string Name;
public ReactiveProperty<int> Count;
public Resource(string name, int count)
{
Name = name;
Count = new ReactiveProperty<int>(count);
}
}
public class DataStorageAgent
{
public T Load<T>(string saveFilePath) where T : class
{
if (File.Exists(saveFilePath))
{
string jsonString = File.ReadAllText(saveFilePath);
try
{
return JsonConvert.DeserializeObject<T>(jsonString, new ReactivePropertyJsonConverter());
}
catch (Exception e)
{
Debug.LogError(e);
}
}
else
{
Debug.LogError($"File does not exist - {saveFilePath}");
}
return default(T);
}
public void Save(object saveData, string saveFilePath)
{
string jsonString = JsonConvert.SerializeObject(saveData, Formatting.Indented, new ReactivePropertyJsonConverter());
File.WriteAllText(saveFilePath, jsonString);
}
}
public class ReactivePropertyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsGenericType && (objectType.GetGenericTypeDefinition() == typeof(ReactiveProperty<>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//JObject jsonObject = JObject.Load(reader);
//return existingValue;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("Value");
serializer.Serialize(writer, value.GetType().GetProperty("Value").GetValue(value));
writer.WriteEndObject();
}
}
Да это оно. Теперь меня интересует только значение. Короткий json в посте — это то, что я хочу получить в данный момент. Просто - Подсчет: Значение
При десериализации я пытаюсь прочитать значение и заменить его в реактивном свойстве.
Итак, вы хотите игнорировать компаратор равенства при сериализации и использовать значение по умолчанию при десериализации?
Да, я пытаюсь сделать что-то подобное »:)
Вы пытаетесь создать один собственный JsonConverter для ReactiveProperty<T> для каждого возможного типа T
, который будет сериализовать реактивное свойство как его значение . Я предлагаю разбить это на два этапа. Сначала создайте общий JsonConverter<ReactiveProperty<T>>, который можно использовать для любого конкретного типа значения T
. Во-вторых, создайте общий преобразователь для всех возможных типов значений, который использует отражение для создания и вызова преобразователей для каждого типа.
Общий JsonConverter<ReactiveProperty<T>>
будет выглядеть следующим образом:
public class ReactivePropertyJsonConverter<T> : JsonConverter<ReactiveProperty<T>>
{
public override ReactiveProperty<T>? ReadJson(JsonReader reader, Type objectType, ReactiveProperty<T>? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var isNull = reader.MoveToContentAndAssert().TokenType == JsonToken.Null;
if (isNull && typeof(T).IsValueType && Nullable.GetUnderlyingType(typeof(T)) == null)
return null;
var innerValue = !isNull ? serializer.Deserialize<T>(reader) : default(T);
if (hasExistingValue && existingValue != null)
existingValue.Value = (innerValue ?? default(T))!;
else if (objectType == typeof(ReactiveProperty<T>))
existingValue = innerValue == null ? new() : new(innerValue!);
else
// ReactiveProperty<T> was subclassed, use parameterized constructor.
existingValue = innerValue == null
? (ReactiveProperty<T>)Activator.CreateInstance(objectType)!
// Here we assume the subclass has a parameterized constructor taking the value as a a single argument.
: (ReactiveProperty<T>)Activator.CreateInstance(objectType, innerValue)!;
return existingValue;
}
public override void WriteJson(JsonWriter writer, ReactiveProperty<T>? value, JsonSerializer serializer) =>
serializer.Serialize(writer, value == null ? default(T?) : value.Value);
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
ArgumentNullException.ThrowIfNull(reader);
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
ArgumentNullException.ThrowIfNull(reader);
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
Метод WriteJson()
прост, но в отношении ReadJson()
следует отметить пару моментов:
Когда передается предварительно выделенный existingValue
, я заменяю его значение, а не создаю новое реактивное свойство.
ReactiveProperty<T>
не запечатан, поэтому objectType
может быть подклассом некоторого ReactiveProperty<T>
. В этом случае нам придется использовать Activator.CreateInstance()
для создания экземпляров этого типа.
Теперь что касается общего преобразователя, мы можем использовать Type.MakeGenericType(Type[]) для создания конкретного типа JsonConverter<ReactiveProperty<T>>
для каждого типа значения T
:
public class ReactivePropertyJsonConverter : JsonConverter
{
static readonly ConcurrentDictionary<Type, JsonConverter> Converters = new();
static JsonConverter CreateReactivePropertyConverterOfT(Type valueType) =>
(JsonConverter)Activator.CreateInstance(typeof(ReactivePropertyJsonConverter<>).MakeGenericType(valueType))!;
static JsonConverter GetReactivePropertyConverterOfT(Type valueType) =>
Converters.GetOrAdd(valueType, CreateReactivePropertyConverterOfT);
static Type? GetReactivePropertyValueType(Type objectType) =>
objectType.BaseTypesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ReactiveProperty<>)).FirstOrDefault()?.GetGenericArguments()[0];
public override bool CanConvert(Type objectType) => GetReactivePropertyValueType(objectType) != null;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) =>
GetReactivePropertyConverterOfT(GetReactivePropertyValueType(objectType)!).ReadJson(reader, objectType, existingValue, serializer);
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
writer.WriteNull();
else
GetReactivePropertyConverterOfT(GetReactivePropertyValueType(value.GetType())!).WriteJson(writer, value, serializer);
}
}
public static partial class JsonExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType!;
}
}
}
Теперь вы можете изменить DataStorageAgent
, чтобы использовать конвертер, например. следующее:
public class DataStorageAgent
{
static UTF8Encoding DefaultEncoding { get; } = new UTF8Encoding(false, true);
static JsonSerializerSettings DefaultSettings { get; } = new ()
{
Converters = { new ReactivePropertyJsonConverter() },
Formatting = Formatting.Indented,
};
public T? Load<T>(string saveFilePath) where T : class
{
try
{
using var textReader = new StreamReader(saveFilePath, DefaultEncoding);
using var jsonReader = new JsonTextReader(textReader);
return JsonSerializer.CreateDefault(DefaultSettings).Deserialize<T>(jsonReader);
}
catch (Exception ex)
{
Debug.LogError(ex);
return default(T);
}
}
public void Save(object saveData, string saveFilePath)
{
using var textWriter = new StreamWriter(saveFilePath, false, DefaultEncoding);
JsonSerializer.CreateDefault(DefaultSettings).Serialize(textWriter, saveData);
}
}
А также измените TestData
, чтобы не заполнять его списки в конструкторе следующим образом:
public class TestData
{
public List<Resource> resourceList = new();
public ObservableList<Resource> observableResourceList = new();
public ReactiveProperty<int> number = new ();
public ReactiveProperty<string> word = new ();
public TestData() { }
public static TestData Create() =>
new ()
{
resourceList = { new Resource("banana", 10), new Resource("apple", 3) },
observableResourceList = { new Resource("oBanana", 5), new Resource("oApple", 2) },
number = new ReactiveProperty<int>(9),
word = new ReactiveProperty<string>("word string"),
};
}
И у вас должно быть все готово.
Примечания:
Type.MakeGenericType()
и Activator.CreateInstance()
могут быть немного медленными, поэтому в ReactivePropertyJsonConverter
я использовал параллельный словарь для кэширования и повторного использования экземпляров конвертера.
Кажется, я припоминаю, что на некоторых платформах (например, IOS) работа Type.GenericMakeGenericType()
не гарантируется. Если вы используете эти платформы, ReactivePropertyJsonConverter
не будет работать, поэтому вам придется вручную создать все необходимые ReactivePropertyJsonConverter<T>
конвертеры и добавить их в список JsonSerializerSettings.Converters
.
По умолчанию при десериализации предварительно выделенного свойства List<T>
Json.NET добавит десериализованные элементы в список. Таким образом, если список уже был заполнен в каком-то конструкторе, содержимое списка может удвоиться.
Чтобы избежать этого, я изменил ваш TestData
для заполнения списков в методе Create()
и сделал var testData = TestData.Create();
в тестовом методе, но если вам нужно заполнить списки внутри конструкторов, вам нужно будет использовать ObjectCreationHandling.Replace
, как показано в JSON.NET. Почему это так Добавить в список вместо перезаписи?.
В разделе Советы по производительности: оптимизируйте использование памяти Newtonsoft рекомендует десериализовать файлы JSON непосредственно из файловых потоков, а не загружать содержимое в строки с последующей десериализацией строк.
Демо-рабочий пример здесь.
Ух ты! Выглядит действительно впечатляюще. Это именно то, чего я пытался достичь. Это оказалось сложнее, чем я думал. Большое спасибо за предоставление этого решения! Я уже потратил много времени на изучение вопроса и, похоже, потратил бы гораздо больше, если бы не ваша помощь.
Что такое Р3? Это github.com/Cysharp/R3 ? Если да, то находится ли
ReactiveProperty<T>
здесь: github.com/Cysharp/R3/blob/main/src/R3/ReactiveProperty.cs? И если да, то что вы хотите делать с сериализацией и десериализациейIEqualityComparer<T>? equalityComparer
? Просто использовать значение по умолчанию при десериализации? Что-то другое?