Search code examples
arrayspowershellmethodsmethod-callmember-access-enumeration

Why does GetType() return Object[] as the type on array, but a bad method call error says it is the type that is in the 0th element in the array?


So, I create an array, call GetType(), and I (reasonably) get the answer Object[]. If I, however, give it a bad method, I get the type of the 0th element in the error message.

Just trying to understand Powershell more -- I assume this is because Powershell is unwrapping the array in the case it doesn't find the method on the array, then also fails on the 0th array which throws the full error?

PS C:\Users\x> $i = @(1)
PS C:\Users\x> $i.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

PS C:\Users\x> $i.HerpDerp()
InvalidOperation: Method invocation failed because [System.Int32] does not contain a method named 'HerpDerp'.
PS C:\Users\x> $a = @("hi")
PS C:\Users\x> $a.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

PS C:\Users\x> $a.HerpDerp()
InvalidOperation: Method invocation failed because [System.String] does not contain a method named 'HerpDerp'.
PS C:\Users\x> $m = @("hi",1)
PS C:\Users\x> $m.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

PS C:\Users\x> $m.HerpDerp()
InvalidOperation: Method invocation failed because [System.String] does not contain a method named 'HerpDerp'.

Solution

  • The relevant information is in the comments, but I think your question deserves an answer post:

    What you're seeing are the effects of member-access enumeration, a convenient PowerShell feature where accessing a member (property or method) on an array (list-like collection) implicitly accesses that member on its elements, one by one, and returns the results as an array, except if there's only one result, which is returned as-is;[1] e.g.:

    # Even though .Year is applied to the *array*, PowerShell returns 
    # the .Year property values from the two [datetime] instances that
    # are the *elements* of that array; e.g., in 2023: 
    #   @( 2023, 2022 )
    @( (Get-Date), (Get-Date).AddYears(-1) ).Year
    

    This only works if the specified member doesn't also exist on the array itself, because the array's own member takes precedence; e.g.:

    # Returns 3, the *array's* .Length property value (count of elements), 
    # not the length of the strings stored in the array's elements.
    @( 'foo', 'bar', 'baz' ).Length
    

    This also explains why $i.GetType() returned the array's type.


    When member-access enumeration is applied, the (default) behavior differs depending on whether you access a property or a method:

    • Property access:

      • Any array element that doesn't have the specified property simply doesn't contribute to the output; if no array element has such a property, the overall result is $null

        # Outputs only 2023 (for instance), because [int] 42 has no .Year property.
        @( (Get-Date), 42 ).Year
        
        • Note: If the property exists but happens to contain $null, it is included in the results.
          However, due to a bug present up to at least PowerShell 7.3.4, [pscustomobject] instances also contribute $null if the property doesn't exist - see GitHub issue #13752
    • Method access:

      • If any array element doesn't have a method by the specified name - or if the method call results in an exception - a statement-terminating error occurs, which means that there's no success output, and only one error is reported, namely for the first element that didn't have the method / caused an exception (this explains the results of your .HerpDerp() calls):

        # !! Statement fails as a whole, because [int] (System.Int32)
        # !! doesn't have a .ToShortDateString() method.
        @( (Get-Date), 42 ).ToShortDateString()
        
        • Note that while you can handle / silence such an error with a try / catch statement, you still will not get partial results from those array elements that do have the method (and didn't cause an exception).

    Note that the above implies that it isn't necessarily the first element that triggers the statement-terminating error and if it isn't, the results from the calls on the earlier elements are in effect discarded.
    This is somewhat problematic, given that method calls can have side effects (and possibly have no output), in which case you may need to determine after the fact what elements the method calls were successfully performed on.


    [1] In doing so, member-access enumeration mimics the behavior of PowerShell's pipeline, which may be surprising. See GitHub issue #6802 for a discussion.