C#字段:必须初始化但仍可访问反序列化器。

huangapple go评论72阅读模式
英文:

C# fields: mandatory initialization but still accessible to deserializer

问题

I'm only providing the translated content without any additional information or answers:

我面临一个在C#(和其他面向对象语言)中非常常见的困境,我试图满足三个竞争性要求:

  1. 我绝对不希望我的类的最终用户在构造时忘记初始化某些字段。
  2. 我希望一个反序列化器能够实例化该类并填充字段,而不会被私有设置器“阻塞”。
  3. 我不希望留下任何未初始化的字段(例如,必须构造列表,不允许null)。

注意:
这不是与这个旧问题相同的问题: https://stackoverflow.com/questions/10794346/how-to-make-a-property-required-in-c
出于两个原因:

  1. 它没有涉及到要求 #2(暴露字段以进行反序列化)。
  2. 它已经有10年了,C#自那时以来发生了很大的变化。

所以,假设这是我的类:

public class MyClass {
     public bool Field1 {get; set;}
     public IEnumerable<bool> Field2 {get; set;} = default!;
}

注意:不要过于关注= default!这部分,在这里我只是让编译器高兴(它检测到未初始化的字段),以便继续核心问题。

如果我只需要满足要求 #1,那么我不会这样做:

var o = new MyClass() {
             Field1 = true
             // 哎呀,我忘记Field2了
        };

相反,我会这样做:

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable<bool> Field2 {get; private set;}

     public MyClass(bool field1, IEnumerable<bool> field2) {
         Field1 = field1;
         Field2 = field2;
     } 
}

对于只有要求 #3,我会这样做:

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable<bool> Field2 {get; private set;} = new List<bool>();

     public MyClass(bool field1) {
         Field1 = field1;
     } 
}

// ...

c.Field2.AddRange(...);

不会这样做,因为尽管保护了字段免受篡改,但不能保证要求 #1

public class MyClass {
     public bool Field1 {get; init;}
     public IEnumerable<bool> Field2 {get; init;} = new List<bool>();
}

但现在我必须满足要求 #2。这是一个问题,因为反序列化器不能填充protectedprivate字段。

这意味着这将不会起作用,因为Field1仍然保持为false
(注意:我们假设反序列化器已正确配置:没有大小写混淆等)

string json = @"{ ""Field1"": true, ""Field2"": [] }";
var o = JsonSerializer.Deserialize<MyClass>(json);
Assert(o.Field1 == true); // 失败

我所知道的满足所有三个要求的唯一解决方案是具有所有字段的构造函数:

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable<bool> Field2 {get; private set;} = new List<bool>();

     [JsonConstructor] // <-- 使其无懈可击!
     public MyClass(bool field1, IEnumerable<bool> field2) {
         Field1 = field1;
         Field2 = field2;
     } 
}

string json = @"{ ""Field1"": true, ""Field2"": [] }";
var o = JsonSerializer.Deserialize<MyClass>(json);
Assert(o.Field1 == true); // 成功

即使这个解决方案也不完美,因为如果我向类中添加一个字段“Field3”,然后可能忘记将其添加到详尽的构造函数中,反序列化会默默忽略它。失败!

我的问题:

在现代C#中是否有一种优雅的方法来实现这一点?
许多关于这个主题的答案都已有10多年了。C#在各个方向都取得了巨大的进步。理想情况下,我希望得到**.Net6(C# 10)**的答案,并且可能还有关于更近期C#(C# 11+)的答案。

英文:

I'm facing a dilemma that's very common in C# (and other OO languages) where I'm trying to meet 3 competing requirements :

> 1. I absolutely don't want the end-user of my class to forget to initialize some of the fields, at construction time.
> 2. I want a deserializer to be able to instantiate that class and populate the fields without silently being "blocked" by a private setter.
> 3. I don't want any non-initialized fields left hanging (e.g. a list has to be constructed, no loose null in there!)
>

Note:
> This is NOT the same question as this old one : https://stackoverflow.com/questions/10794346/how-to-make-a-property-required-in-c
> for two reasons :
> 1. It doesn't deal with requirement #2 (expose fields for deserialization)
> 2. It's 10 years old, and C# has evolved a lot since then.

So, let's say that this is my class :

