Powershell 中的数组类型 - System.Object[] 与特定类型的数组
Array Types In Powershell - System.Object[] vs. arrays with specific types
为什么在字符串数组 return Object[]
而不是 String[]
上计算 GetType().Name
?
这似乎发生在任何元素类型上,例如 Import-Csv
会给你一个 Object[]
但每个元素都是 PSCustomObject
.
这是一个数组 String
的示例
$x = @('a','b','c')
$x[0].GetType().Name #String
$x.GetType().Name #Object[]
因为你没有明确指定数组的数据类型。
例如,将整数分配给 $x[1]
是可行的,因为数组的类型是 Object[]
.
如果在构造数组时指定数据类型,以后将无法分配不兼容类型的值:
C:\PS> [int[]] $myArray = 12,64,8,64,12
C:\PS> $myArray.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32[] System.Array
C:\PS> $myArray[0] = "asd"
Cannot convert value "asd" to type "System.Int32". Error: "Input string was not in a c
orrect format."
At line:1 char:1
+ $myArray[0] = "asd"
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastFromStringToInteger
感谢 PetSerAl 的帮助。
要用 补充 为什么 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.
当你:
使用数组构造运算符创建数组,,
使用数组子表达式运算符,[=强制命令输出到数组中31=]
保存到变量 命令的输出a 集合 包含 2 个或更多 个元素的对象,不考虑原始集合的具体类型 ,或通过 将其包含在 (...)
中在另一个命令的上下文中对其进行操作
你总是得到一个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, 2
不 按预期工作,因为转换具有高 operator precedence,所以表达式被计算为 ([int[]] 1), 2
,这将创建一个常规 [object[]]
数组,其 第一个 元素是一个 嵌套 [int[]]
数组,只有一个元素 1
.
如有疑问,请在数组元素周围使用 @(...)
[1],如果要确保表达式可能 return 只有 单个 项始终被视为数组。
陷阱
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 转换之类的自动强制语言中可能是 bypass 强制的一种方式 - 这是 not 是的。[2]
鉴于 any 类型可以转换为 string,[string[]]
数组是最棘手的情况。
如果无法执行(自动)强制转换,您 do 会收到错误消息,例如 with
[int[]] $arr = 1, 2; $arr[0] = 'one' # error
“添加到”特定类型的数组会创建一个 new 类型 [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 总是 首先 unwraps 集合,然后,如果需要,重新组装它们 - 用于分配给变量,或作为中间结果嵌套在 (...)
中的 命令 - 重新组合的集合 始终为 [object[]]
.
如果对象的类型实现了 IEnumerable
interface, except if it also implements the IDictionary
接口,PowerShell 会将其视为集合。
此异常意味着 PowerShell 的哈希表 ([hashtable]
) and ordered hashtables (the PSv3+ literal variant with ordered keys, [ordered] @{...}
, which is of type [System.Collections.Specialized.OrderedDictionary]
) 是通过管道 作为一个整体 发送的,要改为单独枚举它们的条目(键值对),您必须调用它们.GetEnumerator()
方法。
PowerShell 设计 总是 unwraps 单个-元素输出集合到该单个元素:
换句话说:输出单元素集合时,PowerShell不会return一个数组,而是数组的单个元素本身.
# 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
松散地说,数组子表达式运算符@(...)
的目的是:始终将包含的值视为集合,即使它只包含(或通常会展开)一个单个项目:
如果它是一个 单个 值,将它包装成一个包含 1 个元素的 [object[]]
数组。
已经是集合的值仍然是集合,尽管它们 转换为 新的 [object[]]
数组 ,即使值本身已经 是 一个数组:
$a1 = 1, 2; $a2 = @( $a1 ); [object]::ReferenceEquals($a1, $a2)
输出 $false
,证明数组 $a1
和 $a2
不相同。
对比一下:
只是 (...)
,本身不会改变值的type - 它的目的仅仅是为了澄清优先级或强制一个新的解析上下文:
如果封闭的结构是一个表达式(在表达式模式), 类型未改变;例如,([System.Collections.ArrayList] (1, 2)) -is [System.Collections.ArrayList]
和 ([int[]] (1,2)) -is [int[]]
都是 return $true
- 类型被保留。
如果封闭的结构是命令(单段或多段管道 ),然后应用 默认展开行为;例如:
(&{ , 1 }) -is [int]
returns $true
(单元素数组被展开)和 (& { [int[]] (1, 2) }) -is [object[]]
([int[]]
数组被重组为 [object[]]
数组) return $true
,因为调用运算符 &
的使用使封闭的构造成为 command.
(常规)子表达式运算符$(...)
,通常用于可扩展字符串,表现出默认展开行为:
$(,1) -is [int]
和 $([System.Collections.ArrayList] (1, 2)) -is [object[]]
都 return $true
.
从函数或脚本返回一个集合作为一个整体:
有时您可能希望将集合作为一个整体输出,即将其输出为单个项目,保留其原始类型。
正如我们在上面看到的,按原样输出集合会导致 PowerShell 解包并最终将其重新组合成一个常规 [object[]]
数组。
为防止这种情况,一元形式的数组构造运算符,
可用于将集合包装在 outer 数组 中,然后 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] 请注意 使用 @(...)
创建数组 文字 不是 必要的,因为数组构造运算符 ,
单独 创建数组 .
在 PSv5.1 之前的版本中,您还需要付出(在大多数情况下可能可以忽略不计)性能损失,因为 @()
中的 ,
构造的数组实际上是 克隆的 作者 @()
- 详见我的 。
也就是说,@(...)
有优势:
- 您可以使用相同的语法,无论您的数组文字包含单个 (
@( 1 )
还是多个元素 (@( 1, 2 )
)。将此与仅使用 ,
进行对比:1, 2
与 , 1
.
- 您不需要
,
分隔 多行 @(...)
语句的行(但请注意,每一行在技术上都变成了自己的语句) .
- 没有运算符优先级陷阱,因为
$(...)
和 @(...)
具有最高优先级。
[2] PetSerAl 提供此高级代码片段以显示 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)'
为什么在字符串数组 return Object[]
而不是 String[]
上计算 GetType().Name
?
这似乎发生在任何元素类型上,例如 Import-Csv
会给你一个 Object[]
但每个元素都是 PSCustomObject
.
这是一个数组 String
$x = @('a','b','c')
$x[0].GetType().Name #String
$x.GetType().Name #Object[]
因为你没有明确指定数组的数据类型。
例如,将整数分配给 $x[1]
是可行的,因为数组的类型是 Object[]
.
如果在构造数组时指定数据类型,以后将无法分配不兼容类型的值:
C:\PS> [int[]] $myArray = 12,64,8,64,12
C:\PS> $myArray.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32[] System.Array
C:\PS> $myArray[0] = "asd"
Cannot convert value "asd" to type "System.Int32". Error: "Input string was not in a c
orrect format."
At line:1 char:1
+ $myArray[0] = "asd"
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastFromStringToInteger
感谢 PetSerAl 的帮助。
要用 补充 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.
当你:
使用数组构造运算符创建数组,
,
使用数组子表达式运算符,[=强制命令输出到数组中31=]
保存到变量 命令的输出a 集合 包含 2 个或更多 个元素的对象,不考虑原始集合的具体类型 ,或通过 将其包含在
中在另一个命令的上下文中对其进行操作(...)
你总是得到一个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, 2
不 按预期工作,因为转换具有高 operator precedence,所以表达式被计算为 ([int[]] 1), 2
,这将创建一个常规 [object[]]
数组,其 第一个 元素是一个 嵌套 [int[]]
数组,只有一个元素 1
.
如有疑问,请在数组元素周围使用 @(...)
[1],如果要确保表达式可能 return 只有 单个 项始终被视为数组。
陷阱
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 转换之类的自动强制语言中可能是 bypass 强制的一种方式 - 这是 not 是的。[2]鉴于 any 类型可以转换为 string,
[string[]]
数组是最棘手的情况。
如果无法执行(自动)强制转换,您 do 会收到错误消息,例如 with
[int[]] $arr = 1, 2; $arr[0] = 'one' # error
“添加到”特定类型的数组会创建一个 new 类型
[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 总是 首先 unwraps 集合,然后,如果需要,重新组装它们 - 用于分配给变量,或作为中间结果嵌套在
(...)
中的 命令 - 重新组合的集合 始终为[object[]]
.如果对象的类型实现了
IEnumerable
interface, except if it also implements theIDictionary
接口,PowerShell 会将其视为集合。
此异常意味着 PowerShell 的哈希表 ([hashtable]
) and ordered hashtables (the PSv3+ literal variant with ordered keys,[ordered] @{...}
, which is of type[System.Collections.Specialized.OrderedDictionary]
) 是通过管道 作为一个整体 发送的,要改为单独枚举它们的条目(键值对),您必须调用它们.GetEnumerator()
方法。PowerShell 设计 总是 unwraps 单个-元素输出集合到该单个元素:
换句话说:输出单元素集合时,PowerShell不会return一个数组,而是数组的单个元素本身.
# 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
松散地说,数组子表达式运算符
@(...)
的目的是:始终将包含的值视为集合,即使它只包含(或通常会展开)一个单个项目:
如果它是一个 单个 值,将它包装成一个包含 1 个元素的[object[]]
数组。
已经是集合的值仍然是集合,尽管它们 转换为 新的[object[]]
数组 ,即使值本身已经 是 一个数组:
$a1 = 1, 2; $a2 = @( $a1 ); [object]::ReferenceEquals($a1, $a2)
输出$false
,证明数组$a1
和$a2
不相同。对比一下:
只是
(...)
,本身不会改变值的type - 它的目的仅仅是为了澄清优先级或强制一个新的解析上下文:如果封闭的结构是一个表达式(在表达式模式), 类型未改变;例如,
([System.Collections.ArrayList] (1, 2)) -is [System.Collections.ArrayList]
和([int[]] (1,2)) -is [int[]]
都是 return$true
- 类型被保留。如果封闭的结构是命令(单段或多段管道 ),然后应用 默认展开行为;例如:
(&{ , 1 }) -is [int]
returns$true
(单元素数组被展开)和(& { [int[]] (1, 2) }) -is [object[]]
([int[]]
数组被重组为[object[]]
数组) return$true
,因为调用运算符&
的使用使封闭的构造成为 command.
(常规)子表达式运算符
$(...)
,通常用于可扩展字符串,表现出默认展开行为:$(,1) -is [int]
和$([System.Collections.ArrayList] (1, 2)) -is [object[]]
都 return$true
.
从函数或脚本返回一个集合作为一个整体:
有时您可能希望将集合作为一个整体输出,即将其输出为单个项目,保留其原始类型。
正如我们在上面看到的,按原样输出集合会导致 PowerShell 解包并最终将其重新组合成一个常规
[object[]]
数组。为防止这种情况,一元形式的数组构造运算符
,
可用于将集合包装在 outer 数组 中,然后 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] 请注意 使用 @(...)
创建数组 文字 不是 必要的,因为数组构造运算符 ,
单独 创建数组 .
在 PSv5.1 之前的版本中,您还需要付出(在大多数情况下可能可以忽略不计)性能损失,因为 @()
中的 ,
构造的数组实际上是 克隆的 作者 @()
- 详见我的
也就是说,@(...)
有优势:
- 您可以使用相同的语法,无论您的数组文字包含单个 (
@( 1 )
还是多个元素 (@( 1, 2 )
)。将此与仅使用,
进行对比:1, 2
与, 1
. - 您不需要
,
分隔 多行@(...)
语句的行(但请注意,每一行在技术上都变成了自己的语句) . - 没有运算符优先级陷阱,因为
$(...)
和@(...)
具有最高优先级。
[2] PetSerAl 提供此高级代码片段以显示 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)'