我想退一步,谈论 哈希表。 我现在一直使用它们。 昨晚用户组会议后,我给别人讲解关于它们的知识时,意识到自己和那个人有同样的困惑。 哈希表在 PowerShell 中非常重要,因此最好能够深入了解它们。
注释
本文 的原始版本 出现在 @KevinMarquette撰写的博客上。 PowerShell 团队感谢 Kevin 与我们共享此内容。 请在 PowerShellExplained.com 查看他的博客。
哈希表作为对象的集合
我希望你首先在哈希表的传统定义中看到 哈希表 作为集合。 此定义使你对这些东西的工作原理以及它们在以后用于更高级的应用时的工作方式有了基本的了解。 跳过这种理解通常是混淆的根源。
什么是数组?
在跳入 哈希表 是什么之前,首先需要提及 数组 。 为了进行此讨论,数组是值或对象的列表或集合。
$array = @(1,2,3,5,7,11)
将项放入数组后,可以使用 foreach 循环访问列表或使用索引访问数组中的单个元素。
foreach($item in $array)
{
Write-Output $item
}
Write-Output $array[3]
还可以以相同的方式使用索引更新值。
$array[2] = 13
我只是对数组进行了初步接触,但当我转向学习哈希表时,这应该能帮助把它们放在正确的上下文中。
什么是哈希表?
我将首先从一般意义上介绍哈希表的基本技术描述,然后再说明 PowerShell 如何使用它们。
哈希表是一个数据结构,与数组非常类似,只是使用键存储每个值(对象)。 它是一个基本键/值存储。 首先,我们将创建一个空哈希表。
$ageList = @{}
请注意,大括号(而不是括号)用于定义哈希表。 然后,我们使用如下键添加一个项:
$key = 'Kevin'
$value = 36
$ageList.Add( $key, $value )
$ageList.Add( 'Alex', 9 )
此人的姓名是关键,其年龄是我想保存的值。
使用括号进行访问
将值添加到哈希表后,可以使用同一键(而不是像对数组那样使用数字索引)将其拉取回。
$ageList['Kevin']
$ageList['Alex']
当我想知道凯文的年龄时,我通过他的名字来访问这个信息。 我们也可以使用此方法在哈希表中添加或更新值。 这就像使用 Add() 上述方法一样。
$ageList = @{}
$key = 'Kevin'
$value = 36
$ageList[$key] = $value
$ageList['Alex'] = 9
可以使用另一种语法来访问和更新稍后部分介绍的值。 如果从其他语言转到 PowerShell,这些示例应该与您之前可能使用哈希表的方式一致。
使用值创建哈希表
到目前为止,我已为这些示例创建了一个空哈希表。 创建键和值时,可以预先填充这些键和值。
$ageList = @{
Kevin = 36
Alex = 9
}
作为查找表
这种类型的哈希表的实际价值在于,它们可以用作查找表。 下面是一个简单的示例。
$environments = @{
Prod = 'SrvProd05'
QA = 'SrvQA02'
Dev = 'SrvDev12'
}
$server = $environments[$env]
在此示例中,你为 $env 变量指定了一个环境,它将选取正确的服务器。 可以使用 switch($env){...} 这样的选项,但哈希表是一个很好的选择。
当你动态生成查找表以稍后使用它时,这会变得更好。 因此,在需要交叉引用某些内容时,请考虑使用此方法。 我认为,如果 PowerShell 在通过 Where-Object 管道进行筛选方面没有如此出色,我们会更加明显地看到这一点。 当你处于性能非常重要的情况下,需要考虑使用这种方法。
我不会说它更快, 但它确实适合规则 , 如果性能很重要, 测试它。
多选
通常,将哈希表视为键/值对,可在其中提供一个键并获取一个值。 PowerShell 允许提供键数组来获取多个值。
$environments[@('QA','DEV')]
$environments[('QA','DEV')]
$environments['QA','DEV']
在此示例中,我使用相同的查找哈希表,并提供三种不同的数组样式来获取匹配项。 这是 PowerShell 中大多数人不知道的隐藏宝石。
遍历哈希表
由于哈希表是键/值对的集合,因此循环访问它的方式与数组或常规项列表不同。
首先需要注意的是,如果将哈希表通过管道传输,管道会将其视为一个对象。
PS> $ageList | Measure-Object
count : 1
即使Count 属性显示它包含的值数。
PS> $ageList.Count
2
如果您只需要值,可以通过使用Values属性来解决此问题。
PS> $ageList.Values | Measure-Object -Average
Count : 2
Average : 22.5
通常,枚举键并利用它们来访问值,这样的方式更为有用。
PS> $ageList.Keys | ForEach-Object{
$message = '{0} is {1} years old!' -f $_, $ageList[$_]
Write-Output $message
}
Kevin is 36 years old
Alex is 9 years old
下面是与循环 foreach(){...} 相同的示例。
foreach($key in $ageList.Keys)
{
$message = '{0} is {1} years old' -f $key, $ageList[$key]
Write-Output $message
}
我们逐个遍历哈希表中的键,并利用这些键来访问相应的值。 使用哈希表作为集合时,这是一种常见模式。
GetEnumerator()
接下来我们将使用GetEnumerator()来遍历哈希表。
$ageList.GetEnumerator() | ForEach-Object{
$message = '{0} is {1} years old!' -f $_.Key, $_.Value
Write-Output $message
}
枚举器会一个接一个地为你提供每个键/值对。 它专为此用例设计。 感谢 马克·克劳斯 提醒我这件事。
错误的枚举
在枚举哈希表过程中,无法对其进行修改是一个重要细节。 如果我们从基本 $environments 示例开始:
$environments = @{
Prod = 'SrvProd05'
QA = 'SrvQA02'
Dev = 'SrvDev12'
}
尝试将每个密钥设置为同一服务器值会失败。
$environments.Keys | ForEach-Object {
$environments[$_] = 'SrvDev03'
}
An error occurred while enumerating through a collection: Collection was modified;
enumeration operation may not execute.
+ CategoryInfo : InvalidOperation: tableEnumerator:HashtableEnumerator) [],
RuntimeException
+ FullyQualifiedErrorId : BadEnumeration
这也会失败,即使它看起来也应该很好:
foreach($key in $environments.Keys) {
$environments[$key] = 'SrvDev03'
}
Collection was modified; enumeration operation may not execute.
+ CategoryInfo : OperationStopped: (:) [], InvalidOperationException
+ FullyQualifiedErrorId : System.InvalidOperationException
这种情况的要诀是在执行枚举之前克隆密钥。
$environments.Keys.Clone() | ForEach-Object {
$environments[$_] = 'SrvDev03'
}
注释
无法克隆包含单个键的哈希表。 PowerShell 引发错误。 而是将 Keys 属性转换为数组,然后循环访问数组。
@($environments.Keys) | ForEach-Object {
$environments[$_] = 'SrvDev03'
}
哈希表作为属性集合
到目前为止,我们在哈希表中放置的对象的类型都是相同的对象类型。 我用了所有这些例子中的年龄,关键是人的名字。 当你的对象集合中每个对象都有一个名称时,这是一种很好的查看方法。 在 PowerShell 中使用哈希表的另一种常见方法是保存属性集合,其中键是属性名称。 在下一个示例中,我将逐步了解这个想法。
基于属性的访问
使用基于属性的访问会更改哈希表的动态,以及如何在 PowerShell 中使用它们。 下面是上述将键视为属性的常用示例。
$ageList = @{}
$ageList.Kevin = 35
$ageList.Alex = 9
与上面的示例一样,如果这些键尚不存在于哈希表中,则添加这些键。 根据你定义密钥的方式以及你的值是什么,这要么有点奇怪,要么是完美的。 年龄列表示例一直良好运作,直至现在。 我们需要一个新的示例来确保未来的正确方向。
$person = @{
name = 'Kevin'
age = 36
}
我们可以像这样在 $person 上添加和访问属性。
$person.city = 'Austin'
$person.state = 'TX'
突然间,这个哈希表开始像一个对象那样进行操作和表现。 它仍然是一系列内容,因此上述所有示例仍然适用。 我们只是从不同的角度接近它。
检查键和值
在大多数情况下,只需使用如下所示的值进行测试:
if( $person.age ){...}
这很简单,但一直是我许多 bug 的来源,因为我在逻辑中忽略了一个重要细节。 我开始使用它来测试密钥是否存在。 当值为 $false 零时,该语句将意外返回 $false 。
if( $person.age -ne $null ){...}
这规避了零值的问题,但无法解决$null与不存在键之间的问题。 大多数时候,你不需要进行这种区分,但当你这样做时,有一些方法。
if( $person.ContainsKey('age') ){...}
我们也有一个 ContainsValue() 用于在您无需知道键或遍历整个集合的情况下测试值的情况。
删除和清除密钥
可以使用该方法删除密钥 Remove() 。
$person.Remove('age')
将其赋予 $null 值后,最终只剩下一个具有 $null 值的键。
清除哈希表的一种常见方法是将其初始化为空哈希表。
$person = @{}
虽然这样做是可行的,但请尝试改用 Clear() 方法。
$person.Clear()
这是使用该方法创建自记录代码的实例之一,它使代码的意图非常清晰。
所有有趣的东西
有序哈希表
默认情况下,哈希表既不具有顺序,也不经过排序。 在传统上下文中,当始终使用键访问值时,顺序并不重要。 你可能会发现,你希望这些属性按定义它们的顺序保持。 值得庆幸的是,可以通过ordered关键字来实现这一点。
$person = [ordered]@{
name = 'Kevin'
age = 36
}
现在,当你枚举键和值时,它们会保持该顺序。
内联哈希表
在一行上定义哈希表时,可以使用分号分隔键/值对。
$person = @{ name = 'kevin'; age = 36; }
如果要在管道上创建它们,这将非常有用。
常见管道命令中的自定义表达式
有几个 cmdlet 支持使用哈希表创建自定义或计算属性。 你经常看到这一点和 Select-Object 和 Format-Table。 哈希表具有特殊的语法,完全展开后如下所示。
$property = @{
Name = 'TotalSpaceGB'
Expression = { ($_.Used + $_.Free) / 1GB }
}
cmdlet 会将该列标记为 Name。
Expression 是一个脚本块,该脚本块在管道中对象的值$_ 处执行。 下面是运行中的脚本:
$drives = Get-PSDrive | where Used
$drives | Select-Object -Property Name, $property
Name TotalSpaceGB
---- ------------
C 238.472652435303
我把它放在一个变量中,但它也可以直接内联定义,你可以同时将Name缩短为n,并将Expression缩短为e。
$drives | Select-Object -Property Name, @{n='TotalSpaceGB';e={($_.Used + $_.Free) / 1GB}}
我个人不喜欢命令变得冗长,这常常会导致一些我不想详细讨论的不良行为。 我更有可能创建一个包含我想要的所有字段和属性的新哈希表或pscustomobject,而不是在脚本中使用这种方法。 但是,有很多代码可以执行此作,所以我希望你了解它。 我谈到稍后会创建一个 pscustomobject。
自定义排序表达式
如果对象具有要排序的数据,则很容易对集合进行排序。 可以在对对象进行排序之前将数据添加到对象,也可以为其 Sort-Object创建自定义表达式。
Get-ADUser | Sort-Object -Property @{ e={ Get-TotalSales $_.Name } }
在此示例中,我将获取用户列表,并使用一些自定义 cmdlet 获取其他信息,以便进行排序。
对哈希表的列表进行排序
如果有要排序的哈希表列表,你会发现 Sort-Object 不会将键视为属性。 可以通过使用自定义排序表达式来规避这个问题。
$data = @(
@{name='a'}
@{name='c'}
@{name='e'}
@{name='f'}
@{name='d'}
@{name='b'}
)
$data | Sort-Object -Property @{e={$_.name}}
在 cmdlet 上展开哈希表
这是关于哈希表的我最喜欢的特点之一,许多人一开始并没有发现。 其思路是,先将所有属性打包到一个哈希表中,而不是直接在一行中提供给 cmdlet。 然后,你可以以特殊方式向函数提供哈希表。 下面是以正常方式创建 DHCP 范围的示例。
Add-DhcpServerV4Scope -Name 'TestNetwork' -StartRange '10.0.0.2' -EndRange '10.0.0.254' -SubnetMask '255.255.255.0' -Description 'Network for testlab A' -LeaseDuration (New-TimeSpan -Days 8) -Type "Both"
如果不使用 散点,所有这些内容都需要在单个行上定义。 它要么在屏幕外滚动,要么随意换行。 现在,将它与使用 Splatting 的命令进行比较。
$DHCPScope = @{
Name = 'TestNetwork'
StartRange = '10.0.0.2'
EndRange = '10.0.0.254'
SubnetMask = '255.255.255.0'
Description = 'Network for testlab A'
LeaseDuration = (New-TimeSpan -Days 8)
Type = "Both"
}
Add-DhcpServerV4Scope @DHCPScope
使用 @ 符号而不是 $ 是调用 splat 操作的原因。
只需花点时间了解该示例阅读是多么容易。 他们是完全相同的命令,具有相同的值。 第二个更容易理解,并在未来更容易维护。
每当命令过长时,我都会使用喷洒。 我将长度过长定义为导致我的窗口向右滚动。 如果我命中函数的三个属性,几率是我会使用一个散开的哈希表重写它。
可选参数的展开
我使用 Splatting 的最常见方式之一是处理来自脚本中其他位置的可选参数。 假设我有一个函数来包装 Get-CimInstance 具有可选 $Credential 参数的调用。
$CIMParams = @{
ClassName = 'Win32_BIOS'
ComputerName = $ComputerName
}
if($Credential)
{
$CIMParams.Credential = $Credential
}
Get-CimInstance @CIMParams
首先,使用常用参数创建哈希表。 然后,如果存在,就添加 $Credential。
由于我在这里使用 Splatting,因此只需在代码中调用 Get-CimInstance 一次。 此设计模式非常干净,可以轻松处理大量可选参数。
严格来说,您可以编写命令以允许参数接受$null值。 你并不总是可以控制你正在调用的其他命令。
多个 Splats
可以将多个哈希表直接传递到同一个 cmdlet 中。 如果我们重新访问原始的喷洒示例:
$Common = @{
SubnetMask = '255.255.255.0'
LeaseDuration = (New-TimeSpan -Days 8)
Type = "Both"
}
$DHCPScope = @{
Name = 'TestNetwork'
StartRange = '10.0.0.2'
EndRange = '10.0.0.254'
Description = 'Network for testlab A'
}
Add-DhcpServerv4Scope @DHCPScope @Common
当我有一组通用参数需要传递给大量命令时,我将使用此方法。
清理代码的散点
如果使代码更简洁,则展开单个参数没有任何问题。
$log = @{Path = '.\logfile.log'}
Add-Content "logging this command" @log
Splatting 可执行程序
Splatting 也适用于某些使用 /param:value 语法的可执行文件。
Robocopy.exe例如,具有如下所示的一些参数。
$robo = @{R=1;W=1;MT=8}
robocopy source destination @robo
我不知道这一切都很有用,但我发现它很有趣。
添加哈希表
哈希表支持加法运算符来组合两个哈希表。
$person += @{Zip = '78701'}
仅当两个哈希表不共享密钥时,这才有效。
嵌套哈希表
我们可以将哈希表用作哈希表中的值。
$person = @{
name = 'Kevin'
age = 36
}
$person.location = @{}
$person.location.city = 'Austin'
$person.location.state = 'TX'
我从包含两个键的基本哈希表开始。 我添加了一个名为 location 的键,并将其与一个空的哈希表关联。 然后,我向该 location 哈希表添加了最后两个项。 我们也可以直接执行这一切。
$person = @{
name = 'Kevin'
age = 36
location = @{
city = 'Austin'
state = 'TX'
}
}
这会创建上面看到的相同哈希表,并且可以以相同的方式访问属性。
$person.location.city
Austin
有多种方法可以接近对象的结构。 下面是查看嵌套哈希表的第二种方法。
$people = @{
Kevin = @{
age = 36
city = 'Austin'
}
Alex = @{
age = 9
city = 'Austin'
}
}
这混合了将哈希表用作对象集合和属性集合的概念。 即使使用你喜欢的方法嵌套这些值,这些值仍然很容易访问。
PS> $people.kevin.age
36
PS> $people.kevin['city']
Austin
PS> $people['Alex'].age
9
PS> $people['Alex']['City']
Austin
当我将点属性视为属性时,我倾向于使用点属性。 这些通常是我在代码中静态定义的,我能脱口而出。 如果需要遍遍列表或以编程方式访问密钥,请使用括号提供密钥名称。
foreach($name in $people.Keys)
{
$person = $people[$name]
'{0}, age {1}, is in {2}' -f $name, $person.age, $person.city
}
拥有嵌套哈希表的能力为你提供了很大的灵活性和选项。
查看嵌套哈希表
当您开始嵌套哈希表后,您将需要一种便捷的方法来在控制台中查看它们。 如果我获取最后一个哈希表,则会收到如下所示的输出,并且它只会如此深入:
PS> $people
Name Value
---- -----
Kevin {age, city}
Alex {age, city}
我用于查看这些内容的常用命令是 ConvertTo-Json ,因为它非常简洁,我经常在其他地方使用 JSON。
PS> $people | ConvertTo-Json
{
"Kevin": {
"age": 36,
"city": "Austin"
},
"Alex": {
"age": 9,
"city": "Austin"
}
}
即使不知道 JSON,也应该能够查看要查找的内容。 有这样的 Format-Custom 结构化数据的命令,但我仍然喜欢 JSON 视图。
创建对象
有时,你只需要一个对象,而使用哈希表来保存属性并不能很好地解决问题。 最常见的情况下,你希望将键视为列名称。 一个 pscustomobject 让这变得简单。
$person = [pscustomobject]@{
name = 'Kevin'
age = 36
}
$person
name age
---- ---
Kevin 36
即使你最初没有将其创建为pscustomobject,也可以根据需要在以后进行强制转换。
$person = @{
name = 'Kevin'
age = 36
}
[pscustomobject]$person
name age
---- ---
Kevin 36
我已经为 pscustomobject 写了详细的文章,建议你在读完这篇文章后去阅读。 它是建立在这里学到的许多内容基础上的。
读取和写入文件哈希表
保存到 CSV
把哈希表保存到 CSV 是我上面提到的困难之一。 将哈希表转换为 a pscustomobject ,并将正确保存到 CSV。 从 pscustomobject 开始有助于保留列的顺序。 如果需要,你可以将它转换为 pscustomobject 内联。
$person | ForEach-Object{ [pscustomobject]$_ } | Export-Csv -Path $path
同样,看看我写的关于如何使用pscustomobject的内容。
将嵌套哈希表保存到文件
如果需要将嵌套哈希表保存到文件并读回,可以使用 JSON 命令来实现。
$people | ConvertTo-Json | Set-Content -Path $path
$people = Get-Content -Path $path -Raw | ConvertFrom-Json
此方法有两个重要要点。 首先,JSON 被写出多行,因此我需要使用 -Raw 选项将其读回单个字符串。 第二个是导入的对象不再是一个 [hashtable]。 它现在是一个 [pscustomobject] ,如果你不期望它,这可能会导致问题。
监视深度嵌套哈希表。 将其转换为 JSON 时,可能无法获得预期结果。
@{ a = @{ b = @{ c = @{ d = "e" }}}} | ConvertTo-Json
{
"a": {
"b": {
"c": "System.Collections.Hashtable"
}
}
}
使用 Depth 参数来确保已扩展所有嵌套哈希表。
@{ a = @{ b = @{ c = @{ d = "e" }}}} | ConvertTo-Json -Depth 3
{
"a": {
"b": {
"c": {
"d": "e"
}
}
}
}
如果您需要它在导入时成为[hashtable],那么您需要使用Export-CliXml和Import-CliXml命令。
将 JSON 转换为哈希表
如果需要将 JSON 转换为[hashtable],可以借助 .NET 中的JavaScriptSerializer 完成此操作。
[Reflection.Assembly]::LoadWithPartialName("System.Web.Script.Serialization")
$JSSerializer = [System.Web.Script.Serialization.JavaScriptSerializer]::new()
$JSSerializer.Deserialize($json,'Hashtable')
从 PowerShell v6 开始,JSON 支持使用 NewtonSoft JSON.NET 并添加哈希表支持。
'{ "a": "b" }' | ConvertFrom-Json -AsHashtable
Name Value
---- -----
a b
PowerShell 6.2 添加了 Depth 参数到 ConvertFrom-Json。 默认 深度 为 1024。
直接从文件读取
如果文件包含使用 PowerShell 语法的哈希表,则可以直接导入它。
$content = Get-Content -Path $Path -Raw -ErrorAction Stop
$scriptBlock = [scriptblock]::Create( $content )
$scriptBlock.CheckRestrictedLanguage( $allowedCommands, $allowedVariables, $true )
$hashtable = ( & $scriptBlock )
它会将文件的内容导入到文件中 scriptblock,然后检查以确保它在执行该文件之前没有任何其他 PowerShell 命令。
请注意,你是否知道模块清单( .psd1 文件)只是哈希表?
键可以是任何对象
大多数情况下,键只是字符串。 因此,我们可以把引号放在任何东西周围,使其成为一个键。
$person = @{
'full name' = 'Kevin Marquette'
'#' = 3978
}
$person['full name']
你可以做一些你可能没有意识到你可以做的奇怪的事情。
$person.'full name'
$key = 'full name'
$person.$key
只是因为你可以做点什么, 这并不意味着你应该这样做。 最后一个看上去就像一个即将发生的 bug,很容易被阅览代码的人误解。
从技术上讲,密钥不必是字符串,但如果只使用字符串,则更容易理解。 但是,索引处理不适用于复杂键。
$ht = @{ @(1,2,3) = "a" }
$ht
Name Value
---- -----
{1, 2, 3} a
通过哈希表中的键访问值并不总是起作用。 例如:
$key = $ht.Keys[0]
$ht.$($key)
a
$ht[$key]
a
当键是数组时,必须将变量包装 $key 在子表达式中,以便它可用于成员访问(.) 表示法。 或者,可以使用数组索引 ([]) 表示法。
在自动变量中使用
$PSBoundParameters
$PSBoundParameters 是一个仅存在于函数上下文中的自动变量。 它包含函数调用的所有参数。 这不是哈希表,但足够接近,你可以像哈希表一样使用。
这包括删除键并将其喷出到其他函数。 如果你发现自己正在编写代理函数,请仔细查看此函数。
有关 about_Automatic_Variables 的详细信息,请参阅 。
PSBoundParameters 注意事项
需要记住的一个重要事项是,这只包括作为参数传入的值。 如果还具有具有默认值但未由调用方传入的参数, $PSBoundParameters 则不包含这些值。 这通常被忽视。
$PSDefaultParameterValues
此自动变量允许你将默认值分配给任何 cmdlet,而无需更改 cmdlet。 查看此示例。
$PSDefaultParameterValues["Out-File:Encoding"] = "UTF8"
向$PSDefaultParameterValues哈希表添加一个条目,该条目将Out-File -Encoding设置为UTF8参数的默认值。 这是特定于会话的,因此应将其置于你的$PROFILE中。
我经常使用此值来预分配我经常键入的值。
$PSDefaultParameterValues[ "Connect-VIServer:Server" ] = 'VCENTER01.contoso.local'
这还接受通配符,以便可以批量设置值。 下面是一些可以使用的方法:
$PSDefaultParameterValues[ "Get-*:Verbose" ] = $true
$PSDefaultParameterValues[ "*:Credential" ] = Get-Credential
有关更深入的细分,请参阅迈克尔·索伦斯关于自动默认值的这篇好文章。
正则表达式$Matches
使用 -match 运算符时,将创建一个名为 $Matches 的自动变量,其包含匹配结果。 如果正则表达式中有任何子表达式,则还会列出这些子匹配项。
$message = 'My SSN is 123-45-6789.'
$message -match 'My SSN is (.+)\.'
$Matches[0]
$Matches[1]
命名匹配项
这是我最喜欢的功能之一,大多数人不知道。 如果使用命名正则表达式匹配,则可以按匹配项的名称访问该匹配项。
$message = 'My Name is Kevin and my SSN is 123-45-6789.'
if($message -match 'My Name is (?<Name>.+) and my SSN is (?<SSN>.+)\.')
{
$Matches.Name
$Matches.SSN
}
在上面的示例中, (?<Name>.*) 它是一个命名的子表达式。 然后,此值将放置在属性中 $Matches.Name 。
Group-Object -AsHashtable
一个鲜为人知的功能是,Group-Object 可以帮你将某些数据集转换为哈希表。
Import-Csv $Path | Group-Object -AsHashtable -Property Email
这会将每一行添加到哈希表中,并使用指定的属性作为键来访问它。
复制哈希表
需要知道的一个重要事项是哈希表是对象。 每个变量只是对对象的引用。 这意味着,创建哈希表的有效副本需要更多工作。
分配引用类型
如果你有一个哈希表并将其分配给第二个变量,这两个变量都指向相同的哈希表。
PS> $orig = @{name='orig'}
PS> $copy = $orig
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name
Copy: [copy]
Orig: [copy]
这突出显示了它们相同,因为更改一个中的值也会改变另一个值。 这还适用于将哈希表传入其他函数。 如果这些函数对该哈希表进行更改,您的原始哈希表也会被更改。
浅拷贝,单层
如果我们有一个像上面的示例那样简单的哈希表,可以使用Clone()创建一个浅表副本。
PS> $orig = @{name='orig'}
PS> $copy = $orig.Clone()
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name
Copy: [copy]
Orig: [orig]
这将使我们能够进行一些基本更改,而不影响其他部分。
浅表副本,嵌套
之所以称为浅拷贝,是因为它只复制最基本的属性。 如果其中一个属性是引用类型(如另一个哈希表),则这些嵌套对象仍将相互指向。
PS> $orig = @{
person=@{
name='orig'
}
}
PS> $copy = $orig.Clone()
PS> $copy.person.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.person.name
PS> 'Orig: [{0}]' -f $orig.person.name
Copy: [copy]
Orig: [copy]
因此,你可以看到,即使我克隆了哈希表,也没有克隆对person的引用。 我们需要进行深拷贝,以便真正拥有第二个独立于第一个的哈希表。
深层副本
可通过几种方法创建哈希表的深层副本(并将其保留为哈希表)。 下面是使用 PowerShell 以递归方式创建深层副本的函数:
function Get-DeepClone
{
[CmdletBinding()]
param(
$InputObject
)
process
{
if($InputObject -is [hashtable]) {
$clone = @{}
foreach($key in $InputObject.Keys)
{
$clone[$key] = Get-DeepClone $InputObject[$key]
}
return $clone
} else {
return $InputObject
}
}
}
它不会处理任何其他引用类型或数组,但它是一个很好的起点。
另一种方法是使用 .NET 通过 CliXml 反序列化它,如以下函数所示:
function Get-DeepClone
{
param(
$InputObject
)
$TempCliXmlString = [System.Management.Automation.PSSerializer]::Serialize($obj, [int32]::MaxValue)
return [System.Management.Automation.PSSerializer]::Deserialize($TempCliXmlString)
}
对于非常大的哈希表,反序列化函数在横向扩展时速度更快。但是,使用此方法时需要考虑一些事项。 由于它使用 CliXml,因此占用大量内存,如果您克隆大型哈希表,则可能会是个问题。 CliXml 的另一个限制是深度限制为 48。 这意味着,如果你有一个包含 48 层嵌套哈希表的哈希表,则克隆将失败,并且不会输出任何哈希表。
还有其他的吗?
我很快取得了很大的进展。 我希望你每次阅读这篇文章时,都能学到新的东西或更好地理解它。 由于我涵盖了此功能的全部内容,因此目前可能不适用于你。 这完全正常,某种程度上也在预料之中,这取决于你使用 PowerShell 的工作量。