public class MyClass {
     public bool Field1 {get; set;}
     public IEnumerable&lt;bool&gt; Field2 {get; set;} = default!; 
}

Note : don't obsess over the = default! bit, here I'm just making the compiler happy (it detects non-initialized fields) to move on to the core of the issue.

if I needed to meet requirement #1 only then I would NOT do this :

var o = new MyClass() {
             Field1 = true
             // Uh oh, I forgot Field2
        };

Instead I would do this :

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable&lt;bool&gt; Field2 {get; private set;}

     public MyClass(bool field1, IEnumerable&lt;bool&gt; field2) {
         Field1 = field1;
         Field2 = field2;
     } 
}

For requirement #3 only I would do this :

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable&lt;bool&gt; Field2 {get; private set;} = new List&lt;bool&gt;();

     public MyClass(bool field1) {
         Field1 = field1;
     } 
}

// ...

c.Field2.AddRange(...);

I would NOT do this because despite protecting the fields from tampering, it does not guarantee requirement #1 :

public class MyClass {
     public bool Field1 {get; init;}
     public IEnumerable&lt;bool&gt; Field2 {get; init;} = new List&lt;bool&gt;();
}

But now I have to fullfil requirement #2 . That's a problem because the deserializer cannot populate protected or private fields.

Which means that this would NOT work, as Field1 would remain false :
(note: we're assuming that the deserializer is properly configured : no upper-case/lower-case nonsense or whatnot)

string json = @&quot;{ &quot;&quot;Field1&quot;&quot;: true, &quot;&quot;Field2&quot;&quot;: [] }&quot;;
var o = JsonSerializer.Deserialize&lt;MyClass&gt;(json);
Assert(o.Field1 == true); // fails

The only solution I'm aware of to fulfill all 3 requirements is a constructor that has ALL the fields :

public class MyClass {
     public bool Field1 {get; private set;}
     public IEnumerable&lt;bool&gt; Field2 {get; private set;} = new List&lt;bool&gt;();

     [JsonConstructor] // &lt;-- to make it air-tight!
     public MyClass(bool field1, IEnumerable&lt;bool&gt; field2) {
         Field1 = field1;
         Field2 = field2;
     } 
}

string json = @&quot;{ &quot;&quot;Field1&quot;&quot;: true, &quot;&quot;Field2&quot;&quot;: [] }&quot;;
var o = JsonSerializer.Deserialize&lt;MyClass&gt;(json);
Assert(o.Field1 == true); // succeeds

even that solution is not perfect, because if I add a field "Field3" to the class then I might forget to add it to the exhaustive constructor, and the deserialization would silently ignore it. Fail!

My question :

Is there an elegant way of achieving this in modern C#?
Many answers about this topic are 10+ years old. C# has made a ton of progress in every direction since then.
Ideally, I'd like an answer for .Net6 (C# 10) AND possibly an answer for more recent C# (C# 11+)

答案1

得分: 1

For C# 11 - there is feature introduced to specifically support such scenarios - required modifier which allows your to do something like (also note the usage of init keyword which declares an init-only setter which allows to assign a value to the property only during object construction):

public class MyClass 
{
     public required bool Field1 {get; init;}
     public required IEnumerable&lt;bool&gt; Field2 {get; init;}
}

The last option can also be reduced to using records (available since C# 9):

public record MyClass(bool Field1, IEnumerable&lt;bool&gt; Field2);

P.S.

Note that required modifier is also considered by System.Text.Json, so marking property with it will make the corresponding JSON property required in the JSON payload.

英文:

For C# 11 - there is feature introduced to specifically support such scenarios - required modifier which allows your to do something like (also note the usage of init keyword which declares an init-only setter which allows to assign a value to the property only during object construction):

public class MyClass 
{
     public required bool Field1 {get; init;}
     public required IEnumerable&lt;bool&gt; Field2 {get; init;}
}

The last option can also be reduced to using records (available since C# 9):

public record MyClass(bool Field1, IEnumerable&lt;bool&gt; Field2);

P.S.

Note that required modifier is also considered by System.Text.Json, so marking property with it will make the corresponding JSON property required in the JSON payload.

huangapple
  • 本文由 发表于 2023年5月17日 17:35:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/76270627.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定