感谢PetSerAl 的帮助。
补充Miroslav Adamec's helpful answer 为什么 PowerShell 默认创建System.Object[] 数组 和其他背景信息:
PowerShell 的默认数组旨在灵活:
- 它们允许您存储任何类型的对象(包括
$null),
- 甚至允许您在单个数组中混合不同类型的对象。
要启用此功能,数组必须(隐式)键入为 [object[]] ([System.Object[]]),因为 System.Object 是整个 .NET 类型层次结构的单个根,所有其他类型派生。
例如,下面创建一个[object[]] 数组,其元素的类型分别为[string]、[int]、[datetime] 和$null。
$arr = 'hi', 42, (Get-Date), $null # @(...) is not needed; `, <val>` for a 1-elem. arr.
当你:
你总是得到一个System.Object[]数组 - 即使所有元素碰巧具有相同的类型,就像你的例子一样。
可选的进一步阅读
PowerShell 的默认数组很方便,但也有缺点:
-
它们提供没有类型安全性:如果您想确保所有元素都是特定类型(或者应该转换为特定类型,如果可能的话),默认数组不会这样做;例如:
$intArray = 1, 2 # An array of [int] values.
$intArray[0] = 'one' # !! Works, because a [System.Object[]] array can hold any type.
-
[System.Object[]] 数组对于 值类型(例如 [int])效率低下,因为必须执行 boxing and unboxing - 尽管这在现实世界中通常并不重要.
由于 PowerShell 提供对 .NET 类型系统的访问,如果您使用 cast 或 创建一个仅限于感兴趣的特定类型的数组,则可以避免这些缺点>类型约束变量:
[int[]] $intArray = 1, 2 # A type-constrained array of [int] variable.
$intArray[0] = 'one' # BREAKS: 'one' can't be converted to an [int]
请注意,使用 cast 创建数组 - $intArray = [int[]] (1, 2) - 也可以,但只有类型受限的变量才能确保您以后不能分配变量的不同类型的值(例如,$intArray = 'one', 'two' 会失败)。
casts 的语法缺陷:[int[]] 1, 2not 按预期工作,因为 casts 的 operator precedence 很高,所以该表达式被评估为([int[]] 1), 2,这将创建一个常规[object[]] 数组,其第一个 元素是一个嵌套 [int[]] 数组,其中包含单个元素1。
如有疑问,请在数组元素周围使用@(...)[1],如果您想确保表达式可能只返回 single 项始终被视为一个数组。
陷阱
PowerShell 在后台执行许多类型转换,这通常很有帮助,但也有陷阱:
-
PowerShell 自动尝试将值强制转换为目标类型,您并不总是想要并且可能不会注意到:
[string[]] $a = 'one', 'two'
$a[0] = 1 # [int] 1 is quietly coerced to [string]
# The coercion happens even if you use a cast:
[string[]] $a = 'one', 'two'
$a[0] = [int] 1 # Quiet coercion to [string] still happens.
注意:即使是显式强制转换 - [int] 1 - 也会导致安静的强制转换,这可能会让您感到意外,也可能不会感到惊讶。我的惊讶来自 - 错误地 - 假设在诸如 PowerShell 强制转换之类的自动强制语言中可能是一种绕过强制的方法 - 这是 not 真的。 [2]
鉴于 any 类型可以转换为 string,[string[]] 数组是最棘手的情况。
如果无法执行(自动)强制,您确实会收到错误,例如 with
[int[]] $arr = 1, 2; $arr[0] = 'one' # error
-
“添加到”一个特定类型的数组会创建一个[object[]]类型的新数组:
PowerShell 允许您使用 + 运算符方便地“添加到”数组。
实际上,new 数组是在幕后创建的,并附加了其他元素,但默认情况下,该 new 数组的类型再次为 [object[]] ,与输入数组的类型无关:
$intArray = [int[]] (1, 2)
($intArray + 4).GetType().Name # !! -> 'Object[]'
$intArray += 3 # !! $intArray is now of type [object[]]
# To avoid the problem...
# ... use casting:
([int[]] ($intArray + 4)).GetType().Name # -> 'Int32[]'
# ... or use a type-constrained variable:
[int[]] $intArray = (1, 2) # a type-constrained variable
$intArray += 3 # still of type [int[]], due to type constraint.
-
输出到成功流会将任何集合转换为[object[]]:
命令或管道输出(到成功流)的至少 2 个元素的任何集合会自动转换为 [object[]] 类型的数组,这可能是意料之外的:
# A specifically-typed array:
# Note that whether or not `return` is used makes no difference.
function foo { return [int[]] (1, 2) }
# Important: foo inside (...) is a *command*, not an *expression*
# and therefore a *pipeline* (of length 1)
(foo).GetType().Name # !! -> 'Object[]'
# A different collection type:
function foo { return [System.Collections.ArrayList] (1, 2) }
(foo).GetType().Name # !! -> 'Object[]'
# Ditto with a multi-segment pipeline:
([System.Collections.ArrayList] (1, 2) | Write-Output).GetType().Name # !! -> 'Object[]'
这种行为的原因是 PowerShell 基本上是基于集合的:任何命令的输出都是逐项通过管道;请注意,即使是 单个 命令也是一个管道(长度为 1)。
也就是说,PowerShell 总是首先解包集合,然后,如果需要,重新组装它们 - 对于赋值给一个变量,或者作为嵌套在(...)中的命令的中间结果 - 和重新组装的集合总是[object[]]。
如果对象的类型实现了IEnumerable interface,则PowerShell 将其视为集合,except 如果它还实现了IDictionary 接口。
此异常意味着 PowerShell 的哈希表 ([hashtable]) 和有序哈希表(具有有序键的 PSv3+ 文字变体 [ordered] @{...},其类型为 [System.Collections.Specialized.OrderedDictionary])通过管道发送作为一个整体 ,而要单独枚举它们的条目(键值对),您必须调用它们的 .GetEnumerator() 方法。
-
PowerShell 的设计总是展开一个单个-元素输出集合到那个单个元素:
换句话说:当输出单元素集合时,PowerShell 不会返回一个数组,而是返回数组的单元素本身。
# The examples use single-element array ,1
# constructed with the unary form of array-construction operator ","
# (Alternatively, @( 1 ) could be used in this case.)
# Function call:
function foo { ,1 }
(foo).GetType().Name # -> 'Int32'; single-element array was *unwrapped*
# Pipeline:
( ,1 | Write-Output ).GetType().Name # -> 'Int32'
# To force an expression into an array, use @(...):
@( (,1) | Write-Output ).GetType().Name # -> 'Object[]' - result is array
简单地说,array子表达式运算符@(...)的目的是:始终将封闭的值视为集合,即使它仅包含(或通常会打开)单个项目:
如果它是一个 单个 值,则将其包装为具有 1 个元素的 [object[]] 数组。
已经是集合的值仍然是集合,尽管它们被转换为新的[object[]] 数组,即使值本身已经是一个数组 :
$a1 = 1, 2; $a2 = @( $a1 ); [object]::ReferenceEquals($a1, $a2)
输出$false,证明数组$a1和$a2不一样。
对比:
-
只是(...),它本身并没有改变值的类型 - 它的目的仅仅是为了澄清优先级或强制新的解析上下文:
-
如果封闭的构造是表达式(以表达式模式解析的东西),类型是不 改变了;例如,([System.Collections.ArrayList] (1, 2)) -is [System.Collections.ArrayList] 和 ([int[]] (1,2)) -is [int[]] 都返回 $true - 类型被保留。
-
如果封闭的构造是命令(单段或多段管道),则默认展开行为适用;例如:
(&{ , 1 }) -is [int] 返回$true(单元素数组被解包)和(& { [int[]] (1, 2) }) -is [object[]]([int[]] 数组被重新组合成一个[object[]] 数组)都返回$true,因为使用呼叫操作员& 使封闭的构造成为命令。
-
(正则)子表达式运算符$(...),通常用于可扩展字符串中,显示默认的展开行为:
$(,1) -is [int] 和 $([System.Collections.ArrayList] (1, 2)) -is [object[]] 都返回 $true。
-
从函数或脚本返回集合作为一个整体:
有时您可能希望作为一个整体输出一个集合,即将它作为一个单个项输出,同时保留其原始类型。
正如我们在上面看到的,按原样输出集合会导致 PowerShell 将其解包并最终将其重新组合成一个常规的 [object[]] 数组。
为了防止这种情况,数组构造运算符,的一元形式可用于将集合包装在外部 数组,然后 PowerShell 将其解包到原始集合:
# Wrap array list in regular array with leading ","
function foo { , [System.Collections.ArrayList] (1, 2) }
# The call to foo unwraps the outer array and assigns the original
# array list to $arrayList.
$arrayList = foo
# Test
$arrayList.GetType().Name # -> 'ArrayList'
在 PSv4+ 中,使用 Write-Output -NoEnumerate:
function foo { write-output -NoEnumerate ([System.Collections.ArrayList] (1, 2)) }
$arrayList = foo
$arrayList.GetType().Name # -> 'ArrayList'
[1] 请注意,使用@(...) 创建数组literals 不是必需,因为数组构造运算符, 单独创建数组。
在 PSv5.1 之前的版本中,您还需要支付(在大多数情况下可能可以忽略不计)性能损失,因为 @() 中的 ,-constructed 数组实际上是由@()克隆 - 请参阅我的this answer 了解详情。
也就是说,@(...) 有优势:
- 您可以使用相同的语法,无论您的数组文字包含单个 (
@( 1 ) 还是多个元素 (@( 1, 2 ))。与仅使用 , 进行对比:1, 2 与 , 1。李>
- 您不需要
,-分隔 多行 @(...) 语句的行(但请注意,从技术上讲,每一行都会成为自己的语句)。
- 不存在运算符优先级缺陷,因为
$(...) 和 @(...) 具有最高优先级。
[2] PetSerAl 提供了这个高级代码 sn-p 来显示 PowerShell 确实尊重强制转换的有限场景,即在 的上下文中>.NET 方法调用的重载解析:
# Define a simple type that implements an interface
# and a method that has 2 overloads.
Add-Type '
public interface I { string M(); }
public class C : I {
string I.M() { return "I.M()"; } // interface implementation
public string M(int i) { return "C.M(int)"; }
public string M(object o) { return "C.M(object)"; }
}
'
# Instantiate the type and use casts to distinguish between
# the type and its interface, and to target a specific overload.
$C = New-Object C
$C.M(1) # default: argument type selects overload -> 'C.M(int)'
([I]$C).M() # interface cast is respected -> 'I.M()'
$C.M([object]1) # argument cast is respected -> 'C.M(object)'