powershell null automation-null

powershell - ¿Por qué y cómo son diferentes estos dos valores $ nulos?



null automation-null (3)

Aparentemente, en PowerShell (ver. 3) no todos los $null son iguales:

>function emptyArray() { @() } >$l_t = @() ; $l_t.Count 0 >$l_t1 = @(); $l_t1 -eq $null; $l_t1.count; $l_t1.gettype() 0 IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array >$l_t += $l_t1; $l_t.Count 0 >$l_t += emptyArray; $l_t.Count 0 >$l_t2 = emptyArray; $l_t2 -eq $null; $l_t2.Count; $l_t2.gettype() True 0 You cannot call a method on a null-valued expression. At line:1 char:38 + $l_t2 = emptyArray; $l_t2 -eq $null; $l_t2.Count; $l_t2.gettype() + ~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [], RuntimeException + FullyQualifiedErrorId : InvokeMethodOnNull >$l_t += $l_t2; $l_t.Count 0 >$l_t3 = $null; $l_t3 -eq $null;$l_t3.gettype() True You cannot call a method on a null-valued expression. At line:1 char:32 + $l_t3 = $null; $l_t3 -eq $null;$l_t3.gettype() + ~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [], RuntimeException + FullyQualifiedErrorId : InvokeMethodOnNull >$l_t += $l_t3; $l_t.count 1 >function addToArray($l_a, $l_b) { $l_a += $l_b; $l_a.count } >$l_t = @(); $l_t.Count 0 >addToArray $l_t $l_t1 0 >addToArray $l_t $l_t2 1

Entonces, ¿cómo y por qué $l_t2 diferente de $l_t3 ? En particular, ¿ $l_t2 realmente $null o no? Tenga en cuenta que $l_t2 NO es una matriz vacía ( $l_t1 es, y $l_t1 -eq $null no devuelve nada, como se esperaba), pero tampoco es realmente $null , como $l_t3 . En particular, $l_t2.count devuelve 0 en lugar de un error, y además, agregar $l_t2 a $l_t comporta como agregar una matriz vacía, no como agregar $null . ¿Y por qué $l_t2 repente parece convertirse en "más $null " cuando se pasa en la función addToArray como parámetro ???????

¿Alguien puede explicar este comportamiento o señalarme la documentación que lo explicaría?

Editar: La respuesta de PetSerAl a continuación es correcta. También encontré esta publicación stackOverflow sobre el mismo problema.

Información de la versión de Powershell:

>$PSVersionTable Name Value ---- ----- WSManStackVersion 3.0 PSCompatibleVersions {1.0, 2.0, 3.0} SerializationVersion 1.1.0.1 BuildVersion 6.2.9200.16481 PSVersion 3.0 CLRVersion 4.0.30319.1026 PSRemotingProtocolVersion 2.2


En particular, ¿ $l_t2 realmente $null o no?

$l_t2 no es $null , sino un [System.Management.Automation.Internal.AutomationNull]::Value . Es una instancia especial de PSObject . Se devuelve cuando una tubería devuelve cero objetos. Así es como puedes comprobarlo:

$a=&{} #shortest, I know, pipeline, that returns zero objects $b=[System.Management.Automation.Internal.AutomationNull]::Value $ReferenceEquals=[Object].GetMethod(''ReferenceEquals'') $ReferenceEquals.Invoke($null,($a,$null)) #returns False $ReferenceEquals.Invoke($null,($a,$b)) #returns True

Llamo a ReferenceEquals través de Reflection para evitar la conversión de AutomationNull a $ null por PowerShell.

$l_t1 -eq $null no devuelve nada

Para mí, devuelve una matriz vacía, como espero de ella.

$l_t2.count devuelve 0

Es una nueva característica de PowerShell v3 :

Ahora puede usar Count o Length en cualquier objeto, incluso si no tenía la propiedad. Si el objeto no tenía una propiedad Count o Length, devolverá 1 (o 0 por $ nulo). Los objetos que tienen propiedades Count o Length continuarán funcionando como siempre lo han hecho.

PS> $a = 42 PS> $a.Count 1

¿Y por qué $l_t2 repente parece convertirse en "más $null " cuando se pasa en la función addToArray como parámetro ???????

Parece que PowerShell convierte AutomationNull a $null en algunos casos, como llamar a métodos .NET. En PowerShell v2, incluso al guardar AutomationNull en una variable, se convierte a $null .


Cuando devuelve una colección de una función de PowerShell, PowerShell determina de manera predeterminada el tipo de datos del valor de retorno de la siguiente manera:

  • Si la colección tiene más de un elemento, el resultado devuelto es una matriz. Tenga en cuenta que el tipo de datos del resultado devuelto es System.Array, incluso si el objeto que se devuelve es una colección de un tipo diferente.
  • Si la colección tiene un solo elemento, el resultado de retorno es el valor de ese elemento, en lugar de una colección de un elemento, y el tipo de datos del resultado de retorno es el tipo de datos de ese elemento.
  • Si la colección está vacía, el resultado devuelto es $ nulo

