【问题标题】:Create an ImmutableArray without copying创建一个 ImmutableArray 而不复制
【发布时间】:2018-07-12 07:49:05
【问题描述】:

有没有什么方法(可能是一个肮脏的hack)来创建一个只使用指定数组而不是复制它的 ImmutableArray?

我有一个我知道不会更改的数组,我想创建一个 ImmutableArray 以允许客户端安全地访问我的数组。

source code for ImmutableArray,我看Create方法也无济于事:

public static ImmutableArray<T> Create<T>(params T[] items)
{
    if (items == null)
    {
        return Create<T>();
    }

    // We can't trust that the array passed in will never be mutated by the caller.
    // The caller may have passed in an array explicitly (not relying on compiler params keyword)
    // and could then change the array after the call, thereby violating the immutable
    // guarantee provided by this struct. So we always copy the array to ensure it won't ever change.
    return CreateDefensiveCopy(items);
}

编辑:在 GitHub 上有一个关于此功能的请求,这也提供了最快的 hack 使用:https://github.com/dotnet/corefx/issues/28064

【问题讨论】:

  • 如果你知道你的元素不会改变,为什么不简单地使用一个普通的数组呢?
  • 您为什么要避免复制?这是铸造性能问题吗?您是否过早优化?
  • @sweeper。表现。它用于矢量库,因此必须快速。通过 vector.Add 中的数组进行额外迭代会使性能减半
  • @HimBromBeere。因为我需要为客户端代码授予对数组的只读访问权限
  • 返回一个 IReadOnlyList 怎么样?

标签: c# optimization immutable-collections


【解决方案1】:

如果您知道数组的确切长度,您可以使用ImmutableArray.CreateBuilder&lt;&gt; 加上.MoveToImmutable(),这将在不复制Builder 的内部创建ImmutableArray&lt;&gt;

var builder = ImmutableArray.CreateBuilder<int>(4);
builder.Add(1);
builder.Add(2);
builder.Add(3);
builder.Add(4);
ImmutableArray<int> array = builder.MoveToImmutable();

如果builder.Capacity != builder.Count.MoveToImmutable()方法会抛出异常

请注意,构建器的其他方法(如.ToImmutable())将创建数组的副本。

【讨论】:

  • 从头开始构建新的不可变数组时,这是最好的解决方案。但是,当已经有一个数组只需要使其不可变时,这无济于事,因为它仍然需要复制该数组。可能值得在答案中包含此信息。
【解决方案2】:

还有另外两种 hacky 方法,都在这里建议:https://stackoverflow.com/a/3799030/4418060(一个在回答,一个在评论)。

  1. 将一种结构类型编组为另一种。
  2. 不安全地将一个转换为另一个。

第一个涉及创建一个反映 ImmutableArray 布局的新结构类型(它是单个 T[] 字段)并更改 CLR(运行时)看到的该结构的类型。结构如下所示:

public struct HackImmutableArray<T>
{
    public T[] Array;
}
  1. 编组:

    static ImmutableArray<T> HackyMakeImmutable<T>(T[] array)
    {
        var arrayObject = (object)new HackImmutableArray<T> { Array = array };
        var handle = GCHandle.Alloc(arrayObject, GCHandleType.Pinned);
        var immutable = (ImmutableArray<T>)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
        handle.Free();
        return immutable;
    }
    
  2. 不安全的铸造(好帮手written here,在this blog post 中找到)。 Casting 使用Unsafe System.Runtime.CompilerServices.Unsafe NuGet 中提供的静态类

    using System.Runtime.CompilerServices;
    
    static ImmutableArray<T> HackyMakeImmutable<T>(T[] array)
    {
        return Unsafe.As<T[], ImmutableArray<T>>(ref array);
    }
    

第二个选项“不安全”但相当安全,因为我们可以肯定地假设 ImmutableArray 的结构布局不会改变,这是一个定义特性,而且它也可能比任何其他解决方案都快得多。

