如何使readonly结构XML可序列化?

时间:2017-12-05 07:45:01

标签: c# .net serialization json.net c#-7.2

我有一个只有一个字段的不可变结构:

struct MyStruct
{
    private readonly double number;

    public MyStruct(double number)
        => this.number = number;
}

我希望能够通过以下方式进行序列化/反序列化:

  • 数据合同序列化程序
  • 二进制格式化程序
  • XML序列化程序(编辑:遗忘在原始问题中)
  • Json.NET(不添加Json.NET作为依赖项)

所以结构就变成了这个:

[Serializable]
struct MyStruct : ISerializable, IXmlSerializable
{
    private readonly double number;

    public MyStruct(double number)
        => this.number = number;

    private MyStruct(SerializationInfo info, StreamingContext context)
        => this.number = info.GetDouble(nameof(this.number));

    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        => info.AddValue(nameof(this.number), this.number);

    XmlSchema IXmlSerializable.GetSchema() => null;

    void IXmlSerializable.ReadXml(XmlReader reader)
    {
        // Necessary evil
        reader.Read();
        this = new MyStruct(double.Parse(reader.Value, CultureInfo.InvariantCulture));
    }

    void IXmlSerializable.WriteXml(XmlWriter writer)
        => writer.WriteString(this.number.ToString(CultureInfo.InvariantCulture));
}

由于:

    二进制格式化程序需要
  • [Serializable]
  • Json.NET尊重[DataContract]ISerializable
  • [DataContract]ISerializable不能一起使用。
  • 幸运的是,数据合同序列化程序支持IXmlSerializer

C#7.2为结构和MyStruct引入了readonly修饰符,作为一个不可变的结构似乎是一个理想的候选者。

问题是IXmlSerializable接口需要能够改变MyStruct。这就是我们上面所做的,在this实施中分配给IXmlSerializable.ReadXml

readonly struct MyStruct : IXmlSerializable
{
    // ...
    void IXmlSerializable.ReadXml(XmlReader reader)
    {
        // No longer works since "this" is now readonly.
        reader.Read();
        this = new MyStruct(double.Parse(reader.Value, CultureInfo.InvariantCulture));
    }
    // ...
}

我尝试通过反思作弊,但FieldInfo.SetValue将值设置为FieldInfo.SetValueDirectTypedReference需要__makeref,我无法获取thisIXmlSerializable也被禁止public class DbLibrary { public Guid Id { get; set; } public string Name { get; set; } public List<DbDepartment> Departments { get; set; } = new List<DbDepartment>(); } public class DbDepartment { public Guid Id { get; set; } public string Name { get; set; } } 1}}是只读的。

那么有什么方法可以让MyStruct被XML序列化器序列化?

我还应该提一下,我不关心输出XML是什么样的,我真的不需要DbDepartment接口提供的细粒度控件。我只需要使用我列出的序列化器使MyClass始终可序列化。

3 个答案:

答案 0 :(得分:8)

为了满足您的要求,您只需要:

[Serializable]
[DataContract]
public readonly struct MyStruct {
    [DataMember]
    private readonly double number;

    public MyStruct(double number)
        => this.number = number;
}

测试代码:

var target = new MyStruct(2);
// with Data Contract serializer
using (var ms = new MemoryStream()) {
    var s = new DataContractSerializer(typeof(MyStruct));
    s.WriteObject(ms, target);
    ms.Position = 0;
    var back = (MyStruct) s.ReadObject(ms);
    Debug.Assert(target.Equals(back));
}

// with Json.NET
var json = JsonConvert.SerializeObject(target);
var jsonBack = JsonConvert.DeserializeObject<MyStruct>(json);
Debug.Assert(target.Equals(jsonBack));

// with binary formatter
using (var ms = new MemoryStream()) {
    var formatter = new BinaryFormatter();
    formatter.Serialize(ms, target);
    ms.Position = 0;
    var back = (MyStruct) formatter.Deserialize(ms);
    Debug.Assert(target.Equals(back));
}

更新。由于您还需要支持XmlSerializer,因此您可以使用一些不安全的代码来满足您的要求:

[Serializable]    
public readonly struct MyStruct : ISerializable, IXmlSerializable
{        
    private readonly double number;
    public MyStruct(double number)
        => this.number = number;

    private MyStruct(SerializationInfo info, StreamingContext context)
        => this.number = info.GetDouble(nameof(this.number));

    XmlSchema IXmlSerializable.GetSchema() {
        return null;
    }

    unsafe void IXmlSerializable.ReadXml(XmlReader reader) {
        if (reader.Read()) {
            var value = double.Parse(reader.Value, CultureInfo.InvariantCulture);
            fixed (MyStruct* t = &this) {
                *t = new MyStruct(value);
            }
        }
    }

    void IXmlSerializable.WriteXml(XmlWriter writer) {
        writer.WriteString(this.number.ToString(CultureInfo.InvariantCulture));
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context) {
        info.AddValue(nameof(number), this.number);
    }
}

答案 1 :(得分:8)

作为最后的手段,读者可以被弃去&#34;来自https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe

Unsafe.AsRef

假设您可以有限地使用不安全的代码,那么丢弃读取性比fixed更好,并且可以使用托管类型。

&#34;几乎不可改变的&#34; struct是一个已知问题。这是一个相对罕见的案例,目前没有好的和安全的解决方案。

添加一个语言功能,允许选择性地只选择结构的某些成员,这是建议的长期解决方案之一。

答案 2 :(得分:4)

在某些情况下,虽然您可以成功使用unsafeUnsafe.AsRefFieldInfo.SetValue来更改值,但这在技术上是无效的代码,并且可能导致不确定的行为。

来自ECMA-335:

[注意: initonly 字段上使用ldfldaldsflda会使代码无法验证。在不可验证的代码中,VES无需检查 initonly 字段是否在构造函数之外发生了突变。如果方法更改常数的值,则VES无需报告任何错误。但是,这样的代码无效。 尾注]

FieldInfo.SetValue的官方API文档类似:​​

该方法不能可靠地设置静态,仅初始化(在C#中为readonly)字段的值。在.NET Core 3.0和更高版本中,如果尝试在静态的,仅用于初始化的字段上设置值,则会引发异常。

从技术上讲,运行时可以自由地围绕initonly字段进行优化,目前在某些static, initonly字段中可以进行优化。

您可能对C#9(https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9#init-only-setters)中新增的init only setters功能感兴趣。这提供了一种有效的方式来将属性设置为属性初始值设定项语法的一部分,并将获得适当的支持/更改,以确保它们成功运行并产生有效的代码。