14

I have an immutable struct with only one field:

struct MyStruct
{
    private readonly double number;

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

And I want this to be able to get serialized/deserialized by:

  • Data contract serializer
  • Binary formatter
  • XML serializer (edit: forgotten in the original question)
  • Json.NET (without adding Json.NET as a dependency)

So the struct becomes this:

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

Because:

  • [Serializable] is required by the binary formatter.
  • Json.NET honors both [DataContract] and ISerializable.
  • [DataContract] and ISerializable can't be used together.
  • Luckily, IXmlSerializer is supported by the data contract serializer.

C# 7.2 introduces the readonly modifier for structs and MyStruct, being an immutable struct seems like an ideal candidate for this.

The problem is that IXmlSerializable interface requires the ability to mutate MyStruct. That's what we did above, assigning to this in IXmlSerializable.ReadXml implementation.

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));
    }
    // ...
}

I tried cheating via reflection but FieldInfo.SetValue boxes the value, and FieldInfo.SetValueDirect requires a TypedReference, which I can't obtain since __makeref is also forbidden when this is read-only.

So what are ways that would allow MyStruct to get serialized by the XML serializer?

I should also mention that I do not care what the output XML looks like, I don't really need the fine grained control provided by the IXmlSerializable interface. I only need to make MyClass consistently serializable using the serializers I listed.

Şafak Gür
  • 7,045
  • 5
  • 59
  • 96
  • How's that "[DataContract] and [Serializable] can't be used together"? They can. – Evk Dec 05 '17 at 07:57
  • 1
    The problem is these interface based serialization methods have to first have an object, which they get a specific interface from, before calling that interface. They don't call a specific constructor, which is the only place to change a `readonly` field. They were envisioned before immutable types were a thing in OOP. Another option is to have set/get fields, with a private field that signals whether the object is writable & raises an exception. – Ashley Pillay Dec 05 '17 at 07:58
  • @Evk `DataContractSerializer` throws an `InvalidDataContractException` when you try to serialize an object that is both marked with `[DataContract]` and implements `ISerializable`. – Şafak Gür Dec 05 '17 at 08:03
  • Maybe, but your claim is different: that `[DataContract]` and `[Serializable]` (attribute, not `ISerializable` interface) cannot be used together, while they can. – Evk Dec 05 '17 at 08:05
  • @AshleyPillay Exactly. In times like this, I wish that the XML serializer would cheat like the binary formatter and data contract serializer by creating an uninitialized object... – Şafak Gür Dec 05 '17 at 08:06
  • @Evk, you have a point, I'm updating the question. – Şafak Gür Dec 05 '17 at 08:07
  • This might be irrelevant to the question but why would you even want such a struct in the first place? Why not simply use a `double`? – Zohar Peled Dec 05 '17 at 08:12
  • Is `IXmlSerializable` required to implement? I mean it's not needed to achieve requirements listed. Or that is needed for other purposes? – Evk Dec 05 '17 at 08:14
  • @ZoharPeled I don't, it's a sample to simplify the question. – Şafak Gür Dec 05 '17 at 08:16
  • Something that may give you some mileage, instead of `private readonly` fields with a `public` property accessor, just use a `public readonly` field – Ashley Pillay Dec 05 '17 at 08:21
  • @AshleyPillay I don't expose the object publicly, I only use it internally. – Şafak Gür Dec 05 '17 at 08:22
  • XML deserialization simply copies the values from the XML to your struct. You should be able to add a `static` method for deserializing your struct, but it means all deserializations must go through it instead of normal deserialization. – Zohar Peled Dec 05 '17 at 08:22
  • @Evk, I failed to get MyStruct serialized/deserialized correctly using XmlSerializer without implementing IXmlSerializable. I would appreciate an answer if you can provide the listed requirements with a readonly struct. – Şafak Gür Dec 05 '17 at 08:25
  • @ZoharPeled, I know but I require XmlSerializer to work. – Şafak Gür Dec 05 '17 at 08:26
  • I didn't try it but what if you use a static method to do the actual deserialization, and also implement `IXmlSerializable` (where the `ReadXml` method simply calls the static one)? – Zohar Peled Dec 05 '17 at 08:28
  • @ZoharPeled, `ReadXml` method does not return a new instance, it mutates the current one. – Şafak Gür Dec 05 '17 at 08:31
  • @ŞafakGür oops, correct. Failed to notice that. anyway I see that you got your answer so that's good. – Zohar Peled Dec 05 '17 at 09:22

3 Answers3

10

To satisfy your requirements all you need is:

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

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

Test code:

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

Update. Since you also need to support XmlSerializer, you can use some unsafe code to achieve your requirements:

[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);
    }
}
Evk
  • 98,527
  • 8
  • 141
  • 191
  • And where is the XmlSerializer? – Şafak Gür Dec 05 '17 at 08:30
  • @ŞafakGür well it is not listed in " I want this to be able to get serialized/deserialized by" list. – Evk Dec 05 '17 at 08:31
  • Oh you are right once again, I apologize for forgetting it, I'm not usually this sloppy but I really thought I asked. – Şafak Gür Dec 05 '17 at 08:34
  • I'm editing the question to include it but I'll accept this answer if no one comes up with one supporting the XML serializer. Sorry for your time again. – Şafak Gür Dec 05 '17 at 08:36
  • 1
    @ŞafakGür I've updated answer with one way to achieve that. Hopefully you won't tell now that unsafe is also forbidden :) – Evk Dec 05 '17 at 08:48
  • Devilishly brilliant. :D And no, I think I caused you enough pain for today. – Şafak Gür Dec 05 '17 at 09:05
  • I accepted the answer but I wonder what would you do if the field was of a reference type like string or if it was generic? - `readonly struct MyStruct { private readonly T field; }` - Will you indulge? ;) – Şafak Gür Dec 05 '17 at 09:07
  • @ŞafakGür well in that case I'm not sure how to do that unfortunately. – Evk Dec 05 '17 at 10:01
8

As a last resort, the readonliness can be "cast away" via Unsafe.AsRef from https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe

Assuming you you are ok with limited use of unsafe code, casting away readonliness is a bit nicer than fixed and can work with managed types.

The "almost immutable" struct is a known problem. It is a relatively rare case for which there is no nice and safe solution right now.

Adding a language feature that would allow selectively making only some members of a struct readonly is one of the proposed long-term solutions.

VSadov
  • 961
  • 8
  • 4
4

While you can successfully use unsafe, Unsafe.AsRef, or FieldInfo.SetValue to mutate the value in some scenarios, this is technically invalid code and may result in undefined behavior.

From 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]

Likewise from the official API Docs for FieldInfo.SetValue:

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.

The runtime is technically free to make optimizations around initonly fields and currently does in the case of certain static, initonly fields.

You might be interested in the new init only setters feature coming in C# 9 (https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9#init-only-setters). This provides a valid way to set properties as part of the property initializer syntax and will get the appropriate support/changes to ensure they work successfully and result in valid code.