web-dev-qa-db-fra.com

Types de tableaux dans Powershell - System.Object [] vs tableaux avec des types spécifiques

Pourquoi caler GetType().Name sur un tableau de chaînes retourne Object[] et pas String[]? Cela semble se produire avec n'importe quel type d'élément, par exemple Import-Csv vous donnera un Object[] mais chaque élément est un PSCustomObject.

Voici un exemple avec un tableau de String

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

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

Parce que vous n'avez pas spécifié explicitement le type de données du tableau.

Par exemple, attribuer un entier à $x[1] fonctionnerait, car le type du tableau est Object[].

Si vous spécifiez un type de données lors de la construction du tableau, vous ne pourrez pas attribuer de valeurs d'un type incompatible ultérieurement:

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
8
Miroslav Adamec

Pointe du chapeau à PetSerAl pour toute son aide.

Pour compléter réponse utile de Miroslav Adamec avec pourquoi PowerShell crée System.Object[] tableaux par défaut et des informations supplémentaires:

Les tableaux par défaut de PowerShell sont censés être flexibles :

  • ils vous permettent de stocker des objets de tout type (y compris $ null),
  • vous permettant même de mélanger objets de différents types dans un seul tableau .

Pour activer cela, le tableau doit être (implicitement) tapé comme [object[]] ([System.Object[]]) , car System.Object est la racine unique de toute la hiérarchie de types .NET dont dérivent tous les autres types.

Par exemple, ce qui suit crée un tableau [object[]] dont les éléments sont de type [string], [int], [datetime] et $null, respectivement.

$arr = 'hi', 42, (Get-Date), $null  # @(...) is not needed; `, <val>` for a 1-elem. arr.

Quand vous:

  • créer un tableau en utilisant le tableau construction opérateur , ,

  • forcer la sortie de la commande dans un tableau en utilisant le tableau sous-expression, @(...)

  • enregistrer dans une variable la sortie d'une commande qui émet a collection d'objets avec 2 ou plus éléments, quel que soit le type spécifique de la collection d'origine, ou opérez-le dans le contexte d'une autre commande en en l'enfermant dans (...)

vous toujours obtenez un System.Object[] tableau - même si tous les éléments arrive à ont le même type, comme dans votre exemple.


Lectures complémentaires facultatives

Les tableaux par défaut de PowerShell sont pratiques, mais présentent des inconvénients :

  • Ils ne fournissent aucune sécurité de type : si vous voulez vous assurer que tous les éléments sont d'un type spécifique (ou doivent être convertis si possible), un le tableau par défaut ne fera pas l'affaire; par exemple.:

    $intArray = 1, 2      # An array of [int] values.
    $intArray[0] = 'one'  # !! Works, because a [System.Object[]] array can hold any type.
    
  • [System.Object[]] les tableaux sont inefficaces pour types de valeurs tels que [int], car - boxing et nboxing doit être effectué - bien que cela puisse souvent ne pas avoir d'importance dans le monde réel.

Étant donné que PowerShell fournit un accès au système de type .NET, vous pouvez éviter les inconvénients si vous créez un tableau qui est limité au type d'intérêt spécifique, à l'aide d'un cast ou variable à contrainte de type:

[int[]] $intArray = 1, 2  # A type-constrained array of [int] variable.
$intArray[0] = 'one'      # BREAKS: 'one' can't be converted to an [int]

Notez que l'utilisation d'un cast ​​pour créer le tableau - $intArray = [int[]] (1, 2) - aurait également fonctionné, mais seule la variable à contrainte de type garantit que vous ne pouvez pas plus tard affecter une valeur d'un type différent de la variable (par exemple, $intArray = 'one', 'two' échouerait).

Piège de syntaxe avec transtypages: [int[]] 1, 2 ne pas fonctionne comme prévu, car les transtypages ont un haut priorité de l'opérateur , donc l'expression est évaluée comme ([int[]] 1), 2, ce qui crée un tableau [object[]] régulier dont l'élément first est un tableau imbriqué[int[]] avec un seul élément 1.
En cas de doute, utilisez @(...) autour des éléments de votre tableau [1], ce qui est également requis si vous souhaitez vous assurer qu'une expression qui peut renvoyer uniquement un élément single est toujours traitée comme un tableau.


Pièges

