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:
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.[DataContract]
and ISerializable
.[DataContract]
and ISerializable
can't be used together.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.
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
orldsflda
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://docs.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.
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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With