【问题标题】:Array Types In Powershell - System.Object[] vs. arrays with specific typesPowershell 中的数组类型 - System.Object[] 与具有特定类型的数组
【发布时间】:2017-07-10 09:16:58
【问题描述】:

为什么在字符串数组上调用GetType().Name 会返回Object[] 而不是String[]? 这似乎发生在任何元素类型上,例如Import-Csv 会给你一个Object[],但每个元素都是一个PSCustomObject

这是一个包含 String 数组的示例

$x = @('a','b','c')

$x[0].GetType().Name #String
$x.GetType().Name #Object[]

【问题讨论】:

    标签: arrays powershell collections types coercion


    【解决方案1】:

    感谢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.
    

    当你:

    • 使用数组构造运算符,

      创建一个数组
    • 使用数组子表达式运算符@(...)

      将命令输出强制到数组中
    • 命令的输出保存到变量,该命令发出集合具有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, 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 - 类型被保留。

        • 如果封闭的构造是命令(单段或多段管道,则默认展开行为适用;例如:
          (&amp;{ , 1 }) -is [int] 返回$true(单元素数组被解包)和(&amp; { [int[]] (1, 2) }) -is [object[]][int[]] 数组被重新组合成一个[object[]] 数组)都返回$true,因为使用呼叫操作员&amp; 使封闭的构造成为命令

      • (正则)子表达式运算符$(...),通常用于可扩展字符串中,显示默认的展开行为$(,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)'
    

    【讨论】:

      【解决方案2】:

      因为你没有明确指定数组的数据类型。

      例如,将整数分配给$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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-12-06
        • 2017-03-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-10-14
        • 2020-04-14
        • 1970-01-01
        相关资源
        最近更新 更多