Что определяет, развернет ли конвейер Powershell коллекцию? - PullRequest
9 голосов
/ 01 декабря 2009
# array
C:\> (1,2,3).count
3
C:\> (1,2,3 | measure).count
3

# hashtable
C:\> @{1=1; 2=2; 3=3}.count
3
C:\> (@{1=1; 2=2; 3=3} | measure).count
1

# array returned from function
C:\> function UnrollMe { $args }
C:\> (UnrollMe a,b,c).count
3
C:\> (UnrollMe a,b,c | measure).count
1
C:\> (1,2,3).gettype() -eq (UnrollMe a,b,c).gettype()
True

Расхождение с HashTables довольно хорошо известно , хотя в официальной документации упоминается только косвенно (на примере).

Проблема с функциями, однако, для меня новость. Я отчасти шокирован, что до сих пор меня не укусило. Есть ли какой-то руководящий принцип, которому мы, сценаристы, можем следовать? Я знаю, что при написании командлетов в C # возникает перегрузка WriteObject , где вы можете явно управлять перечислением, но AFAIK нет такой конструкции в самом языке Posh. Как показывает последний пример, интерпретатор Posh, похоже, считает, что нет никакой разницы в типе объектов, передаваемых по конвейеру. Я подозреваю, что может быть некоторая странность Object против PSObject под капотом, но это бесполезно, когда вы пишете чистый Posh и ожидаете, что язык сценариев «просто сработает».

/ РЕДАКТИРОВАТЬ /

Кит правильно указал, что в моем примере я передаю один строковый аргумент, а не 3 строковых аргумента. Другими словами, причина, по которой Measure-Object говорит, что Count = 1, заключается в том, что он видит один массив массивов, первый элемент которого равен @ ("a", "b", "c"). Справедливо. Эти знания позволяют обойти проблему несколькими способами:

# stick to single objects
C:\> (UnrollMe a b c | measure).count
3

# rewrite the function to handle nesting
C:\> function UnrollMe2 { $args[0] }
C:\> (UnrollMe2 a,b,c | measure).count
3

# ditto
C:\> function UnrollMe3 { $args | %{ $_ } }
C:\> (UnrollMe3 a,b,c | measure).count
3

Однако это не все объясняет ...

# as seen earlier - if we're truly returning @( @("a","b","c") ) why not count=1?
C:\> (UnrollMe a,b,c).count
3

# our theory must also explain these results:
C:\> ((UnrollMe a,b,c) | measure).count
3
C:\> ( @(@("a","b","c")) | measure).count
3
C:\> ((UnrollMe a,b,c d) | measure).count
2

Из того, что я могу экстраполировать, есть еще одно правило в игре: если у вас есть массив с ровно одним элементом И парсер находится в режиме выражения , то интерпретатор "развернет" указанный элемент. Есть еще какие-то тонкости, которые мне не хватает?

Ответы [ 2 ]

11 голосов
/ 02 декабря 2009

$ args развернут. Помните, что параметры функции обычно передаются с использованием пробела для их разделения. Когда вы передаете 1,2,3, вы передаете один аргумент, который представляет собой массив из трех чисел, которому присваивается $ args [0]:

PS> function UnrollMe { $args }
PS> UnrollMe 1 2 3 | measure

Count    : 3

Помещение результатов (массива) в выражение группировки (или подвыражение, например, $()) делает его снова пригодным для развертывания, поэтому следующее развертывает объект [], содержащий 1,2,3, возвращаемый UnrollMe:

PS> ((UnrollMe 1,2,3) | measure).Count
3

, что эквивалентно:

PS> ((1,2,3) | measure).Count
3

Кстати, это не относится только к массиву с одним элементом.

PS> ((1,2),3) | %{$_.GetType().Name}
Object[]
Int32

Использование подвыражения массива (@()) для чего-то, что уже является массивом, не имеет никакого значения, независимо от того, сколько раз вы применяете его. :-) Если вы хотите предотвратить развертывание, используйте оператор запятой, потому что он всегда создаст другой внешний массив, который будет развернут. Обратите внимание, что в этом сценарии вы на самом деле не предотвращаете развертывание, вы просто работаете над развертыванием, вводя внешний «упаковочный» массив, который развертывается вместо исходного массива, например ::11017*

PS> (,(1,2,3) | measure).Count
1

Наконец, когда вы выполните это:

PS> (UnrollMe a,b,c d) | %{$_.GetType().Name}
Object[]
String

Вы можете видеть, что UnrollMe возвращает два элемента (a, b, c) в виде массива и d в качестве скаляра. Эти два элемента отправляются по конвейеру отдельно, в результате чего получается число 2.

1 голос
/ 01 декабря 2009

Кажется, это как-то связано с тем, как работает Measure-Object и как объекты передаются по конвейеру.

Когда вы говорите

1,2,3 | measure

вы получаете 3 объекта Int32, переданных в конвейер, затем объект измерения подсчитывает каждый объект, который он видит в конвейере.

Когда вы «развертываете» его с помощью своей функции, вы получаете один объект массива, переданный в конвейер, который измеряет число объектов как 1, он не пытается перебирать объекты в массиве, как показано здесь:

PS C:\> (measure -input 1,2,3).count
1

Возможный обходной путь - «перекатить» массив на конвейер, используя foreach:

PS C:\> (UnrollMe 1,2,3 | %{$_} | measure).count
3
...