如何使只读结构 XML 可序列化?

How to make readonly structs XML serializable?

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

struct MyStruct
{
    private readonly double number;

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

我希望它能够通过以下方式获得 serialized/deserialized:

所以结构变成这样:

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

因为:

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

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

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.SetValueDirect 需要 TypedReference,我无法获得,因为 __makeref 也被禁止this 是只读的。

那么允许 MyStruct 被 XML 序列化器序列化的方法是什么?

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

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

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

作为最后的手段,readonliness 可以是 "cast away" via Unsafe.AsRef from https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe

假设您可以接受有限使用不安全代码,那么放弃只读性比 fixed 好一点并且可以使用托管类型。

"almost immutable" 结构是一个已知问题。这是一种相对罕见的情况,目前还没有好的和安全的解决方案。

添加一种语言功能,允许有选择地只使结构的 一些 成员只读是建议的 long-term 解决方案之一。

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

来自 ECMA-335:

[Note: The use of ldflda or ldsflda on an initonly field makes code unverifiable. In unverifiable code, the VES need not check whether initonly fields are mutated outside the constructors. The VES need not report any errors if a method changes the value of a constant. However, such code is not valid. end note]

同样来自 FieldInfo.SetValue 的官方 API 文档:

This method cannot be used to set values of static, init-only (readonly in C#) fields reliably. In .NET Core 3.0 and later versions, an exception is thrown if you attempt to set a value on a static, init-only field.

运行时在技术上可以免费围绕 initonly 字段进行优化,目前在某些 static, initonly 字段的情况下进行优化。

您可能对 C# 9 (https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9#init-only-setters) 中的新 init only setters 功能感兴趣。这提供了一种将属性设置为 属性 初始化程序语法的一部分的有效方法,并将获得适当的 support/changes 以确保它们成功工作并生成有效代码。