CA2227 的解决方案或更好的方法?
Solution for CA2227 or better approach?
我只使用代码分析来清理、组织和确保这些更改是针对特定警告的所有实例全局执行的。我进入了决赛,它是 CA2227。
CA2227 Collection properties should be read only Change '' to be
read-only by removing the property setter.
请注意,这是用于 EDI 文档的映射。这些 class 表示 EDI 文档的全部或一部分。
public class PO1Loop
{
public SegmentTypes.PO1LoopSegmentTypes.PO1 PO1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID1> PIDRepeat1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID2> PIDRepeat2 { get; set; }
public SegmentTypes.PO1LoopSegmentTypes.PO4 PO4 { get; set; }
/* Max Use: 8 */
public Collection<SegmentTypes.PO1LoopSegmentTypes.ACK> ACKRepeat { get; set; }
}
你可以看到所有的集合属性都会给我这个警告,而且有数百个。使用上面的 class 时,我在没有任何数据的情况下实例化它。然后我在外部添加数据并通过其 public 访问器设置每个单独的变量。我没有使用构造函数方法准备和传递的所有数据来实例化此 class(IMO,这些可以达到的大小很容易对眼睛造成严重破坏)。完成并分配所有属性后,class 将作为一个整体用于生成它所代表的文档部分。
我的问题是,对于上述用法,正确设置它的更好方法是什么?我是保留 public 访问器并完全禁止显示此警告,还是有完全不同的解决方案可行?
以下是 MSDN 对错误的描述,以及如何避免它。
这是我对这个问题的看法。
考虑以下 class:
class BigDataClass
{
public List<string> Data { get; set; }
}
这个 class 将抛出完全相同的问题。为什么?因为 Collections
而不是 需要 setter。现在,我们可以对该对象做 任何事情:将 Data
分配给任意 List<string>
,向 Data
添加元素,从 [=17] 删除元素=],等等。如果我们删除 setter
,我们 仅 失去 直接分配给 属性 的能力。
考虑以下代码:
class BigDataClass
{
private List<string> data = new List<string>();
public List<string> Data { get { return data; } } // note, we removed the setter
}
var bigData = new BigDataClass();
bigData.Data.Add("Some String");
此代码完全有效,实际上推荐的做事方式。为什么?因为 List<string>
是对内存位置的 引用 ,它包含数据的其余部分。
现在,唯一你现在不能用这个做的事情是直接设置 Data
属性. IE。以下无效:
var bigData = new BigDataClass();
bigData.Data = new List<string>();
这不一定是坏事。您会注意到在 许多 .NET 类型上使用了此模型。这是不变性的基础。您 通常 不希望直接访问 Collections
的可变性,因为这可能会导致一些具有奇怪问题的意外行为。这就是 Microsoft 建议您省略 setters.
的原因
示例:
var bigData = new BigDataClass();
bigData.Data.Add("Some String");
var l2 = new List<string>();
l2.Add("String 1");
l2.Add("String 2");
bigData.Data = l2;
Console.WriteLine(bigData.Data[0]);
我们可能期望 Some String
,但我们会得到 String 1
。这也意味着您无法可靠地将事件附加到有问题的Collection
,因此您无法可靠地确定是添加了新值还是删除了值。
A writable collection property allows a user to replace the collection with a completely different collection.
本质上,如果您只曾经需要运行构造函数或赋值一次,则省略set
修饰符。你不需要它,直接分配集合是违反最佳实践的。
现在,我并不是说 永远不要在 Collection
上使用 setter,有时你可能需要一个,但通常你不应该使用它们。
你总是可以在Collections
上使用.AddRange
、.Clone
等,你只是失去了[=33]的能力=].
序列化
最后,如果我们希望 Serialize
或 Deserialize
包含我们的 Collection
而没有 set
的 class,我们该怎么办?好吧,总是有不止一种方法可以做到这一点,最简单的(在我看来)是创建一个 property
来表示序列化集合。
以我们的BigDataClass
为例。如果我们希望 Serialize
,然后 Deserialize
这个 class 使用以下代码, Data
属性 将没有元素。
JavaScriptSerializer jss = new JavaScriptSerializer();
BigDataClass bdc = new BigDataClass();
bdc.Data.Add("Test String");
string serd = jss.Serialize(bdc);
Console.WriteLine(serd);
BigDataClass bdc2 = jss.Deserialize<BigDataClass>(serd);
所以,要解决这个问题,我们可以简单地修改我们的 BigDataClass
一点,让它使用新的 string
属性 来达到 Serialization
的目的。
public class BigDataClass
{
private List<string> data = new List<string>();
[ScriptIgnore]
public List<string> Data { get { return data; } } // note, we removed the setter
public string SerializedData { get { JavaScriptSerializer jss = new JavaScriptSerializer(); return jss.Serialize(data); } set { JavaScriptSerializer jss = new JavaScriptSerializer(); data = jss.Deserialize<List<string>>(value); } }
}
另一个选项始终是 DataContractSerializer
(总的来说,这确实是一个更好的选项。)您可以在 this Whosebug question.
上找到有关它的信息
感谢@Matthew、@CraigW 和@EBrown 帮助我理解了这个警告的解决方案。
public class PO1Loop
{
public SegmentTypes.PO1LoopSegmentTypes.PO1 PO1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID1> PIDRepeat1 { get; private set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID2> PIDRepeat2 { get; private set; }
public SegmentTypes.PO1LoopSegmentTypes.PO4 PO4 { get; set; }
/* Max Use: 8 */
public Collection<SegmentTypes.PO1LoopSegmentTypes.ACK> ACKRepeat { get; private set; }
public PO1Loop()
{
PIDRepeat1 = new Collection<SegmentTypes.PO1LoopSegmentTypes.PID1>();
PIDRepeat2 = new Collection<SegmentTypes.PO1LoopSegmentTypes.PID2>();
ACKRepeat = new Collection<SegmentTypes.PO1LoopSegmentTypes.ACK>();
}
}
当想要将数据分配给集合类型时,使用 AddRange、Clear 或任何其他修改集合的方法变体。
我必须修复一些 CA2227 违规行为,所以我必须将 "readonly" 关键字添加到集合字段,然后当然必须删除 setter 属性 .一些使用了 setter 的代码刚刚创建了一个最初为空的新集合对象。这段代码肯定没有编译,所以我不得不添加一个 SetXxx() 方法来实现缺少的 setter 的功能。我是这样做的:
public void SetXxx(List<string> list)
{
this.theList.Clear();
this.theList.AddRange(list);
}
使用 setter 的调用方代码已替换为对方法 SetXxx() 的调用。
不是创建一个完整的新列表,而是现有列表将被清除并填充来自另一个列表的新项目,作为参数传入。原始列表,由于它是只读的并且只创建一次,将永远保留。
我相信这也是避免 garbagae 收集器必须删除超出范围的旧对象的好方法,其次,创建新的收集对象,尽管已经有一个。
使用当前的 VS2019 我们可以简单地这样做:
public List<string> Data { get; } = new List<string>();
这满足CA2227,可以serialized/deserialized。
反序列化有效是因为 List<> 有一个“Add”方法,并且序列化程序知道如何使用 Add 方法处理 read-only 集合 属性(属性 read-only 但不是元素)(我使用 Json.Net,其他序列化器的行为可能不同)。
编辑:
正如所指出的那样,它应该是“=”而不是“=>”(编译器会阻止您使用“=>”)。如果我们使用“public List Data => new List();”然后它会在每次访问 属性 时创建一个新列表,这也不是我们想要的。
编辑:
请注意,如果 属性 的类型是接口,例如 IList
,这将不起作用
编辑:
我认为接口的处理是由使用的序列化器决定的。以下工作完美。我确信所有常见的序列化程序都知道如何处理 ICollection。如果您有一些未实现 ICollection 的自定义接口,那么您应该能够配置序列化程序来处理它,但在这种情况下,CA2227 可能不会被触发,因此它在这里无关紧要。 (因为它是一个 read-only 属性 你必须在 class 中分配一个具体的值所以它应该总是序列化并且 de-serializing a non-null值)
public class CA2227TestClass
{
public IList Data { get; } = new List<string>();
}
[TestMethod]
public void CA2227_Serialization()
{
var test = new CA2227TestClass()
{
Data = { "One", "Two", "Three" }
};
var json = JsonConvert.SerializeObject(test);
Assert.AreEqual("{\"Data\":[\"One\",\"Two\",\"Three\"]}", json);
var jsonObject = JsonConvert.DeserializeObject(json, typeof(CA2227TestClass)) as CA2227TestClass;
Assert.IsNotNull(jsonObject);
Assert.AreEqual(3, jsonObject.Data.Count);
Assert.AreEqual("One", jsonObject.Data[0]);
Assert.AreEqual("Two", jsonObject.Data[1]);
Assert.AreEqual("Three", jsonObject.Data[2]);
Assert.AreEqual(typeof(List<string>), jsonObject.Data.GetType());
}
备选方案
在我的情况下,将 属性 设置为只读是不可行的,因为整个列表(作为参考)可能会更改为新列表。
我能够通过将属性的 setter 范围更改为 internal
来解决此警告。
public List<Batch> Batches
{
get { return _Batches; }
internal set { _Batches = value; OnPropertyChanged(nameof(Batches)); }
}
注意也可以使用 private set
...
此警告的提示 (achilleas heal) 似乎确实指向文档库说(加粗我的):
An externally visible writable property is a type that implements
System.Collections.ICollection.
对我来说,"Ok, I won't make it viewable externally...." 和 internal
对应用程序来说很好。
涵盖解决 CA2227 错误的所有可能情况:
这涵盖了我们使用 Entity Framework.
时的实体关系映射
class Program
{
static void Main(string[] args)
{
ParentClass obj = new ParentClass();
obj.ChildDetails.Clear();
obj.ChildDetails.AddRange();
obj.LstNames.Clear();
obj.LstNames.AddRange();
}
}
public class ChildClass
{ }
public class ParentClass
{
private readonly ICollection<ChildClass> _ChildClass;
public ParentClass()
{
_ChildClass = new HashSet<ChildClass>();
}
public virtual ICollection<ChildClass> ChildDetails => _ChildClass;
public IList<string> LstNames => new List<string>();
}
只有绑定DTO时,才需要抑制警告。
否则需要自定义 ModelBinder 自定义 ModelBinder 来绑定集合。
引用规则文档:
When to suppress warnings
You can suppress the warning if the property is part of a Data Transfer Object (DTO) class.
Otherwise, do not suppress warnings from this rule.
https://docs.microsoft.com/pt-br/visualstudio/code-quality/ca2227?view=vs-2019
DTO 通常需要序列化和反序列化。因此,它们必须是可变的。
必须创建备用支持 属性 很痛苦。
只需将 属性 类型从 List<string>
更改为 IReadOnlyList<string>
然后这将在没有 CA2227 的情况下按预期工作。
集合是通过 属性 设置的,但如果您想添加或删除项目,也可以转换为 List<string>
。
class Holder
{
public IReadOnlyList<string> Col { get; set; } = new List<string>();
}
var list = new List<string> { "One", "Two" };
var holder = new Holder() { Col = list } ;
var json = JsonConvert.SerializeObject(holder);
// output json {"Col":["One","Two"]}
var deserializedHolder = JsonConvert.DeserializeObject<Holder>(json);
作为 Der Kommissar 出色回答的补充。
从 .NET 5 (C# 9.0) 开始,有 init-only properties. These properties are only settable under specific circumstances, see here 可供参考。
以下示例不应引发警告 CA2227,但仍允许在对象初始化期间设置集合。
using System.Collections.Generic;
namespace BookStore
{
public class BookModel
{
public ICollection<string> Chapters { get; init; }
}
}
请注意,当前版本的 .NET SDK 在使用内置分析器(而非 NuGet 包)时仍会发出警告。这是一个已知的错误,应该在将来修复。
我只使用代码分析来清理、组织和确保这些更改是针对特定警告的所有实例全局执行的。我进入了决赛,它是 CA2227。
CA2227 Collection properties should be read only Change '' to be read-only by removing the property setter.
请注意,这是用于 EDI 文档的映射。这些 class 表示 EDI 文档的全部或一部分。
public class PO1Loop
{
public SegmentTypes.PO1LoopSegmentTypes.PO1 PO1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID1> PIDRepeat1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID2> PIDRepeat2 { get; set; }
public SegmentTypes.PO1LoopSegmentTypes.PO4 PO4 { get; set; }
/* Max Use: 8 */
public Collection<SegmentTypes.PO1LoopSegmentTypes.ACK> ACKRepeat { get; set; }
}
你可以看到所有的集合属性都会给我这个警告,而且有数百个。使用上面的 class 时,我在没有任何数据的情况下实例化它。然后我在外部添加数据并通过其 public 访问器设置每个单独的变量。我没有使用构造函数方法准备和传递的所有数据来实例化此 class(IMO,这些可以达到的大小很容易对眼睛造成严重破坏)。完成并分配所有属性后,class 将作为一个整体用于生成它所代表的文档部分。
我的问题是,对于上述用法,正确设置它的更好方法是什么?我是保留 public 访问器并完全禁止显示此警告,还是有完全不同的解决方案可行?
以下是 MSDN 对错误的描述,以及如何避免它。
这是我对这个问题的看法。
考虑以下 class:
class BigDataClass
{
public List<string> Data { get; set; }
}
这个 class 将抛出完全相同的问题。为什么?因为 Collections
而不是 需要 setter。现在,我们可以对该对象做 任何事情:将 Data
分配给任意 List<string>
,向 Data
添加元素,从 [=17] 删除元素=],等等。如果我们删除 setter
,我们 仅 失去 直接分配给 属性 的能力。
考虑以下代码:
class BigDataClass
{
private List<string> data = new List<string>();
public List<string> Data { get { return data; } } // note, we removed the setter
}
var bigData = new BigDataClass();
bigData.Data.Add("Some String");
此代码完全有效,实际上推荐的做事方式。为什么?因为 List<string>
是对内存位置的 引用 ,它包含数据的其余部分。
现在,唯一你现在不能用这个做的事情是直接设置 Data
属性. IE。以下无效:
var bigData = new BigDataClass();
bigData.Data = new List<string>();
这不一定是坏事。您会注意到在 许多 .NET 类型上使用了此模型。这是不变性的基础。您 通常 不希望直接访问 Collections
的可变性,因为这可能会导致一些具有奇怪问题的意外行为。这就是 Microsoft 建议您省略 setters.
示例:
var bigData = new BigDataClass();
bigData.Data.Add("Some String");
var l2 = new List<string>();
l2.Add("String 1");
l2.Add("String 2");
bigData.Data = l2;
Console.WriteLine(bigData.Data[0]);
我们可能期望 Some String
,但我们会得到 String 1
。这也意味着您无法可靠地将事件附加到有问题的Collection
,因此您无法可靠地确定是添加了新值还是删除了值。
A writable collection property allows a user to replace the collection with a completely different collection.
本质上,如果您只曾经需要运行构造函数或赋值一次,则省略set
修饰符。你不需要它,直接分配集合是违反最佳实践的。
现在,我并不是说 永远不要在 Collection
上使用 setter,有时你可能需要一个,但通常你不应该使用它们。
你总是可以在Collections
上使用.AddRange
、.Clone
等,你只是失去了[=33]的能力=].
序列化
最后,如果我们希望 Serialize
或 Deserialize
包含我们的 Collection
而没有 set
的 class,我们该怎么办?好吧,总是有不止一种方法可以做到这一点,最简单的(在我看来)是创建一个 property
来表示序列化集合。
以我们的BigDataClass
为例。如果我们希望 Serialize
,然后 Deserialize
这个 class 使用以下代码, Data
属性 将没有元素。
JavaScriptSerializer jss = new JavaScriptSerializer();
BigDataClass bdc = new BigDataClass();
bdc.Data.Add("Test String");
string serd = jss.Serialize(bdc);
Console.WriteLine(serd);
BigDataClass bdc2 = jss.Deserialize<BigDataClass>(serd);
所以,要解决这个问题,我们可以简单地修改我们的 BigDataClass
一点,让它使用新的 string
属性 来达到 Serialization
的目的。
public class BigDataClass
{
private List<string> data = new List<string>();
[ScriptIgnore]
public List<string> Data { get { return data; } } // note, we removed the setter
public string SerializedData { get { JavaScriptSerializer jss = new JavaScriptSerializer(); return jss.Serialize(data); } set { JavaScriptSerializer jss = new JavaScriptSerializer(); data = jss.Deserialize<List<string>>(value); } }
}
另一个选项始终是 DataContractSerializer
(总的来说,这确实是一个更好的选项。)您可以在 this Whosebug question.
感谢@Matthew、@CraigW 和@EBrown 帮助我理解了这个警告的解决方案。
public class PO1Loop
{
public SegmentTypes.PO1LoopSegmentTypes.PO1 PO1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID1> PIDRepeat1 { get; private set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID2> PIDRepeat2 { get; private set; }
public SegmentTypes.PO1LoopSegmentTypes.PO4 PO4 { get; set; }
/* Max Use: 8 */
public Collection<SegmentTypes.PO1LoopSegmentTypes.ACK> ACKRepeat { get; private set; }
public PO1Loop()
{
PIDRepeat1 = new Collection<SegmentTypes.PO1LoopSegmentTypes.PID1>();
PIDRepeat2 = new Collection<SegmentTypes.PO1LoopSegmentTypes.PID2>();
ACKRepeat = new Collection<SegmentTypes.PO1LoopSegmentTypes.ACK>();
}
}
当想要将数据分配给集合类型时,使用 AddRange、Clear 或任何其他修改集合的方法变体。
我必须修复一些 CA2227 违规行为,所以我必须将 "readonly" 关键字添加到集合字段,然后当然必须删除 setter 属性 .一些使用了 setter 的代码刚刚创建了一个最初为空的新集合对象。这段代码肯定没有编译,所以我不得不添加一个 SetXxx() 方法来实现缺少的 setter 的功能。我是这样做的:
public void SetXxx(List<string> list)
{
this.theList.Clear();
this.theList.AddRange(list);
}
使用 setter 的调用方代码已替换为对方法 SetXxx() 的调用。
不是创建一个完整的新列表,而是现有列表将被清除并填充来自另一个列表的新项目,作为参数传入。原始列表,由于它是只读的并且只创建一次,将永远保留。
我相信这也是避免 garbagae 收集器必须删除超出范围的旧对象的好方法,其次,创建新的收集对象,尽管已经有一个。
使用当前的 VS2019 我们可以简单地这样做:
public List<string> Data { get; } = new List<string>();
这满足CA2227,可以serialized/deserialized。
反序列化有效是因为 List<> 有一个“Add”方法,并且序列化程序知道如何使用 Add 方法处理 read-only 集合 属性(属性 read-only 但不是元素)(我使用 Json.Net,其他序列化器的行为可能不同)。
编辑: 正如所指出的那样,它应该是“=”而不是“=>”(编译器会阻止您使用“=>”)。如果我们使用“public List Data => new List();”然后它会在每次访问 属性 时创建一个新列表,这也不是我们想要的。
编辑:
请注意,如果 属性 的类型是接口,例如 IList
编辑: 我认为接口的处理是由使用的序列化器决定的。以下工作完美。我确信所有常见的序列化程序都知道如何处理 ICollection。如果您有一些未实现 ICollection 的自定义接口,那么您应该能够配置序列化程序来处理它,但在这种情况下,CA2227 可能不会被触发,因此它在这里无关紧要。 (因为它是一个 read-only 属性 你必须在 class 中分配一个具体的值所以它应该总是序列化并且 de-serializing a non-null值)
public class CA2227TestClass
{
public IList Data { get; } = new List<string>();
}
[TestMethod]
public void CA2227_Serialization()
{
var test = new CA2227TestClass()
{
Data = { "One", "Two", "Three" }
};
var json = JsonConvert.SerializeObject(test);
Assert.AreEqual("{\"Data\":[\"One\",\"Two\",\"Three\"]}", json);
var jsonObject = JsonConvert.DeserializeObject(json, typeof(CA2227TestClass)) as CA2227TestClass;
Assert.IsNotNull(jsonObject);
Assert.AreEqual(3, jsonObject.Data.Count);
Assert.AreEqual("One", jsonObject.Data[0]);
Assert.AreEqual("Two", jsonObject.Data[1]);
Assert.AreEqual("Three", jsonObject.Data[2]);
Assert.AreEqual(typeof(List<string>), jsonObject.Data.GetType());
}
备选方案
在我的情况下,将 属性 设置为只读是不可行的,因为整个列表(作为参考)可能会更改为新列表。
我能够通过将属性的 setter 范围更改为 internal
来解决此警告。
public List<Batch> Batches
{
get { return _Batches; }
internal set { _Batches = value; OnPropertyChanged(nameof(Batches)); }
}
注意也可以使用 private set
...
此警告的提示 (achilleas heal) 似乎确实指向文档库说(加粗我的):
An externally visible writable property is a type that implements System.Collections.ICollection.
对我来说,"Ok, I won't make it viewable externally...." 和 internal
对应用程序来说很好。
涵盖解决 CA2227 错误的所有可能情况: 这涵盖了我们使用 Entity Framework.
时的实体关系映射class Program
{
static void Main(string[] args)
{
ParentClass obj = new ParentClass();
obj.ChildDetails.Clear();
obj.ChildDetails.AddRange();
obj.LstNames.Clear();
obj.LstNames.AddRange();
}
}
public class ChildClass
{ }
public class ParentClass
{
private readonly ICollection<ChildClass> _ChildClass;
public ParentClass()
{
_ChildClass = new HashSet<ChildClass>();
}
public virtual ICollection<ChildClass> ChildDetails => _ChildClass;
public IList<string> LstNames => new List<string>();
}
只有绑定DTO时,才需要抑制警告。 否则需要自定义 ModelBinder 自定义 ModelBinder 来绑定集合。
引用规则文档:
When to suppress warnings
You can suppress the warning if the property is part of a Data Transfer Object (DTO) class.
Otherwise, do not suppress warnings from this rule.
https://docs.microsoft.com/pt-br/visualstudio/code-quality/ca2227?view=vs-2019
DTO 通常需要序列化和反序列化。因此,它们必须是可变的。
必须创建备用支持 属性 很痛苦。
只需将 属性 类型从 List<string>
更改为 IReadOnlyList<string>
然后这将在没有 CA2227 的情况下按预期工作。
集合是通过 属性 设置的,但如果您想添加或删除项目,也可以转换为 List<string>
。
class Holder
{
public IReadOnlyList<string> Col { get; set; } = new List<string>();
}
var list = new List<string> { "One", "Two" };
var holder = new Holder() { Col = list } ;
var json = JsonConvert.SerializeObject(holder);
// output json {"Col":["One","Two"]}
var deserializedHolder = JsonConvert.DeserializeObject<Holder>(json);
作为 Der Kommissar 出色回答的补充。
从 .NET 5 (C# 9.0) 开始,有 init-only properties. These properties are only settable under specific circumstances, see here 可供参考。
以下示例不应引发警告 CA2227,但仍允许在对象初始化期间设置集合。
using System.Collections.Generic;
namespace BookStore
{
public class BookModel
{
public ICollection<string> Chapters { get; init; }
}
}
请注意,当前版本的 .NET SDK 在使用内置分析器(而非 NuGet 包)时仍会发出警告。这是一个已知的错误,应该在将来修复。