PowerShell effectue de nombreuses conversions de types en arrière-plan , qui sont généralement très utiles, mais il existe des pièges :

  • PowerShell essaie automatiquement de contraindre une valeur à un type cible , ce que vous ne voulez pas toujours et ne remarquerez peut-être pas:

    [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.
    

    Remarque: que même une distribution explicite - [int] 1 - provoque une coercition silencieuse peut ou non être une surprise pour vous. Ma surprise est venue - à tort - en supposant que dans un langage de coercition automatique tel que les transtypages PowerShell pourrait être un moyen de contourner la contrainte - qui est pas vrai.[2]

    Étant donné que le type any peut être converti en string, un tableau [string[]] est le cas le plus délicat.
    Vous faites obtenez une erreur si la contrainte (automatique) ne peut pas être effectuée, comme avec
    [int[]] $arr = 1, 2; $arr[0] = 'one' # error

  • "Ajouter à" un tableau de type spécifique crée un tableau nouvea de type [object[]]:

    PowerShell vous permet commodément d'ajouter des tableaux avec l'opérateur +.
    En réalité, un nouvea tableau est créé dans les coulisses avec le ou les éléments supplémentaires ajoutés, mais ce nouveau tableau est par défaut de nouveau de type [object[]], quel que soit le type du tableau d'entrée :

    $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.
    
  • La sortie vers le flux de réussite convertit toute collection en [object[]]:

    Toute collection avec au moins 2 éléments qu'une commande ou un pipeline sort (vers le flux de réussite) est automatiquement converti en un tableau de type [object[]], ce qui peut être inattendu:

    # 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[]'
    

    La raison de ce comportement est que PowerShell est fondamentalement basé sur la collection: toute commande la sortie est envoyée article par article via le pipeline ; notez que même une commande single est un pipeline (de longueur 1).

    Autrement dit, PowerShell toujours d'abord déballe les collections, puis, si nécessaire, réassemble les - pour affectation à une variable, ou comme résultat intermédiaire d'une commande imbriquée dans (...) - et la collection réassemblée est toujours de type [object[]].

    PowerShell considère un objet comme une collection si son type implémente l'interface IEnumerable , except ​​s'il implémente également IDictionary interface.
    Cette exception signifie que les tables de hachage de PowerShell ( [hashtable] ) et les tables de hachage ordonnées (la variante littérale PSv3 + avec des clés ordonnées, [ordered] @{...}, qui est de type [System.Collections.Specialized.OrderedDictionary] ) sont envoyés via le pipeline dans son ensemble, et pour énumérer à la place leurs entrées (paires clé-valeur) individuellement, vous devez appeler leur .GetEnumerator() méthode.

  • PowerShell par conception toujours déballe a nique - collection de sortie d'élément vers cet élément unique :

    En d'autres termes: lorsqu'une collection à élément unique est sortie, PowerShell ne renvoie pas un tablea, mais l'élément unique du tableau lui-même.

    # 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
    

    En gros, le but de array opérateur de sous-expression @(...) est: Toujours traiter la valeur incluse comme un collection, même si elle ne contient (ou ne déballerait normalement) qu'un élément unique:
    S'il s'agit d'une valeur nique, enveloppez-la dans un tableau [object[]] avec 1 élément.
    Les valeurs qui sont déjà des collections restent des collections, bien qu'elles soient converties en nouveau [object[]] tablea, même si la valeur elle-même déjà est ​​un tableau :
    $a1 = 1, 2; $a2 = @( $a1 ); [object]::ReferenceEquals($a1, $a2)
    affiche $false, prouvant que les tableaux $a1 et $a2 ne sont pas identiques.

    Comparez cela avec:

    • juste (...), ce que fait pas en soi changer la type de valeur - son but est simplement de clarifier la priorité ou de forcer un nouveau contexte d'analyse:

      • Si la construction incluse est une expression (quelque chose analysé dans mode d'expression), le type est not ​​changé ; par exemple, ([System.Collections.ArrayList] (1, 2)) -is [System.Collections.ArrayList] et ([int[]] (1,2)) -is [int[]] renvoient tous les deux $true - le type est conservé.

      • Si la construction incluse est une commande (mono ou multi-segment pipeline) , alors le comportement de déballage par défaut s'applique ; par exemple.:
        (&{ , 1 }) -is [int] renvoie $true (le tableau à un élément a été déballé) et (& { [int[]] (1, 2) }) -is [object[]] (le tableau [int[]] a été réassemblé dans un tableau [object[]]) renvoient tous les deux $true, car l'utilisation de l'opérateur d'appel & a fait de la construction jointe une commande.

    • (régulier) opérateur de sous-expression $(...), généralement utilisé dans les chaînes extensibles, qui présente la valeur par défaut comportement de déballage : $(,1) -is [int] et $([System.Collections.ArrayList] (1, 2)) -is [object[]] renvoient tous les deux $true.

  • Retour d'une collection dans son ensemble à partir d'une fonction ou d'un script:

    À l'occasion, vous souhaiterez peut-être générer une collection dans son ensemble, c'est-à-dire la produire sous la forme d'un élément single, en conservant son type d'origine.

    Comme nous l'avons vu ci-dessus, la sortie d'une collection telle quelle entraîne PowerShell à la déballer et finalement à la réassembler dans un tableau [object[]] normal.

    Pour éviter cela, la forme naire de l'opérateur de construction de tableau , peut être utilisée pour envelopper la collection dans un tableau externe, que PowerShell décompresse ensuite dans la collection d'origine:

    # 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'
    

    Dans PSv4 + , utilisez Write-Output -NoEnumerate:

    function foo { write-output -NoEnumerate ([System.Collections.ArrayList] (1, 2)) }
    $arrayList = foo
    $arrayList.GetType().Name # -> 'ArrayList'
    

[1] Notez que utiliser @(...) pour créer un tableau littéraux n'est pas nécessaire, car la construction du tableau opérateur ,seul crée des tableaux .
Sur les versions antérieures à PSv5.1, vous payez également une pénalité de performance (dans la plupart des cas probablement négligeable), parce que le tableau construit par , dans @() est effectivement - cloné par @() - voir cette réponse à moi pour plus de détails.
Cela dit, @(...) a des avantages :
* Vous pouvez utiliser la même syntaxe, que votre littéral de tableau contienne un seul (@( 1 ) ou plusieurs éléments (@( 1, 2 )). Comparez cela avec simplement l'utilisation du code ,: 1, 2 vs , 1.
* Vous n'avez pas besoin de ,- séparer les lignes des instructions multiline@(...) (mais notez que chaque ligne devient alors techniquement la sienne déclaration).
* Il n'y a aucun piège de priorité opérateur, car $(...) et @(...) ont la priorité la plus élevée.

[2] PetSerAl fournit cet extrait de code avancé pour montrer les scénarios limités dans lesquels PowerShell ne respecte les transtypages :

# 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()"; } 
    public string M()         { return "C.M()"; } 
    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()        
([I]$C).M()       # cast is respected
$C.M(1)
$C.M([object]1)   # cast is respected
20
mklement0