$l_t = @() asigna una matriz vacía a $ l_t .

$l_t2 = emptyArray asigna $ null a $ l_t2 , porque la función emptyArray devuelve una colección vacía y, por lo tanto, el resultado devuelto es $ null .

$ l_t2 y $ l_t3 son nulos y se comportan de la misma manera. Como ha declarado previamente $ l_t como una matriz vacía, cuando le agrega $ l_t2 o $ l_t3 , ya sea con el operador + = o la función addToArray , un elemento cuyo valor es ** $ null * se agrega a la matriz

Si desea forzar la función para preservar el tipo de datos del objeto de colección que está devolviendo, use el operador de coma:

PS> function emptyArray {,@()} PS> $l_t2 = emptyArray PS> $l_t2.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array PS> $l_t2.Count 0

Nota: Los paréntesis vacíos después de emtpyArray en la declaración de función son superfluos. Solo necesita paréntesis después del nombre de la función si los está usando para declarar parámetros.

Un punto interesante a tener en cuenta es que el operador de coma no necesariamente convierte el valor de retorno en una matriz.

Recuerde que, como mencioné en el primer punto, por defecto, el tipo de datos del resultado devuelto de una colección con más de un elemento es System.Array, independientemente del tipo de datos real de la colección. Por ejemplo:

PS> $list = New-Object -TypeName System.Collections.Generic.List[int] PS> $list.Add(1) PS> $list.Add(2) PS> $list.Count 2 PS> $list.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True List`1 System.Object

Tenga en cuenta que el tipo de datos de esta colección es List`1 , no System.Array .

Sin embargo, si lo devuelve desde una función, dentro de la función, el tipo de datos de $ list es List`1 , pero se devuelve como un System.Array que contiene los mismos elementos.

PS> function Get-List {$list = New-Object -TypeName System.Collections.Generic.List[int]; $list.Add(1); $list.Add(2); return $list} PS> $l = Get-List PS> $l.Count 2 PS> $l.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array

Si desea que el resultado devuelto sea una colección del mismo tipo de datos que el que está dentro de la función que está devolviendo, el operador de coma logrará eso:

PS> function Get-List {$list = New-Object -TypeName System.Collections.Generic.List[int]; $list.Add(1); $list.Add(2); return ,$list} PS> $l = Get-List PS> $l.Count 2 PS> $l.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True List`1 System.Object

Esto no se limita a los objetos de colección tipo matriz. Hasta donde he visto, cada vez que PowerShell cambia el tipo de datos del objeto que está devolviendo, y desea que el valor devuelto conserve el tipo de datos original del objeto, puede hacerlo precediendo al objeto que se devuelve con una coma . Encontré este problema por primera vez al escribir una función que consultaba una base de datos y devolvía un objeto DataTable. El resultado devuelto fue una matriz de tablas hash en lugar de una DataTable. Cambiando return $my_datatable_object por return ,$my_datatable_object hizo que la función devolviera un objeto DataTable real.


Para complementar la excelente respuesta de PetSerAl con un resumen pragmático :

  • Los comandos que no producen resultados no devuelven $null , pero el [System.Management.Automation.Internal.AutomationNull]::Value singleton , que puede considerarse como un "valor de matriz $null " o, para acuñar un término, matriz nula .

    • Tenga en cuenta que, debido al desenvolvimiento de colecciones de PowerShell, incluso un comando que genera explícitamente un objeto de colección vacío como @() no tiene salida (a menos que se Write-Output -NoEnumerate explícitamente la enumeración, como con Write-Output -NoEnumerate ).
  • En resumen, este valor especial se comporta como $null en contextos escalares y como una matriz vacía en contextos de matriz / canalización , como lo demuestran los ejemplos a continuación.

Advertencias :

  • Al pasar [System.Management.Automation.Internal.AutomationNull]::Value como [System.Management.Automation.Internal.AutomationNull]::Value un cmdlet / parámetro de función , invariablemente lo convierte a $null .

  • En PSv3 + , incluso un $null real (escalar) no se enumera en un bucle foreach ; se enumera en una tubería, sin embargo, ver abajo.

  • En PSv2- , guardar una matriz nula en una variable la convirtió silenciosamente a $null y $null se enumeró en un bucle foreach (no solo en una tubería) - ver abajo.

