【问题标题】:Solution for CA2227 or better approach?CA2227 的解决方案还是更好的方法?
【发布时间】:2021-09-06 15:23:04
【问题描述】:

我仅使用代码分析来清理、组织和确保针对特定警告的所有实例全局执行这些更改。我进入决赛了,它是 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 属性都会给我这个警告,而且有数百个。使用上述类时,我在没有任何数据的情况下实例化它。然后在外部我添加数据并通过其公共访问器设置每个单独的变量。我没有使用构造函数方法准备和传递所有数据来实例化这个类(IMO 对于这些可以达到的大小很容易对眼睛造成严重破坏)。完成并分配所有属性后,该类作为一个整体用于生成它所代表的文档部分。

我的问题是,对于上述用法,正确设置它的更好方法是什么?我是保留公共访问者并完全禁止此警告,还是有完全不同的解决方案可行?

【问题讨论】:

  • 什么是设置属性?你能让二传手privateinternal 吗?如果不是,我只会取消警告。
  • 你真的在课外设置为PO1Loop.PIDRepeat1 = new Collection&lt;SegmentTypes.PO1LoopSegmentTypes.PID1&gt;();吗?如果您只是集合中的addingremovingaltering 元素,则删除不必要的set
  • 您可以只公开公共吸气剂并在您拥有值时对它们执行AddRange。更好的选择是忽略此建议。
  • 该错误的 MSDN 页面给出了它背后的基本原理以及如何实施以避免它的建议。 msdn.microsoft.com/en-us/library/…
  • @EBrown 我是课外设置。这个想法是拥有一个组成一组特定 EDI 文档的组件库。从处理和生成这些文档的项目中,它被“填充”

标签: c# visual-studio code-analysis


【解决方案1】:

下面是 MSDN 关于错误的说明,以及如何避免它。

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

考虑以下类:

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

这个类会抛出完全相同的问题。为什么?因为Collections 不需要需要一个二传手。现在,我们可以用该对象做任何事情:将Data 分配给任意List&lt;string&gt;,将元素添加到Data,从Data 中删除元素,等等。如果我们删除@987654329 @,我们只会失去直接分配给该属性的能力。

考虑以下代码:

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&lt;string&gt; 是对内存位置的引用,它包含剩余的数据。

现在,唯一你现在不能用它做的事情是直接设置Data属性。 IE。以下内容无效:

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(总的来说,这确实是一个更好的选择。)您可以在this StackOverflow question 上找到有关它的信息。

【讨论】:

  • 谢谢,这确实有助于缓解我对以我的方式实施此解决方案的困扰。我觉得更舒服,因为这是使用不可变类型的正确方法..
  • @DavidCarrigan 是的,我会在下班回家后发布序列化集合属性的示例。这也应该能缓解你对这个主题的一些感受。
  • @DavidCarrigan 我添加了有关序列化此类对象的信息。
  • 这是一个关于序列化的有趣花絮,实际上帮助我完成了另一个项目。我觉得您的解决方案可以轻松扩展,同时避免维护噩梦。再次感谢!
  • @DavidCarrigan 没问题。由于这样的原因,我倾向于过度回答。很高兴您发现所有这些对您有帮助。
【解决方案2】:

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

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

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

反序列化之所以有效,是因为 List 有一个“Add”方法,并且序列化程序知道如何使用 Add 方法处理只读集合属性(该属性是只读的,但不是元素)(我使用 Json .Net,其他序列化程序的行为可能不同)。

编辑: 正如所指出的,它应该是“=”而不是“=>”(编译器会阻止你使用“=>”)。如果我们使用“public List Data => new List();”那么它会在每次访问该属性时创建一个新列表,这也不是我们想要的。

编辑: 请注意,如果属性的类型是接口,这将不起作用,例如IList

编辑: 我认为接口的处理是由使用的序列化程序决定的。以下工作完美。我确信所有常见的序列化程序都知道如何处理 ICollection。如果您有一些未实现 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());
    }

【讨论】:

  • 我用过这个,谢谢你的帮助,但我花了一段时间才弄清楚=&gt;应该是=
  • 也适用于 VS2017。
【解决方案3】:

? 替代解决方案 ?

在我的情况下,将属性设置为只读是不可行的,因为整个列表(作为参考)可能会更改为新列表。

我可以通过将属性的设置器范围更改为internal 来解决此警告。

public List<Batch> Batches
{
    get { return _Batches; }
    internal set { _Batches = value; OnPropertyChanged(nameof(Batches)); }
}

注意也可以使用private set...


此警告的提示(achilleas愈)似乎确实指向了文档中的库(Bolding mine):

外部可见可写属性是一种实现 System.Collections.ICollection.

对我来说,“好的,我不会让它在外部可见....”并且internal 适合该应用程序。

【讨论】:

    【解决方案4】:

    感谢 @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 或任何其他修改集合的方法。

    【讨论】:

      【解决方案5】:

      只有在绑定 DTO 时,您才需要禁止显示警告。 否则需要自定义 ModelBinder 自定义 ModelBinder 来绑定集合。

      引用规则文档:

      何时禁止显示警告

      如果属性是数据传输对象 (DTO) 类的一部分,您可以禁止显示警告。
      否则,不要禁止来自该规则的警告。

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

      【讨论】:

        【解决方案6】:

        DTO 通常需要序列化和反序列化。因此,它们必须是可变的。

        必须创建备用支持属性是一件很痛苦的事情。
        只需将属性类型从 List&lt;string&gt; 更改为 IReadOnlyList&lt;string&gt; 即可在没有 CA2227 的情况下按预期工作。

        集合是通过属性设置的,但如果您希望追加或删除项目,也可以强制转换为 List&lt;string&gt;

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

        【讨论】:

          【解决方案7】:

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

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

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

          现在将清除现有列表并用另一个列表中的新项目填充,而不是创建一个完整的新列表,作为参数传入。原始列表,由于它是只读的并且只创建一次,将永远保留。

          我相信这也是避免垃圾收集器必须删除超出范围的旧对象以及创建新的收集对象(尽管已经有一个)的好方法。

          【讨论】:

            【解决方案8】:

            作为 Der Kommissar 出色答案的补充。

            从 .NET 5 (C# 9.0) 开始有 init-only properties。这些属性只能在特定情况下设置,参考here

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

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

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

            【讨论】:

              【解决方案9】:

              涵盖解决 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>();
              }
              

              【讨论】:

              • 您可能想更好地解释您的答案。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-08-05
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-02-23
              • 1970-01-01
              相关资源
              最近更新 更多