1

I have some read-only variables set with Set-Variable myVar -Value 10 -Option ReadOnly syntax but I need read-only arrays too. How do I make read-only arrays?

Thanks

mklement0
  • 382,024
  • 64
  • 607
  • 775

1 Answers1

2

PetSerAl, as countless times before, has provided the solution in a terse comment on the question; he's also helped to improve this answer:
Use [Array]::AsReadOnly($someArray), available in PSv3+[1] :

# Create a 2-element read-only collection containing strings.
$readOnlyColl = [Array]::AsReadOnly(('one', 'two'))

# Try to modify an element, which now fails:
$readOnlyColl[0] = 'uno'

In PSv2, use a cast to [System.Collections.ObjectModel.ReadOnlyCollection[<type>]], as shown below.

The above yields the following, demonstrating that elements cannot be modified:

Unable to index into an object of type System.Collections.ObjectModel.ReadOnlyCollection`1[System.String].

Note that the error message is somewhat misleading, because it is only write access via indexing that isn't allowed, read access (e.g., $readOnlyColl[0]) works just fine.

While $readOnlyColl is not strictly an array, it behaves like one for all practical purposes.
Technically, $readOnlyColl is an instance of generic type System.Collections.ObjectModel.ReadOnlyCollection`1 using an element type inferred from the input array's elements.[2]
Caveat: The return collection is only a wrapper around an array and, depending on the specific input array and casts you apply, later modifications to the input array's elements could be reflected in the wrapper collection.[3]


If you want to control the elements' data type explicitly, use a cast.

E.g., to create a [string]-typed collection from [int] values:

# Cast to an array of the desired type first.
[Array]::AsReadOnly([string[]] (1, 2, 3)

# Alternatively, cast to the target collection type directly.
# Always use this in PSv2, where [Array]::AsReadOnly() cannot be called.
[System.Collections.ObjectModel.ReadOnlyCollection[string]] (1, 2, 3)

Use [object[]] / [object] for [object]-typed elements, as with regular PowerShell arrays, but note that if the input array is indeed already an [object[]] array, the resulting collection will be a wrapper around the array - see footnote [2].


Just to spell out why Set-Variable myVar -Option ReadOnly is not enough to create a read-only array: it makes the variable read-only, meaning that you cannot assign a different value to it; by contrast, modifying a property of the data that happens to be stored in the variable - such as an array's element - is not prevented.


[1] The method has been available since .NET v2, on which PSv2 is built; however, only PSv3 introduced the ability to call generic methods directly, so PSv3+ is required; however, in PSv2 you can cast to [System.Collections.ObjectModel.ReadOnlyCollection[<type>]] directly.

[2] That is, even though PowerShell creates [object[]] arrays by default, if the actual elements happen to be all of the same type, PowerShell chooses that specific type rather than [object]; in the case at hand, because both elements of the input array were strings, a new [string[] typed array was created behind the scenes, which the resulting read-only collection wraps as type [System.Collections.ObjectModel.ReadOnlyCollection[string]].

[3] Because the collection is only a wrapper around the input array, anyone with access to the input array could still modify its elements and the wrapper collection would reflect that change.
In PowerShell, however, you're often shielded from this potential problem if PowerShell happens to create a new array behind the scenes that it then passes to [Array]::AsReadOnly(). A new array is created for PS arrays ([object[]]) whose elements all happen to have the same type, assuming you don't explicitly cast such an array to [object[]] in the [Array]::AsReadOnly() call. For a specifically typed input array (e.g., [int[]]), a new array is only created if you cast to a different type (e.g., [string[]]); demonstration of the wrapper problem if no new array is created:
$arr = [int[]] (1..3); $coll = [Array]::AsReadOnly($arr); $arr[1] = 42; $coll[1] - $coll[1] now reflects 42, i.e., the changed value that was assigned via the underlying array, $a.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • One thing to be aware of: `ReadOnlyCollection` is a wrapper not a copy. So, unless PowerShell does the copy, `ReadOnlyCollection` is connected to original array: `$a = 1..3; $b = [Array]::AsReadOnly([object[]]$a); $b[1]; $a[1] = 42; $b[1]`. Also, one interesting thing: it is enough to wrap array into `Collection` for it to be read-only: `([System.Collections.ObjectModel.Collection[object]] (1, 2, 3)).IsReadOnly`. – user4003407 Aug 16 '18 at 17:15
  • Thanks for the wrapper info, @PetSerAl: I've added it to the answer, which I've also restructured slightly. – mklement0 Aug 16 '18 at 18:42
  • @PetSerAl: Re `[System.Collections.ObjectModel.Collection[object]]` being enough to make the collection read-only. That is not only surprising, but also seems undesirable - a bug? Note that `.IsReadOnly`, stemming from the `ICollection` interface, only indicates that the collection as a whole is read-only, it makes no guarantees regarding the mutability of its _elements_. `Get-Member` shows the `.Item` property as settable: `Item ParameterizedProperty System.Object Item(int index) {get;set;}`. Do you have an explanation. – mklement0 Aug 16 '18 at 18:46
  • `Get-Member` only report that there is a setter, because `Collection` have element setter. But that setter can just [throw an exception](https://referencesource.microsoft.com/#mscorlib/system/collections/objectmodel/collection.cs,50) instead of setting the element. It was good Connect issue describing this behavior, but as Connect now gone, I can not link to it. – user4003407 Aug 16 '18 at 19:40
  • @PetSerAl: When you cast an array to `Collection`, its generic `IList` interface is wrapped. However, [`Collection`'s indexer explicitly prevents _set_ indexing attempts based on querying the `.IsReadOnly` property](https://source.dot.net/#System.Private.CoreLib/shared/System/Collections/ObjectModel/Collection.cs,49), which returns `$True` for arrays via the [`IList` interface](https://source.dot.net/#System.Private.CoreLib/src/System/Array.cs,2568) (as opposed to [via the _non-generic_ `[IList]` interface](https://source.dot.net/#System.Private.CoreLib/src/System/Array.cs,576)) – mklement0 Aug 16 '18 at 22:44
  • @PetSerAl: As an aside: I recently created an answer to a related question that I'd like your input on, if you're up for it: https://stackoverflow.com/a/51663135/45375 – mklement0 Aug 16 '18 at 22:46