# A true $null value: $v1 = $null # An operation with no output returns # the [System.Management.Automation.Internal.AutomationNull]::Value singleton, # which is treated like $null in a scalar expression context, # but behaves like an empty array in a pipeline or array expression context. $v2 = & {} # calling (&) an empty script block ({}) produces no output # In a *scalar expression*, [System.Management.Automation.Internal.AutomationNull]::Value # is implicitly converted to $null, which is why all of the following commands # return $true. $null -eq $v2 $v1 -eq $v2 $null -eq [System.Management.Automation.Internal.AutomationNull]::Value & { param($param) $null -eq $param } $v2 # By contrast, in a *pipeline*, $null and # [System.Management.Automation.Internal.AutomationNull]::Value # are NOT the same: # Actual $null *is* sent as data through the pipeline: # The (implied) -Process block executes once. $v1 | % { ''input received'' } # -> ''input received'' # [System.Management.Automation.Internal.AutomationNull]::Value is *not* sent # as data through the pipeline, it behaves like an empty array: # The (implied) -Process block does *not* execute (but -Begin and -End blocks would). $v2 | % { ''input received'' } # -> NO output; effectively like: @() | % { ''input received'' } # Similarly, in an *array expression* context # [System.Management.Automation.Internal.AutomationNull]::Value also behaves # like an empty array: (@() + $v2).Count # -> 0 - contrast with (@() + $v1).Count, which returns 1. # CAVEAT: Passing [System.Management.Automation.Internal.AutomationNull]::Value to # *any parameter* converts it to actual $null, whether that parameter is an # array parameter or not. # Passing [System.Management.Automation.Internal.AutomationNull]::Value is equivalent # to passing true $null or omitting the parameter (by contrast, # passing @() would result in an actual, empty array instance). & { param([object[]] $param) [Object].GetMethod(''ReferenceEquals'').Invoke($null, @($null, $param)) } $v2 # -> $true; would be the same with $v1 or no argument at all.

La [System.Management.Automation.Internal.AutomationNull]::Value documentación del [System.Management.Automation.Internal.AutomationNull]::Value establece:

Cualquier operación que no devuelva ningún valor real debería devolver AutomationNull.Value .

Cualquier componente que evalúe una expresión de Windows PowerShell debe estar preparado para recibir y descartar este resultado. Cuando se recibe en una evaluación donde se requiere un valor, debe reemplazarse por null .

PSv2 vs. PSv3 + e inconsistencias generales :

PSv2 no ofreció distinción entre [System.Management.Automation.Internal.AutomationNull]::Value y $null para los valores almacenados en variables :

  • El uso de un comando sin salida directamente en una declaración / canalización foreach funcionó como se esperaba : no se envió nada a través de la canalización / no se ingresó el bucle foreach :

    Get-ChildItem nosuchfiles* | ForEach-Object { ''hi'' } foreach ($f in (Get-ChildItem nosuchfiles*)) { ''hi'' }

  • Por el contrario, si un comando sin salida se guardó en una variable o se usó un $null explícito, el comportamiento era diferente :

    # Store the output from a no-output command in a variable. $result = Get-ChildItem nosuchfiles* # PSv2-: quiet conversion to $null happens here # Enumerate the variable. $result | ForEach-Object { ''hi1'' } foreach ($f in $result) { ''hi2'' } # Enumerate a $null literal. $null | ForEach-Object { ''hi3'' } foreach ($f in $null) { ''hi4'' }

    • PSv2 : todos los comandos anteriores generan una cadena que comienza con hi , porque $null se envía a través de la tubería / se enumera por foreach :
      A diferencia de PSv3 +, [System.Management.Automation.Internal.AutomationNull]::Value se convierte en $null al asignar a una variable , y $null siempre se enumera en PSv2 .

    • PSv3 + : el comportamiento cambió en PSv3 , tanto para bien como para mal:

      • Mejor : no se envía nada a través de la canalización para los comandos que enumeran $result : el bucle foreach no se ingresa , porque el [System.Management.Automation.Internal.AutomationNull]::Value se conserva al asignar a una variable , a diferencia de PSv2 .

      • Posiblemente peor: foreach ya no enumera $null (ya sea especificado como literal o almacenado en una variable), por lo que foreach ($f in $null) { ''hi4'' } quizás sorprendentemente no produce ningún resultado.
        En el lado positivo, el nuevo comportamiento ya no enumera las variables no inicializadas , que se evalúan como $null (a menos que se Set-StrictMode completo con Set-StrictMode ).
        En general, sin embargo, no enumerar $null habría estado más justificado en PSv2, dada su incapacidad para almacenar el valor de colección nula en una variable.

En resumen , el comportamiento de PSv3 + :

  • elimina la capacidad de distinguir entre $null y [System.Management.Automation.Internal.AutomationNull]::Value en el contexto de una instrucción foreach

  • por lo tanto, introduce una inconsistencia con el comportamiento de la tubería , donde se respeta esta distinción.

En aras de la compatibilidad con versiones anteriores, el comportamiento actual no se puede cambiar. Este comentario sobre GitHub propone una forma de resolver estas inconsistencias para una posible versión futura de PowerShell que no necesita ser compatible con versiones anteriores.