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ónaddToArray
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 seWrite-Output -NoEnumerate
explícitamente la enumeración, como conWrite-Output -NoEnumerate
).
-
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
-
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
.- Vea este problema de GitHub .
-
En PSv3 + , incluso un
$null
real (escalar) no se enumera en un bucleforeach
; 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 bucleforeach
(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.
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 bucleforeach
: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 porforeach
:
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 bucleforeach
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 queforeach ($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 seSet-StrictMode
completo conSet-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ónforeach
-
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.