解决方案CA2227或更好的方法

本文关键字:方法 更好 CA2227 解决方案 | 更新日期: 2023-09-27 17:50:14

我只使用Code Analysis来清理、组织和确保对特定警告的所有实例全局执行这些更改。我晋级决赛了,成绩是CA2227。

CA2227集合属性应该是只读的通过移除属性设置器实现只读。

注意,这是用于EDI文档的映射。这些类表示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; }
}

你可以看到所有的Collection属性都会给我这个警告,它们有几百个。当使用上面的类时,我在没有任何数据的情况下实例化它。然后在外部添加数据并通过公共访问器设置每个单独的变量。我没有使用构造函数方法准备和传递的所有数据来实例化这个类(对于这些数据的大小来说,它很容易对眼睛造成严重破坏)。当完成并分配了所有属性后,将使用类作为一个整体来生成它所代表的文档的那一部分。

我的问题是,对于上面描述的用法,正确设置它的更好方法是什么?我应该保留公共访问器并完全抑制此警告,还是有一个完全不同的解决方案可以工作?

解决方案CA2227或更好的方法

以下是MSDN对错误的解释,以及如何避免错误。

这是我对这个问题的看法。

考虑以下类:

class BigDataClass
{
    public List<string> Data { get; set; }
}

这个类将抛出完全相同的问题。为什么?因为Collections 需要setter。现在,我们可以对该对象执行任何操作:将Data分配给任意List<string>,向Data添加元素,从Data删除元素,等等。如果我们删除了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属性。也就是说,以下内容无效:

var bigData = new BigDataClass();
bigData.Data = new List<string>();

不一定是坏事。您会注意到在许多 . net类型中使用了该模型。这是不变性的基本原理。通常不希望直接访问Collections的可变性,因为这可能导致一些具有奇怪问题的意外行为。这就是为什么Microsoft建议省略setter。

的例子:

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上,因此不能可靠地确定是否添加了新值或删除了值。

一个可写的集合属性允许用户用一个完全不同的集合替换这个集合。

本质上,如果您只每次需要运行构造函数或赋值一次,则省略set修饰符。你不需要它,直接赋值集合是违反最佳实践的。

现在,我并不是说永远不要在Collection上使用setter,有时你可能需要一个,但通常你不应该使用它们。

你总是可以在Collections上使用.AddRange, .Clone等,你只有失去了direct assignment的能力。

序列化

最后,如果我们希望SerializeDeserialize是一个包含Collection而没有set的类,我们该怎么做?好吧,总是有不止一种方法可以做到这一点,最简单的(在我看来)是创建一个表示序列化集合的property

BigDataClass为例。如果我们希望Serialize,然后用下面的代码Deserialize这个类,那么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(这确实是一个更好的选择,一般来说。)你可以在这个StackOverflow问题上找到关于它的信息。

对于当前的VS2019,我们可以简单地这样做:

public List<string> Data { get; } = new List<string>();

满足CA2227,可以序列化/反序列化。

反序列化之所以有效是因为List<>有一个"添加";方法,序列化器知道如何用Add方法处理只读集合属性(该属性是只读的,但不是元素)(我使用Json。,其他序列化器的行为可能不同)。

编辑:

应该是"="而不是"=>"(编译器会阻止你使用"=>")。如果我们使用"public List Data =>新列表();"然后,每次访问属性时,它都会创建一个新列表,这也不是我们想要的。

编辑:

注意,如果属性的类型是一个接口,比如IList

,这将不起作用。编辑:

我认为接口的处理是由所使用的序列化器决定的。下面的工作完美。我确信所有常见的序列化器都知道如何处理iccollection。如果您有一些不实现ICollection的自定义接口,那么您应该能够配置序列化器来处理它,但在这种情况下,CA2227可能不会被触发,因此它在这里无关紧要。(因为它是一个只读属性,你必须在类中分配一个具体的值,所以它应该总是序列化和反序列化一个非空值)

    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)似乎真的指向了库,因为文档说(Bolding mine):

外部可见的 writable属性是实现的类型System.Collections.ICollection .

对我来说,"好吧,我不会让它在外部可见...."和internal是很好的应用程序。

只有在绑定DTO时,你需要抑制警告。否则需要自定义ModelBinder来绑定集合。

引用规则文档:

何时抑制警告

如果属性是数据传输对象(Data Transfer Object, DTO)类的一部分,则可以抑制警告。
否则,不要抑制来自该规则的警告。

https://learn.microsoft.com/pt-br/visualstudio/code-quality/ca2227?view=vs-2019

感谢@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>();
    }
}

当想要将数据赋值给集合类型时,使用adrange, Clear或任何其他修改集合方法的变体。

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)开始,有了只允许初始化的属性。这些属性只能在特定情况下设置,请参考此处。

下面的示例不应该引发警告CA2227,但仍然允许在对象初始化期间设置集合。

using System.Collections.Generic;
namespace BookStore
{
    public class BookModel
    {
        public ICollection<string> Chapters { get; init; }
    }
}

请注意,当前版本的。net SDK在使用内置分析器(而不是NuGet包)时仍然会发出警告。这是一个已知的错误,应该在将来修复

我必须修复一些CA2227违规,所以我必须将"readonly"关键字添加到集合字段,然后当然,必须删除setter属性。一些使用setter的代码只是创建了一个新的集合对象,它最初是空的。这段代码肯定没有编译,所以我不得不添加一个SetXxx()方法来实现丢失的setter的功能。我是这样做的:

public void SetXxx(List<string> list)
{
    this.theList.Clear();
    this.theList.AddRange(list);
}

使用setter的调用者的代码已被替换为对SetXxx()方法的调用。

现在不再创建一个完整的新列表,而是将清除现有列表并填充来自另一个列表的新项,并将其作为参数传入。原来的列表,由于它是只读的并且只创建一次,因此将始终保留。

我相信这也是避免垃圾收集器必须删除超出作用域的旧对象的好方法,其次,创建新的收集对象(尽管已经有一个)。

涵盖解决CA2227错误的所有可能场景:这涵盖了我们使用实体框架时的实体关系映射。

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