【问题标题】:Are immutable arrays possible in .NET?.NET 中是否可以使用不可变数组?
【发布时间】:2023-03-19 19:45:01
【问题描述】:

是否有可能以某种方式将System.Array 标记为不可变。当放在 public-get/private-set 后面时,它们不能被添加,因为它需要重新分配和重新分配,但消费者仍然可以设置他们希望的任何下标:

public class Immy
{
    public string[] { get; private set; }
}

我认为 readonly 关键字可以解决问题,但没有这样的运气。

【问题讨论】:

  • 该属性缺少名称,不是吗?
  • 该属性被称为“不可变”——从这个意义上说,字符串是不可变的(除非你在不安全的代码中访问 c​​har 指针,你真的不应该这样做,因为它们是被实习的。)
  • 请记住,不可变数组仅与其成员一样不可变。 @PhilWhittington 他在代码中的意思是,该属性缺少名称。它在语法上不正确。不过,这并不是一个真正可行的例子。
  • 我知道这是几年前的事了,但 Svish 指出,你应该有 public string[] variablename { ... } 他是说,该属性缺少名称...跨度>
  • 也许它不再是实际的但有一个System.Collections.Immutable.ImmutableArray in.net

标签: c# .net arrays immutability


【解决方案1】:

Framework Design Guidelines 建议返回 Array 的副本。这样,消费者就无法更改数组中的项目。

// bad code
// could still do Path.InvalidPathChars[0] = 'A';
public sealed class Path {
   public static readonly char[] InvalidPathChars = 
      { '\"', '<', '>', '|' };
}

这些更好:

public static ReadOnlyCollection<char> GetInvalidPathChars(){
   return Array.AsReadOnly(InvalidPathChars);
}

public static char[] GetInvalidPathChars(){
   return (char[])InvalidPathChars.Clone();
}

示例直接来自书中。

【讨论】:

  • 第二个选项(char[])InvalidPathChars.Clone(); 确实返回了一个副本,但客户可以修改它,即他可以将他的副本用于自己的目的,而原始数组不受影响。
【解决方案2】:

ReadOnlyCollection&lt;T&gt; 可能是您正在寻找的。它没有Add() 方法。

【讨论】:

  • 更准确地说,它没有公开的 Add() 方法。它确实有一个,因为接口需要它。但是好的接口代码会在调用之前先检查.ReadOnly,因为这样做会导致异常。
  • 这与不可变数组并不完全相同——ReadOnlyCollection 不允许任何修改,但它只是包装了一个常规的List,创建者仍然可以更改它.
  • @HenryJackson 最后,.NET 中的所有数据都可以通过反射进行更改。因此,有意义的不变性只是使可变性不可访问的问题。对可变集合的只读包装器肯定会被视为不可变如果然后丢弃对可变集合的引用,从而使突变变得不可能。
  • @AndrewArnott:不幸的是,ReadOnlyCollection 和任何其他类似的框架类型都没有提供任何方法来保证实例的后备存储是不可变的(例如,因为包装器创建了原始数组的克隆,它拥有宇宙中任何地方的唯一参考)。在许多情况下,接收集合的代码应该创建快照,除非它是不可变的,在这种情况下它不应该打扰,但是没有办法确定是否需要快照。
  • 如果我没记错的话,不变性和只读是不一样的。
【解决方案3】:

请参阅基类库中的Immutable Collections Now Available(目前为预览版)。

