因此,我正在尝试创建一个可用于数据导航的树型变量。我在尝试在 PowerShell 中的哈希表上使用引用变量时遇到了问题。请考虑以下代码:
$Tree = @{ TextValue = "main"; Children = @() }
$Item = @{ TextValue = "sub"; Children = @() }
$Pointer = [ref] $Tree.Children
$Pointer.Value += $Item
$Tree
当检查引用变量$Pointer
时,它会显示适当的值,但主变量$Tree
不受影响。是否无法在 PowerShell 中创建对哈希表元素的引用,我必须切换到二维数组?
编辑更多信息:
我已经接受了 Mathias 的回答,因为使用List
看起来正是我所需要的,但需要更清楚地了解数组和引用的交互方式。试试这个代码:
$Tree1 = @()
$Pointer = $Tree1
$Pointer += 1
Write-Host "tree1 is " $Tree1
$Tree2 = @()
$Pointer = [ref] $Tree2
$Pointer.Value += 1
Write-Host "tree2 is " $Tree2
从输出中可以看出,可以获取对数组的引用,然后通过该引用修改数组的大小。我认为如果一个数组是另一个数组或哈希表的元素,它也可以工作,但它没有。PowerShell似乎以不同的方式处理这些问题。
我怀疑这是+=
在数组上工作方式的不幸副作用。
在固定大小的数组上使用+=
时,PowerShell 会将原始数组替换为新的(更大的)数组。我们可以验证$Pointer.Value
不再引用具有GetHashCode()
的相同数组:
PS C:> $Tree = @{ Children = @() }
PS C:> $Pointer = [ref]$Tree.Children
PS C:> $Tree.Children.GetHashCode() -eq $Pointer.Value.GetHashCode()
True
PS C:> $Pointer.Value += "Anything"
PS C:> $Tree.Children.GetHashCode() -eq $Pointer.Value.GetHashCode()
False
一种方法是避免使用@()
和+=
.
您可以改用List
类型:
$Tree = @{ TextValue = "main"; Children = New-Object System.Collections.Generic.List[psobject] }
$Item = @{ TextValue = "sub"; Children = New-Object System.Collections.Generic.List[psobject] }
$Pointer = [ref] $Tree.Children
$Pointer.Value.Add($Item)
$Tree
为了补充Mathias R. Jessen的有用答案:
-
事实上,任何数组都是固定大小的,不能就地扩展(
@()
创建一个空的[object[]]
数组)。 -
PowerShell 中的
+=
悄悄地创建一个新数组,其中包含所有原始元素和新元素的副本,并将其分配给 LHS。
您使用[ref]
毫无意义,因为仅$Pointer = $Tree.Children
就足以复制对存储在$Tree.Children
中的数组的引用。
有关[ref]
的适当用法的讨论,请参阅底部部分。
因此,$Tree.Children
和$Pointer
都将包含对同一数组的引用,就像$Pointer.Value
在基于[ref]
的方法中所做的那样。
但是,由于+=
创建了一个新数组,因此 LHS 上的任何内容(无论是$Pointer.Value
还是没有[ref]
,只是$Pointer
- 都只是接收对新数组的新引用,而$Tree.Children
仍然指向旧数组。
可以通过使用直接方法来确定两个变量或表达式是否"指向">引用类型的同一实例(所有集合都是)来验证这一点:
PS> [object]::ReferenceEquals($Pointer.Value, $Tree.Children)
False
请注意,[object]::ReferenceEquals()
仅适用于引用类型,不适用于值类型 - 包含后者的变量直接存储值,而不是引用存储在其他位置的数据。
Mathias 的方法通过使用[List`1]
实例而不是数组来解决您的问题,数组可以使用其.Add()
方法就地扩展,以便存储在$Pointer[.Value]
中的引用永远不需要更改并继续引用与$Tree.Children
相同的列表。
关于您的后续问题:[ref]
的适当使用:
$Tree2 = @()
$Pointer = [ref] $Tree2
在这种情况下,由于[ref]
应用于变量- 按照设计 - 它会创建一个有效的变量别名:$Pointer.Value
一直指向$Tree2
包含的任何内容,即使稍后将不同的数据分配给$Tree2
(无论该数据是值类型还是引用类型实例):
PS> $Tree2 = 'Now I am a string.'; $Pointer.Value
Now I am a string.
另请注意,典型的[ref]
用例是将变量传递给函数,以使用具有ref
或out
参数的.NET API 方法;虽然您也可以将其与 PowerShell脚本和函数一起使用,以便传递按引用参数(如以下示例所示),但最好避免这种情况:
# Works, but best avoided in PowerShell code.
PS> function foo { param([ref] $vRef) ++$vRef.Value }; $v=1; foo ([ref] $v); $v
2 # value of $v was incremented via $vRef.Value
相比之下,你不能使用[ref]
来创建对数据的持久间接引用,例如变量中包含的对象的属性,并且在那里使用[ref]
基本上毫无意义:
$Tree2 = @{ prop = 'initial val' }
$Pointer = [ref] $Tree2.prop # [ref] is pointless here
后来更改$Tree2.prop
不会反映在$Pointer.Value
中,因为$Pointer.Value
静态地引用最初存储在$Tree2.prop
中的引用:
PS> $Tree2.prop = 'later val'; $Pointer.Value
initial val # $Pointer.Value still points to the *original* data
PowerShell应该可以防止将[ref]
与任何不是变量的东西一起使用。但是,对于[ref]
,有一个合法的 - 尽管是异国情调的 - "标签外"用法,用于促进从后代作用域更新调用方作用域中的值,如概念about_Ref帮助主题所示。
您可以在树结构中创建指针:
$Tree = @{ TextValue = "main"; Children = [ref]@() }
$Item1 = @{ TextValue = "sub1"; Children = [ref]@() }
$Item2 = @{ TextValue = "sub2"; Children = [ref]@() }
$Item3 = @{ TextValue = "subsub"; Children = [ref]@() }
$Pointer = $Tree.Children
$Pointer.Value += $Item1
$Pointer.Value += $Item2
$Pointer.Value.Get(0).Children.Value += $Item3
function Show-Tree {
param ( [hashtable] $Tree )
Write-Host $Tree.TextValue
if ($Tree.Children.Value.Count -ne 0) {
$Tree.Children.Value | ForEach-Object { Show-Tree $_ }
}
}
Show-Tree $Tree
输出:
main
sub1
subsub
sub2