Объекты без свойства.Count - использование @() (оператор подвыражения массива) по сравнению с приведением [Array]

Я пытаюсь выполнить некоторые простые операторы if, но все новые командлеты, основанные на [Microsoft.Management.Infrastructure.CimInstance], по-видимому, не предоставляют метод.count?

$Disks = Get-Disk
$Disks.Count

Ничего не возвращает Я обнаружил, что могу привести это как [массив], что заставляет его возвращать метод.NET .count, как и ожидалось.

[Array]$Disks = Get-Disk
$Disks.Count

Это работает без непосредственного преобразования его в массив для предыдущих командлетов:

(Get-Services).Count

Каков рекомендуемый способ обойти это?

Пример, который не работает:

$PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
  If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
   Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
   Else If ($PageDisk.Count -eq 1) { Do X }

Вариант А (в виде массива):

[Array]$PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
  If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
   Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
   Else If ($PageDisk.Count -eq 1) { Do X }

Вариант B (использовать массивные индексы):

 $PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
  If ($PageDisk[0] -eq $Null) {Write-Host "No suitable drives."; Continue}
   Else If ($PageDisk[1] -ne $Null) {Write-Host "Too many drives found, manually select it."}
   Else If (($PageDisk[0] -ne $Null) -and (PageDisk[1] -eq $Null)) { Do X }

Вариант C (Массив) - Спасибо @PetSerAl:

$PageDisk = @(Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)})
  If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
   Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
   Else If ($PageDisk.Count -eq 1) { Do X }

В чем причина того, что командлеты на основе CIM не предоставляют метод.Count? Каков рекомендуемый способ справиться с этим? Вариант B кажется мне запутанным и трудно читаемым. Вариант А работает, но разве PowerShell не должен преобразовать это в массив для меня? Я поступаю об этом совершенно неправильно?

2 ответа

Решение

В PSv3+ с унифицированной обработкой скаляров и коллекций любой объект - даже $null - должен иметь .Count собственность (и, за исключением $null, должен поддерживать индексацию с [0]).

Любое вхождение объекта, не поддерживающего вышеизложенное, должно рассматриваться как ошибка.

Например, [pscustomobject] экземпляры, не играющие по этим правилам, являются известной ошибкой.

