0

Let's say I have array Grid that was initialized with

$Grid = @(@(1..3), @(4..6), @(7..9))

and I want to change Grid[0][0] to value "Test" but I want to make it unchangeable, is there a way I could to that?

So far I've tried playing around with classes that allow read-only or constant declarations through the class as opposed to using New-Variable/Set-Variable but it doesn't affect the index itself but the individual element as in

$Grid[0][0] = [Array]::AsReadOnly(@(1,2,3))   
$Grid[0][0]                                   # 1 \n 2 \n 3
$Grid[0][0].IsReadOnly                        # True
$Grid[0][0] = "test"
$Grid[0][0]                                   # test

I assume this is due to $Grid[0][0] being read-only as opposed to constant and the behaviour I experienced supported that:

$test = [Array]::AsReadOnly(@(1,2,3,4))
$test[0]=1                                    # Errors
$test = "test"
$test                                         # test

$Grid = @(@(1..3), @(4..6), @(7..9))
$Grid[0][0] = [Array]::AsReadOnly(@(1,2,3))   
$Grid[0][0][0] = 1                            # Errors
$Grid[0][0] = "test"
$Grid[0][0]                                   # test

I'm not sure what to try next and I know that this is very simple with classes but I am not looking for that as a solution.

5
  • Regardless of how you convert the inner array to a read-only collection, the outer array ($grid) is still just a regular array. Given PowerShell's love for enumerating/flattening array expressions, this is going to become near-unreadable to construct as a literal - can you perhaps describe the underlying problem you're trying to solve instead? Commented Apr 9, 2021 at 18:07
  • @MathiasR.Jessen Sure! Sorry for the ambiguity, the underlying question is how can I define a constant element in a manner like $var = value as opposed to using Set-Variable and nv. I'm not sure how else to describe this in generality but if you have any specific questions I can answer them. Commented Apr 9, 2021 at 18:09
  • You can't, arrays in .NET have no such facility - you'll have to define a custom collection type for this purpose (you might be able to do so with PowerShell classes, but I'd suggest using C# instead) - again, knowing which real-life problem you're trying to solve might be helpful here :) Commented Apr 9, 2021 at 18:11
  • I see, thank you for the information! Commented Apr 9, 2021 at 18:12
  • @MathiasR.Jessen I've taken issue with the hideous array of arrays literal syntax before along with the flattening issue. If memory serves I got around it by using [ArrayList] instead. It didn't seem to have the flattening issue and was syntactically cleaner. Array Lists are otherwise pretty seamless, so it worked out. Commented Apr 9, 2021 at 20:01

1 Answer 1

1

You'll have to make both dimensions of your nested array read-only to prevent anyone from overwriting $grid[0]:

$grid = 
  [array]::AsReadOnly(@(
    ,[array]::AsReadOnly(@(1,2,3))
    ,[array]::AsReadOnly(@(3,2,1))
  ))

(the unary , is not a typo, it prevents PowerShell from "flattening" the resulting read-only collection)

Now $grid should behave as you expect:

$grid[0]         # 1,2,3
$grid[0][0]      # 1
$grid[0][0] = 4  # error
$grid[0] = 4     # error

If you want to be able to prevent writing to individual "cells", you'll have to define a custom type:

using namespace System.Collections

class Grid {
    hidden [int[,]] $data
    hidden [bool[,]] $mask

    Grid([int]$width,[int]$height){
        $this.mask = [bool[,]]::new($width, $height)
        $this.data = [int[,]]::new($width, $height)
    }

    [int]
    Get([int]$x,[int]$y)
    {
        if(-not $this.CheckBounds($x,$y)){
            throw [System.ArgumentOutOfRangeException]::new()
        }

        return $this.data[$x,$y]
    }

    Set([int]$x,[int]$y,[int]$value)
    {
        if(-not $this.CheckBounds($x,$y)){
            throw [System.ArgumentOutOfRangeException]::new()
        }

        if(-not $this.mask[$x,$y])
        {
            $this.data[$x,$y] = $value
        }
        else
        {
            throw [System.InvalidOperationException]::new("Cell [$x,$y] is currently frozen")
        }
    }

    Freeze([int]$x,[int]$y)
    {
        if(-not $this.CheckBounds($x,$y)){
            throw [System.ArgumentOutOfRangeException]::new()
        }

        $this.mask[$x,$y] = $true
    }

    Unfreeze([int]$x,$y)
    {
        if(-not $this.CheckBounds($x,$y)){
            throw [System.ArgumentOutOfRangeException]::new()
        }

        $this.mask[$x,$y] = $false
    }

    hidden [bool]
    CheckBounds([int]$x,[int]$y)
    {
        return (
            $x -ge $this.data.GetLowerBound(0) -and 
            $x -le $this.data.GetUpperBound(0) -and 
            $y -ge $this.data.GetLowerBound(1) -and 
            $y -le $this.data.GetUpperBound(1)
        )
    }
}

Now you can do:

$grid = [Grid]::new(5,5)

$grid.Set(0, 0, 1)     # Set cell value
$grid.Get(0, 0)        # Get cell value

$grid.Freeze(0, 0)     # Now freeze cell 0,0
$grid.Set(0, 0, 2)     # ... and this will now throw an exception

$grid.Set(0, 1, 1)     # Setting any other cell still works

If you want native support for index expressions (ie. $grid[0,0]), the Grid class will need to implement System.Collections.IList

Sign up to request clarification or add additional context in comments.

1 Comment

Great answer! I probably wouldn't have thought about declaring the outer array. One more question, I have looked through the array members and from this answer I assume that there is no way to declare a constant variable besides New-Variable and Set-Variable? Is that right? Also congrats on 100k it seems like I missed it!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.