我有一个只有一个字段的不可变结构:
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
变异的能力。这就是我们上面所做的,在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 可以通过Unsafe.AsRef
从 https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe"抛弃">
假设您可以有限地使用不安全的代码,那么丢弃 readonliness 比fixed
更好一些,并且可以处理托管类型。
"几乎不可变"结构是一个已知问题。这是一个相对罕见的情况,目前没有良好和安全的解决方案。
添加允许有选择地仅将结构的某些成员设为只读的语言功能是建议的长期解决方案之一。
虽然在某些情况下可以成功地使用unsafe
、Unsafe.AsRef
或FieldInfo.SetValue
来改变值,但这在技术上是无效的代码,可能会导致未定义的行为。
从 ECMA-335:
[注意:在initonly字段上使用
ldflda
或ldsflda
会使代码无法验证。在无法验证的代码中,VES 不需要检查initonly字段是否在构造函数外部发生了变化。如果方法更改常量的值,则 VES 无需报告任何错误。但是,此类代码无效。尾注]
同样来自官方 API 文档FieldInfo.SetValue
:
此方法不能用于可靠地设置静态、仅初始化(
readonly
中的 C#)字段的值。在 .NET Core 3.0 及更高版本中,如果尝试在静态的仅初始化字段上设置值,则会引发异常。
从技术上讲,运行时可以自由地围绕initonly
字段进行优化,目前在某些static, initonly
字段的情况下也是如此。
你可能对 C# 9 (https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9#init-only-setters) 中推出的新功能init only setters
感兴趣。这提供了一种将属性设置为属性初始值设定项语法一部分的有效方法,并将获得适当的支持/更改,以确保它们成功工作并生成有效的代码。