Yair Halberstadt
Yair Halberstadt

Reputation: 6841

Create an ImmutableArray without copying

Is there any way (possibly a dirty hack) to create an ImmutableArray which will just use a specified array, instead of copying it over?

I have an array which I know won't change, and I want to create an ImmutableArray to allow clients to access my array safely.

Looking at the source code for ImmutableArray, I see the Create method will not help:

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

Edit: There is a request for exactly this feature on GitHub, which also gives the quickest hack to use: https://github.com/dotnet/corefx/issues/28064

Upvotes: 9

Views: 4033

Answers (8)

Gabriel Vanca
Gabriel Vanca

Reputation: 1

 private static readonly ImmutableArray<Key> FinisherKeys = new[]
 {
     Key.Down,
     Key.Return,
     Key.Enter,
 }.ToImmutableArray();

or

  private static readonly ImmutableArray<Key> FinisherKeys = [
            ..new[]
                {
                    Key.Down,
                    Key.Return,
                    Key.Enter,
                },
        ];

Upvotes: -2

Misha Zaslavsky
Misha Zaslavsky

Reputation: 9666

In .NET 8 preview no need to use Unsafe anymore.

They exposed some additional methods to access the internal constructor.

Use ImmutableCollectionsMarshal.AsImmutableArray<T>:

ImmutableArray<T> im = ImmutableCollectionsMarshal.AsImmutableArray(array);

Upvotes: 9

Palec
Palec

Reputation: 13581

ReadOnlyCollection<T> could be used to achieve the same purpose in many cases. It is not true it gives access to the original array -- the Items property is protected.

There are two downsides to using ReadOnlyCollection<T> instead of ImmutableArray<T>, though:

  1. An extra allocation takes place. ReadOnlyCollection<T> is a class wrapping an IList<T>, while ImmutableArray<T> is a struct wrapping an T[].
  2. The receiver of the collection has weaker guarantees about immutability. The collection cannot be modified from the outside, but whoever created it may still hold a reference to the original array and may use it to modify the collection.

The guarantee that no such modifications will take place can be provided by documentation, but it is still a somewhat weaker guarantee than the technically enforced one.

Upvotes: 1

xanatos
xanatos

Reputation: 111890

If you know the exact length of the array, you can use the ImmutableArray.CreateBuilder<> plus the .MoveToImmutable() that will create an ImmutableArray<> from the internals of the Builder without copying it:

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

The method .MoveToImmutable() will throw an exception if builder.Capacity != builder.Count

Note that other methods of the builder (like .ToImmutable()) will create a copy of the array.

Upvotes: 17

Amiś
Amiś

Reputation: 189

There is also another two hacky approaches, both suggested here: https://stackoverflow.com/a/3799030/4418060 (one in answer, one in comment).

  1. Marshal one struct type to another.
  2. Unsafely cast one to another.

First one involves creating a new struct type that mirrors layout of ImmutableArray (which is a single T[] field) and changing the type of that struct as seen by CLR (runtime). The struct would look like this:

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

    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. Unsafe casting (nice helpers 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);
    }
    

The second option is "not safe" but quite safe, as we can with certainty assume ImmutableArray's struct layout not to change, being a defining feature, and it'll also be probably much faster than any other solution.

Upvotes: 5

Yair Halberstadt
Yair Halberstadt

Reputation: 6841

At https://github.com/dotnet/corefx/issues/28064 They recommend the fastest way is using System.Runtime.CompilerServices.Unsafe:

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

Upvotes: 4

InBetween
InBetween

Reputation: 32770

Do you need an ImmutableArray<T> or is an IReadOnlyList<T> enough? If its the latter, you can always implement a very lightweight array wrapper that meets your needs:

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>...
}

And now you can pass the wrapper to your client safely.

Upvotes: 0

MineR
MineR

Reputation: 2204

This is probably a bad idea, and they can use the same trick against you, but you can cheat with reflection:

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

And call it like this:

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

The reflection cost would have to be weighed against the Array.Copy cost.

Upvotes: 1

Related Questions