【讨论】:

    【解决方案3】:

    https://github.com/dotnet/corefx/issues/28064 他们推荐最快的方法是使用 System.Runtime.CompilerServices.Unsafe:

    ImmutableArray<T> im = Unsafe.As<T[], ImmutableArray<T>>(ref array);
    

    【讨论】:

      【解决方案4】:

      这可能是个坏主意,他们可以用同样的伎俩对付你,但你可以用反射来作弊:

      public static ImmutableArray<T> GetImmutableArray<T>(T[] arr)
      {
          var immutableArray = ImmutableArray.Create(new T[0]);
          var boxed = ((object) immutableArray);
          var t = boxed.GetType();
          var fi = t.GetField("array", BindingFlags.NonPublic | BindingFlags.Instance);
          fi.SetValue(boxed, arr);
          return (ImmutableArray<T>)boxed;
      }
      

      然后这样称呼它:

      var arr = new int[] { 1, 2, 3 };
      Console.WriteLine("Arr: " + string.Join(",", arr)); //Arr: 1,2,3
      var imm = GetImmutableArray(arr);
      Console.WriteLine("ImmutableArray: " + string.Join(",", imm)); //ImmutableArray: 1,2,3
      arr[0] = 234;
      imm[0] = 235; //Compile Error
      Console.WriteLine("ImmutableArray: " + string.Join(",", imm)); //ImmutableArray: 234,2,3
      

      反射成本必须与 Array.Copy 成本进行权衡。

      【讨论】:

      • @MineR 我喜欢坏主意。总是让我觉得自己更像一个程序员;-)。谢谢你。除非真的有必要,否则我会保留它,但它可能会派上用场。
      【解决方案5】:

      您需要ImmutableArray&lt;T&gt; 还是IReadOnlyList&lt;T&gt; 就足够了?如果是后者,您总是可以实现一个非常轻量级的数组包装器来满足您的需求:

      public class ImmutableArrayWrapper<T>: IReadOnlyList<T>
      {
           public static ImmutableArrayWrapper<T> Wrap(T[] array)
               => new ImmutableArrayWrapper(array);
      
          private readonly T[] innerArray;
      
          private ImmutableArrayWrapper(T[] arr) {
               if (arr == null)
                   throw new ArgumentNullException();
      
               innerArray = arr; }
      
          public int Count => innerArray.Count();
          public T this[int index] => innerArray[index];
      
          //IEnumerable<T>...
      }
      

      现在您可以安全地将包装器传递给您的客户端。

      【讨论】:

      • 我建议使用像 ImmutableArray 这样的结构,因为这意味着与数组相比,它的开销为零
      • @YairHalberstadt 使用ImmutableArray 的问题是Create 复制了数组内容;这就是 OP 想要避免的开销。
      • 我是操作者 ;-)。我的意思是使用结构而不是类,就像不可变数组一样
      • @YairHalberstadt 大声笑,对不起。好吧,如果使用结构是必须的性能,那么这个解决方案就行不通了;如果您使用 IReadOnlyListz&lt;T&gt; 引用,则将包装器更改为结构没有帮助。
      【解决方案6】:

      ReadOnlyCollection&lt;T&gt; 在许多情况下可用于实现相同目的。它不提供对原始数组的访问权限——Items 属性受到保护。

      不过,使用ReadOnlyCollection&lt;T&gt; 而不是ImmutableArray&lt;T&gt; 有两个缺点:

      1. 发生额外分配。 ReadOnlyCollection&lt;T&gt; 是一个封装了IList&lt;T&gt; 的类,而ImmutableArray&lt;T&gt; 是一个封装了T[] 的结构体。
      2. 集合的接收者对不变性的保证较弱。集合无法从外部修改,但创建它的人仍可能持有对原始数组的引用并可以使用它来修改集合。

      可以通过文档来保证不会发生此类修改,但它仍然比技术上强制执行的保证要弱一些。

      【讨论】:

      • “集合的接收者对不可变性的保证较弱。”更像是“集合的接收者没有保证不可变性”恕我直言。
      • 有很多类型的不变性,@TheodorZoulias。 docs.microsoft.com/en-us/archive/blogs/ericlippert/…
      • Eric 可能忘了提到另外两种不变性。 常规不变性:你将后缀Immutable附加到数组的名称中,并希望调用者得到备忘录。 Remarked immutability: 你在&lt;remarks&gt; 部分写了数组不会被调用者改变,也不应该被调用者改变,否则小狗和小猫会死。
      • 言归正传,Eric 谈到了不变性的种类,而不是不变性的。但是现在我正在考虑它,谈论关于不变性的更弱和更强的保证是有道理的。通过混淆器传递的集合可能比未混淆的集合更不可变,因为通过反射来改变它更难。一个被厚厚的铅罩覆盖的机器中的集合比我的 PC 中的集合更不可变,因为它更难被高能宇宙射线变异。
      猜你喜欢
      • 2012-04-27
      • 2021-03-26
      • 1970-01-01
      • 1970-01-01
      • 2013-05-27
      • 2015-09-25
      • 2020-10-26
      • 1970-01-01
      • 2011-08-29
      相关资源
      最近更新 更多