创建一个不复制的 ImmutableArray
Create an ImmutableArray without copying
有什么方法(可能是肮脏的 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 上正好有此功能的请求,这也提供了最快的破解方法:https://github.com/dotnet/corefx/issues/28064
如果您知道数组的确切长度,您可以使用 ImmutableArray.CreateBuilder<>
加上 .MoveToImmutable()
,这将从 Builder
的内部创建一个 ImmutableArray<>
不复制它:
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()
)将创建数组的副本。
这可能是个坏主意,他们可以用同样的把戏来对付你,但你可以用反思作弊:
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 成本进行权衡。
你需要 ImmutableArray<T>
还是 IReadOnlyList<T>
就够了?如果是后者,你总是可以实现一个非常轻量级的数组包装器来满足你的需要:
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>...
}
现在您可以将包装器安全地传递给您的客户了。
在 https://github.com/dotnet/corefx/issues/28064 他们推荐最快的方法是使用 System.Runtime.CompilerServices.Unsafe:
ImmutableArray<T> im = Unsafe.As<T[], ImmutableArray<T>>(ref array);
还有另外两种 hacky 方法,均在此处建议:(一种在回答中,一种在评论中)。
- 将一种结构类型编组为另一种结构类型。
- 不安全地将一个转换为另一个。
第一个涉及创建一个反映 ImmutableArray 布局的新结构类型(它是一个 T[]
字段)并更改该结构的类型,如 CLR(运行时)所见。该结构如下所示:
public struct HackImmutableArray<T>
{
public T[] Array;
}
编组:
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;
}
不安全的转换(好帮手written here, found in this blog post). Casting uses Unsafe
static class available in System.Runtime.CompilerServices.Unsafe NuGet
using System.Runtime.CompilerServices;
static ImmutableArray<T> HackyMakeImmutable<T>(T[] array)
{
return Unsafe.As<T[], ImmutableArray<T>>(ref array);
}
第二个选项是 "not safe" 但非常安全,因为我们可以肯定地假设 ImmutableArray 的结构布局不会更改,这是一个定义性功能,而且它也可能比任何其他解决方案都快得多。
在许多情况下,ReadOnlyCollection<T>
可用于实现相同的目的。它允许访问原始数组是不正确的——Items
属性 是受保护的。
使用 ReadOnlyCollection<T>
而不是 ImmutableArray<T>
有两个缺点:
- 发生额外分配。
ReadOnlyCollection<T>
是 class 包装 IList<T>
,而 ImmutableArray<T>
是包装 T[]
. 的结构
- 集合的接收者对不变性的保证较弱。集合不能从外部修改,但创建它的人可能仍然持有对原始数组的引用,并可以使用它来修改集合。
文档可以提供不会发生此类修改的保证,但与技术上强制执行的保证相比,它仍然是一种较弱的保证。
有什么方法(可能是肮脏的 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 上正好有此功能的请求,这也提供了最快的破解方法:https://github.com/dotnet/corefx/issues/28064
如果您知道数组的确切长度,您可以使用 ImmutableArray.CreateBuilder<>
加上 .MoveToImmutable()
,这将从 Builder
的内部创建一个 ImmutableArray<>
不复制它:
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()
)将创建数组的副本。
这可能是个坏主意,他们可以用同样的把戏来对付你,但你可以用反思作弊:
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 成本进行权衡。
你需要 ImmutableArray<T>
还是 IReadOnlyList<T>
就够了?如果是后者,你总是可以实现一个非常轻量级的数组包装器来满足你的需要:
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>...
}
现在您可以将包装器安全地传递给您的客户了。
在 https://github.com/dotnet/corefx/issues/28064 他们推荐最快的方法是使用 System.Runtime.CompilerServices.Unsafe:
ImmutableArray<T> im = Unsafe.As<T[], ImmutableArray<T>>(ref array);
还有另外两种 hacky 方法,均在此处建议:(一种在回答中,一种在评论中)。
- 将一种结构类型编组为另一种结构类型。
- 不安全地将一个转换为另一个。
第一个涉及创建一个反映 ImmutableArray 布局的新结构类型(它是一个 T[]
字段)并更改该结构的类型,如 CLR(运行时)所见。该结构如下所示:
public struct HackImmutableArray<T>
{
public T[] Array;
}
编组:
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; }
不安全的转换(好帮手written here, found in this blog post). Casting uses
Unsafe
static class available in System.Runtime.CompilerServices.Unsafe NuGetusing System.Runtime.CompilerServices; static ImmutableArray<T> HackyMakeImmutable<T>(T[] array) { return Unsafe.As<T[], ImmutableArray<T>>(ref array); }
第二个选项是 "not safe" 但非常安全,因为我们可以肯定地假设 ImmutableArray 的结构布局不会更改,这是一个定义性功能,而且它也可能比任何其他解决方案都快得多。
ReadOnlyCollection<T>
可用于实现相同的目的。它允许访问原始数组是不正确的——Items
属性 是受保护的。
使用 ReadOnlyCollection<T>
而不是 ImmutableArray<T>
有两个缺点:
- 发生额外分配。
ReadOnlyCollection<T>
是 class 包装IList<T>
,而ImmutableArray<T>
是包装T[]
. 的结构
- 集合的接收者对不变性的保证较弱。集合不能从外部修改,但创建它的人可能仍然持有对原始数组的引用,并可以使用它来修改集合。
文档可以提供不会发生此类修改的保证,但与技术上强制执行的保证相比,它仍然是一种较弱的保证。