Поскольку я не знаю, связана ли эта ошибка с [Microsoft.Management.Infrastructure.CimInstance#ROOT/Microsoft/Windows/Storage/MSFT_Disk] случаи, которые Get-Disk выходы, а так Get-Disk - по крайней мере, в настоящее время - доступно только в Windows PowerShell, я рекомендую вам подать отдельную ошибку на uservoice.com.

Использование оператора подвыражения массива @(...) необходимо только:

  • в качестве обходного пути для ошибки выше.

  • в случае, если скалярный объект имеет свой собственный .Count собственность


Как правило, если вам нужно убедиться, что что-то является массивом, используйте @(...) скорее, чем [Array] ... / [object[]] ... - @() PowerShell-идиоматичный, более краткий и синтаксически более простой.

Тем не менее, учитывая, что @() технически создает (мелкую) копию существующего массива, вы можете предпочесть [Array] при работе с потенциально большими массивами.

Дополнительно, @(...) а также [Array] ... как правило, не эквивалентны, как PetSerAl полезные примеры PetSerAl в комментарии к вопросу; адаптировать один из его примеров:

@($null) возвращает массив из одного элемента, чей единственный элемент $null, в то время как [Array] $null не имеет никакого эффекта (остается $null).

Это поведение @() соответствует его цели (см. ниже): так как $null это не массив, @() оборачивает его в один (в результате чего [System.Object[]] экземпляр с $null как единственный элемент).

В других примерах PetSerAl, @() поведение с New-Object созданные массивы и коллекции - может быть удивительным - см. ниже.


Цель @(...) и как это работает:

Цель @() оператор подвыражения массива, грубо говоря, гарантирует, что результат выражения / команды будет обрабатываться как массив, даже если это скаляр (единственный объект).

Точнее, @() ведет себя следующим образом: Наконечник шляпы PetSerAl за его обширную помощь.

  • В PSv5.1 +, используя выражение, которое напрямую создает массив, используя , (оператор выражения массива) оптимизирует @() далеко:

    • Например, @(1, 2) так же, как просто 1, 2, а также @(, 1) так же, как просто , 1,

    • В случае массива, построенного только с , - который дает System.Object[] массив - эта оптимизация полезна, потому что она экономит ненужный шаг: сначала разверните этот массив, а затем перепакуйте его (см. ниже).
      Предположительно, эта оптимизация была вызвана широко распространенной и ранее неэффективной практикой использования @( ..., ..., ...) строить массивы, исходя из ошибочного убеждения, что @() необходим для построения массива.

    • Однако в Windows PowerShell v5.1 только оптимизация также применяется при построении массива с определенным типом с использованием преобразования, такого как [int[]] (поведение было исправлено в PowerShell Core и более старые версии Windows PowerShell не затрагиваются); например,
      @([int[]] (1, 2)).GetType().Name доходность Int32[], Это единственная ситуация, в которой @() возвращает что-то кроме System.Object[] и предполагая, что это всегда может привести к неожиданным ошибкам и побочным эффектам; например:
      @([int[]] (1, 2))[-1] = 'foo' брейки.
      $a = [int[]] (1, 2); $b = @([int[]] $a) неожиданно не создает новый массив - смотрите эту проблему GitHub.

  • В противном случае: если (первое) утверждение внутри @(...) является выражением, оно собирается с использованием стандартной техники развертывания (разворачивания) PowerShell; однако вывод команды интерпретируется как есть; в любом случае итоговое количество объектов определяет поведение:

    • Если результат представляет собой один элемент / не содержит элементов, результат помещается в массив типа "один элемент / пустой" [System.Object[]],

      • Например, @('foo').GetType().Name доходность Object[] а также @('foo').Count доходность 1 (хотя, как указано, в PSv3+ вы можете использовать 'foo'.Count напрямую).
        @( & { } ).Count доходность 0 (выполнение пустого блока скрипта выдает "нулевую коллекцию" ([System.Management.Automation.Internal.AutomationNull]::Value)

      • Предостережение: @() вокруг New-Object вызов, который создает массив / коллекцию, выводит этот массив / коллекцию, обернутую во внешний элемент из одного элемента.

        • @(New-Object System.Collections.ArrayList).Count доходность 1 - пустой список массивов обернут в один элемент System.Object[] пример.
        • Причина в том, что New-Object в силу того, что команда (например , вызов командлета) не подлежит развертыванию, @() чтобы увидеть только один элемент (который является массивом / коллекцией), который он поэтому оборачивает в массив из одного элемента.

        • Что может сбить с толку, так это то, что этого не происходит, когда вы используете выражение для создания массива / коллекции, потому что вывод выражения развернут (развернут):

          • @([system.collections.arraylist]::new()).Count доходность 0; выражение выводит пустую коллекцию, развернутую до результата, не содержащего элементов, что @() переупакован как пустой System.Object[] массив.
            Обратите внимание, что в PSv3+ просто используется дополнительный набор скобок ((...)) с New-Object - который преобразует New-Object Команда для выражения - даст тот же результат:
            @((New-Object System.Collections.ArrayList)).Count доходность 0 тоже.
    • Если результат содержит несколько элементов, эти элементы возвращаются в виде массива (как правило, но не всегда в WinPSv5.1 - см. Выше) в виде обычного массива PowerShell ( [System.Object[]] ):

      • Например, $arr = [int[]] (1, 2); @($arr) раскатывает [int[]] массив $arr а затем переупаковывает элементы как System.Object[] массив.
        (Как обсуждалось выше, только в WinPSv5.1, если вы поместите выражение создания массива непосредственно внутри @(), вы на самом деле получаете [int[]] массив).

У @mklement0 есть отличный ответ, но нужно добавить: если ваш скрипт (или скрипт, вызывающий ваш) имеет Set-StrictMode, автоматический .Count а также .Length свойства перестают работать:

      $ Set-StrictMode -off
$ (42).Length
1
$ Set-StrictMode -Version Latest
$ (42).Length
ParentContainsErrorRecordException: The property 'Length' cannot be found on this object. Verify that the property exists.

На всякий случай вы можете заключить любую неизвестную переменную в массив @(...) перед проверкой длины:

      $ $result = Get-Something
$ @($result).Length
1
Другие вопросы по тегам