【讨论】:

    【解决方案4】:

    您可以使用 Array.AsReadOnly 方法返回。

    【讨论】:

    • 只是为了添加信息 - ReadOnlyCollection 类没有像 AddAdd&lt;T&gt;RemoveRemove&lt;T&gt; 这样的 API 以确保只读性质。同时,它们的索引器也是只读的。如果我们尝试使用 set indexer,它会导致编译时错误 - char[] myCharArray = { '\"', '&lt;', '&gt;', '|' }; var b1 = Array.AsReadOnly(myCharArray); b1[0] = '\0'; //results in compile time error.
    【解决方案5】:

    出于这个确切原因,我相信最佳实践是在公共 API 中使用 IList 而不是数组。 readonly 将阻止在构造函数之外设置成员变量,但正如您所发现的,不会阻止人们在数组中分配元素。

    更多信息请参见Arrays Considered Somewhat Harmful

    编辑:数组不能是只读的,但可以通过 Array.AsReadOnly() 将它们转换为只读的 IList 实现,正如@shahkalpesh 指出的那样。

    【讨论】:

      【解决方案6】:

      .NET 倾向于避开数组,但最简单和最传统的用例除外。对于其他一切,有各种可枚举/集合实现。

      当您想将一组数据标记为不可变时,您将超越传统数组提供的功能。 .NET 提供了等效的功能,但在技术上不是以数组的形式。要从数组中获取不可变集合,请使用Array.AsReadOnly&lt;T&gt;

      var mutable = new[]
      {
          'a', 'A',
          'b', 'B',
          'c', 'C',
      };
      
      var immutable = Array.AsReadOnly(mutable);
      

      immutable 将是一个 ReadOnlyCollection&lt;char&gt; 实例。作为更一般的用例,您可以从任何通用的 IList&lt;T&gt; 实现创建 ReadOnlyCollection&lt;T&gt;

      var immutable = new ReadOnlyCollection<char>(new List<char>(mutable));
      

      请注意,它必须是通用实现;普通的旧IList 将不起作用,这意味着您不能在传统数组上使用此方法,它仅实现IList。这揭示了使用Array.AsReadOnly&lt;T&gt; 作为一种快速访问通常无法通过传统数组访问的通用实现的方法的可能性。

      ReadOnlyCollection&lt;T&gt; 将使您能够访问不可变数组所期望的所有功能:

      // Note that .NET favors Count over Length; all but traditional arrays use Count:
      for (var i = 0; i < immutable.Count; i++)
      {
          // this[] { get } is present, as ReadOnlyCollection<T> implements IList<T>:
          var element = immutable[i]; // Works
      
          // this[] { set } has to be present, as it is required by IList<T>, but it
          // will throw a NotSupportedException:
          immutable[i] = element; // Exception!
      }
      
      // ReadOnlyCollection<T> implements IEnumerable<T>, of course:
      foreach (var character in immutable)
      {
      }
      
      // LINQ works fine; idem
      var lowercase =
          from c in immutable
          where c >= 'a' && c <= 'z'
          select c;
      
      // You can always evaluate IEnumerable<T> implementations to arrays with LINQ:
      var mutableCopy = immutable.ToArray();
      // mutableCopy is: new[] { 'a', 'A', 'b', 'B', 'c', 'C' }
      var lowercaseArray = lowercase.ToArray();
      // lowercaseArray is: new[] { 'a', 'b', 'c' }
      

      【讨论】:

        【解决方案7】:

        唯一要补充的是数组暗示可变性。当你从一个函数返回一个数组时,你是在向客户端程序员建议他们可以/应该改变一些事情。

        【讨论】:

        • 这是一个约定还是有更具体的原因让客户期待这种行为?
        【解决方案8】:

        除了 Matt 的回答之外,IList 是一个完整的数组抽象接口,因此它允许添加、删除等。我不确定 Lippert 为什么似乎建议它作为需要不变性的 IEnumerable 的替代方案。 (编辑: 因为 IList 实现可以为那些变异方法抛出异常,如果你喜欢那种东西的话)。

        也许要记住的另一件事是列表中的项目也可能具有可变状态。如果你真的不希望调用者修改这样的状态,你有一些选择:

        确保列表中的项目是不可变的(如您的示例中:字符串是不可变的)。

        返回所有内容的深层克隆,因此在这种情况下,无论如何您都可以使用数组。

        返回一个接口,该接口授予对项目的只读访问权限:

        interface IImmutable
        {
            public string ValuableCustomerData { get; }
        }
        
        class Mutable, IImmutable
        {
            public string ValuableCustomerData { get; set; }
        }
        
        public class Immy
        {
            private List<Mutable> _mutableList = new List<Mutable>();
        
            public IEnumerable<IImmutable> ImmutableItems
            {
                get { return _mutableList.Cast<IMutable>(); }
            }
        }
        

        请注意,可从 IImmutable 接口访问的每个值本身必须是不可变的(例如字符串),或者是您即时创建的副本。

        【讨论】:

          【解决方案9】:

          您希望做的最好的事情就是扩展现有的集合来构建您自己的集合。最大的问题是它的工作方式必须与现有的每个集合类型不同,因为每次调用都必须返回一个新集合。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2020-08-12
            • 1970-01-01
            • 2015-11-28
            • 2018-01-25
            • 1970-01-01
            • 1970-01-01
            • 2016-09-16
            相关资源
            最近更新 更多