Объекты без свойства '.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? Каков рекомендуемый способ справиться с этим? Вариант Б кажется мне запутанным и трудным для чтения. Вариант А работает, но не следует ли PowerShell преобразовывать его в массив для меня? Я поступаю совершенно неправильно?


person CobyCode    schedule 13.07.2017    source источник
comment
(Get-Services).Count работает, потому что Get-Services возвращает несколько объектов. Если вам нужен массив (потенциально из 0 или 1 объекта (ов)), используйте оператор подвыражения массива (@(...)), как предложено @PetSerAl выше.   -  person Mathias R. Jessen    schedule 13.07.2017
comment
@PetSerAl Спасибо. Разве это не то же самое, что [Array] $ Result = Command_Here?   -  person CobyCode    schedule 13.07.2017
comment
@ MathiasR.Jessen Спасибо! У меня создалось впечатление, что почти все в PowerShell настроено для обработки нескольких объектов? Даже (10 * 2) .Счет работает. Что определяет, может ли созданный объект обрабатывать несколько или отдельные объекты?   -  person CobyCode    schedule 13.07.2017
comment
@CobyCode Нет, они разные: [Array]$a = &{}; $b = @(&{}); $a.GetType(); $b.GetType() или [Array]$a = New-Object Object[] 10; $b = @(New-Object Object[] 10); $a.Count; $b.Count или [Array]$a = New-Object Collections.Generic.List[Object]; $b = @(New-Object Collections.Generic.List[Object]); $a.Count; $b.Count.   -  person user4003407    schedule 13.07.2017
comment
@PetSerAl Мне потребовалось время, чтобы сообразить! Я также нашел дополнительную информацию из этого вопроса ссылка, на который, похоже, вы тоже приложили руку. Я вижу, что они разные, но до сих пор не совсем понимаю, в чем разница. .GetType () показывает базовый тип system.array для них обоих. Также: Можете ли вы написать ответ, чтобы я мог отдать вам должное?   -  person CobyCode    schedule 14.07.2017


Ответы (2)


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

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

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

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

  • в качестве обходного пути для обнаруженной ошибки / ошибки Set-StrictMode -Version 2.

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


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

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

Кроме того, @(...) и [Array] ... в целом не эквивалентны, поскольку PetSerAl Полезные примеры в комментарии к вопросу демонстрируют; чтобы адаптировать один из его примеров:

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

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

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


Назначение @(...) и принцип его работы:

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

  • @(...) собирает закрытые выходные команды как есть / закрытые выражения перечисленные выходные данные в - < em> всегда новый - [object[]] массив, даже если имеется только единственный объект вывода.

  • @(...) никогда не нужен для литералов (в v5.1 + он оптимизирован), но он полезен в двух случаях:

    • для создания пустого массива: @()

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

      @(
        'one'
        Write-Output two
      )
      
  • Подводные камни:

    • @() не является конструктором массива, а является гарантом: поэтому @(@(1,2)) не создает вложенный массив:

      • @(@(1, 2)) is effectively the same as @(1, 2) (and just 1, 2). In fact, each additional @() is an expensive no-op, because it simply creates a copy of the array output by the previous one.
      • Используйте унарную форму , оператор конструктора массива для создания вложенных массивов:
        , (1, 2)
    • $null рассматривается @() как отдельный объект, поэтому в результате получается одноэлементный массив с элементом $null:

      • @($null).Count is 1
    • Вызовы Command, которые выводят единый массив в целом, приводят к вложенному массиву:

      • @(Write-Output -NoEnumerate 1, 2).Count is 1
    • В выражении упаковка коллекции любого типа в @() перечисляет и неизменно собирает элементы в (новом) [object[]] array:

      • @([System.Collections.ArrayList] (1, 2)).GetType().Name returns 'Object[]'

При необходимости читайте более подробную информацию.


Подробности:

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

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

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

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

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

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

    • Если результатом является одиночный элемент / не содержит никаких элементов, результат будет заключен в одноэлементный / пустой массив типа [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 в силу того, что он является командой (например, вызовом cmdlet), не подлежит перечислению (разворачиванию), что приводит к @(), чтобы увидеть только одиночный элемент (который оказывается массивом / коллекцией), который, следовательно, заключен в массив из одиночных элементов.

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

          • @([system.collections.arraylist]::new()).Count yields 0; the expression outputs an empty collection that is enumerated, and since there's noting to enumerate, @() creates an empty System.Object[] array.
            Note that, in PSv3+, simply using an extra set of parentheses ((...)) with New-Object - which converts the New-Object command to an expression - would yield the same result:
            @((New-Object System.Collections.ArrayList)).Count yields 0 too.
    • Если результат содержит несколько элементов, эти элементы возвращаются как обычный массив PowerShell ([System.Object[]]); например.:

      • With a command:
        • $arr = @(Get-ChildItem *.txt) collects the one-by-one streaming output from Get-ChildItem in a System.Object[] array
      • With an expression:
        • $arr = [int[]] (1, 2); @($arr) перечисляет [int[]] массив $arr, а затем переупаковывает элементы как System.Object[] массив.

        • Обратите внимание на неэффективность и потенциальную потерю верности типа этого процесса: исходный массив перечисляется и собирается в новом массив, который всегда имеет тип System.Object[]; эффективная альтернатива - преобразовать в [array] (который также работает с командами): [array] $result = ...

person mklement0    schedule 13.07.2017
comment
Спасибо! В этом есть смысл. Но тогда как лучше всего обойти это? Если я использую $ a = @ (...) позже, когда я использую значение $ a, оно не работает, когда я использую его в качестве входных данных для некоторого другого cmldet, ожидающего system.object [ ] ... итак '$ disk = @ (get-disk); новый-том $ диск 'завершится ошибкой. Думаю, я мог бы использовать «новый том $ disk [0]» или «$ disk = disk [0]; новый том $ disk '.... - person CobyCode; 14.07.2017
comment
Также следует отметить, что я, кажется, получаю это для всех командлетов, построенных на [Microsoft.Management.Infrastructure.CimInstance] - person CobyCode; 14.07.2017
comment
@CobyCode: $disk = @(get-disk) действительно возвращает экземпляр [System.Object[]], который вы можете проверить с помощью Get-Member -InputObject $disk. Насколько я могу судить, первым позиционным параметром New-Volume является тип скаляр, поэтому при попытке передать его экземпляру [System.Object[]] ожидается неудача; см. New-Volume -?. - person mklement0; 14.07.2017
comment
@ mklement0 Возможно, это самый четко написанный комментарий, который я когда-либо видел (за исключением вашего другого ответа, здесь. Спасибо! В соответствии с вашим предложением я отправлю это на uservoice. - person CobyCode; 14.07.2017

@ mklement0 имеет отличный ответ, но кое-что нужно добавить: если ваш скрипт (или скрипт, вызывающий ваш) имеет _ 1_, автоматически перестают работать свойства .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
person Carl Walsh    schedule 11.02.2021
comment
Спасибо и хороший замечание - я также обновил свой ответ. Эта ошибка существует уже давно и, в частности, была зарегистрирована самим Джеффри Сновером - см. выпуск GitHub # 2798. - person mklement0; 31.03.2021