เลือกค่าของคุณสมบัติหนึ่งบนวัตถุทั้งหมดของอาร์เรย์ใน PowerShell


135

สมมติว่าเรามีอาร์เรย์ของวัตถุ $ object สมมติว่าวัตถุเหล่านี้มีคุณสมบัติ "ชื่อ"

นี่คือสิ่งที่ฉันต้องการทำ

 $results = @()
 $objects | %{ $results += $_.Name }

วิธีนี้ได้ผล แต่จะทำได้ดีกว่านี้หรือไม่?

หากฉันทำสิ่งที่ชอบ:

 $results = objects | select Name

$resultsคืออาร์เรย์ของวัตถุที่มีคุณสมบัติ Name ฉันต้องการให้ $ results มีอาร์เรย์ของ Names

มีวิธีที่ดีกว่า?


4
เพียงเพื่อความสมบูรณ์นอกจากนี้คุณยังสามารถลบเครื่องหมาย "+ =" จากรหัสเดิมของคุณเพื่อให้ foreach $results = @($objects | %{ $_.Name })เพียงเลือกชื่อ: บางครั้งการพิมพ์ในบรรทัดคำสั่งจะสะดวกกว่าแม้ว่าฉันคิดว่าคำตอบของสก็อตต์มักจะดีกว่า
Emperor XLII

1
@EmperorXLII: จุดดีและใน PSv3 + คุณสามารถทำให้ง่ายขึ้นถึง:$objects | % Name
mklement0

คำตอบ:


212

ฉันคิดว่าคุณอาจสามารถใช้ExpandPropertyพารามิเตอร์ของSelect-Object.

ตัวอย่างเช่นในการรับรายการของไดเร็กทอรีปัจจุบันและเพียงแค่มีคุณสมบัติ Name ปรากฏขึ้นเราจะทำสิ่งต่อไปนี้:

ls | select -Property Name

สิ่งนี้ยังคงส่งคืนอ็อบเจ็กต์ DirectoryInfo หรือ FileInfo คุณสามารถตรวจสอบประเภทที่มาทางท่อได้ตลอดเวลาโดยส่งไปที่Get-Member (นามแฝงgm)

ls | select -Property Name | gm

ดังนั้นหากต้องการขยายวัตถุให้เป็นประเภทของคุณสมบัติที่คุณกำลังมองหาคุณสามารถทำสิ่งต่อไปนี้:

ls | select -ExpandProperty Name

ในกรณีของคุณคุณสามารถทำสิ่งต่อไปนี้เพื่อให้ตัวแปรเป็นอาร์เรย์ของสตริงโดยที่สตริงคือคุณสมบัติ Name:

$objects = ls | select -ExpandProperty Name

73

เป็นวิธีแก้ปัญหาที่ง่ายยิ่งขึ้นคุณสามารถใช้:

$results = $objects.Name

ซึ่งควรกรอกข้อมูล$resultsที่มีมากมายของทุก 'ชื่อ' $objectsค่าคุณสมบัติขององค์ประกอบใน


Exchange Management Shellหมายเหตุว่านี้ไม่ได้ทำงานใน เมื่อใช้ Exchange เราจำเป็นต้องใช้$objects | select -Property Propname, OtherPropname
Bassie

2
@Bassie: การเข้าถึงสถานที่ให้บริการในระดับคอลเลกชันที่จะได้รับค่าของสมาชิกเป็นอาร์เรย์จะเรียกว่าสมาชิกแจงนับและเป็นคุณลักษณะ PSv3 + ; สันนิษฐานว่า Exchange Management Shell ของคุณคือ PSv2
mklement0

32

เพื่อเสริมมาก่อนคำตอบที่เป็นประโยชน์กับการแนะนำของเมื่อใช้งานง่ายซึ่งวิธีการและการเปรียบเทียบประสิทธิภาพ

  • นอกท่อให้ใช้ (PSv3 +):

    $ วัตถุ ชื่อ
    ดังที่แสดงให้เห็นในคำตอบของ rageandqqซึ่งทั้งง่ายกว่าและเร็วกว่ามาก

    • การเข้าถึงคุณสมบัติที่ระดับคอลเลกชันเพื่อรับค่าของสมาชิกเป็นอาร์เรย์เรียกว่าการแจงนับสมาชิกและเป็นคุณสมบัติ PSv3 +
    • หรือในPSv2ให้ใช้foreach คำสั่งซึ่งคุณสามารถกำหนดผลลัพธ์ให้กับตัวแปรได้โดยตรง:
      $ results = foreach ($ obj ใน $ objects) {$ obj.Name}
    • การแลกเปลี่ยน :
      • ทั้งคอลเลกชันอินพุทและเอาท์พุทอาร์เรย์ ต้องพอดีในหน่วยความจำที่เป็นทั้ง
      • หากคอลเลกชันอินพุตเป็นผลลัพธ์ของคำสั่ง (ไปป์ไลน์) (เช่น(Get-ChildItem).Name) ก่อนอื่นคำสั่งนั้นจะต้องรันจนเสร็จสิ้นก่อนจึงจะสามารถเข้าถึงองค์ประกอบของอาร์เรย์ที่เป็นผลลัพธ์ได้
  • ในท่อที่ต้องประมวลผลผลลัพธ์เพิ่มเติมหรือผลลัพธ์ไม่พอดีกับหน่วยความจำโดยรวมให้ใช้:

    $ วัตถุ | Select-Object -ExpandProperty Name

    • จำเป็นที่จะต้อง-ExpandPropertyมีการอธิบายในคำตอบที่สกอตต์ของซาด
    • คุณจะได้รับประโยชน์ไปป์ไลน์ตามปกติของการประมวลผลแบบหนึ่งต่อหนึ่งซึ่งโดยทั่วไปจะสร้างเอาต์พุตทันทีและทำให้การใช้หน่วยความจำคงที่ (เว้นแต่คุณจะรวบรวมผลลัพธ์ในหน่วยความจำในท้ายที่สุด)
    • แลกเปลี่ยน :
      • การใช้งานของท่อค่อนข้างช้า

สำหรับคอลเลกชันอินพุตขนาดเล็ก (อาร์เรย์) คุณอาจไม่สังเกตเห็นความแตกต่างและโดยเฉพาะอย่างยิ่งในบรรทัดคำสั่งบางครั้งความสามารถในการพิมพ์คำสั่งได้อย่างง่ายดายนั้นสำคัญกว่า


นี่คือง่ายต่อการอีกทางเลือกหนึ่งซึ่ง แต่เป็นที่ช้าที่สุดวิธี ; มันใช้ไวยากรณ์แบบง่ายที่ForEach-Objectเรียกว่าคำสั่งการดำเนินการ (อีกครั้ง PSv3 +):; เช่นโซลูชัน PSv3 + ต่อไปนี้ง่ายต่อการผนวกเข้ากับคำสั่งที่มีอยู่:

$objects | % Name      # short for: $objects | ForEach-Object -Process { $_.Name }

เพื่อความสมบูรณ์: วิธีการอาร์เรย์ PSv4 + ที่.ForEach() ไม่ค่อยมีใครรู้จักซึ่งมีการกล่าวถึงในบทความนี้เป็นอีกทางเลือกหนึ่ง :

# By property name (string):
$objects.ForEach('Name')

# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
  • วิธีนี้คล้ายกับการแจกแจงสมาชิกโดยมีการแลกเปลี่ยนเดียวกันยกเว้นว่าจะไม่ใช้ตรรกะของท่อ มันช้ากว่าเล็กน้อยแม้ว่าจะยังเร็วกว่าไปป์ไลน์อย่างเห็นได้ชัด

  • สำหรับการแยกค่าคุณสมบัติเดียวตามชื่อ ( อาร์กิวเมนต์สตริง ) โซลูชันนี้จะเทียบเท่ากับการแจงนับสมาชิก (แม้ว่าหลังจะง่ายกว่าทางไวยากรณ์)

  • สคริปต์บล็อกตัวแปรช่วยให้พลแปลง ; มันเป็นได้เร็วขึ้น - all-in-หน่วยความจำในครั้งเดียว - ทางเลือกให้กับท่อที่ใช้ForEach-Object cmdlet (% )


การเปรียบเทียบประสิทธิภาพของแนวทางต่างๆ

ต่อไปนี้คือตัวอย่างการกำหนดเวลาสำหรับวิธีการต่างๆโดยพิจารณาจากคอลเล็กชันอินพุตของ10,000อ็อบเจ็กต์โดยเฉลี่ยใน 10 รัน ตัวเลขที่แน่นอนไม่สำคัญและแตกต่างกันไปตามปัจจัยหลายประการ แต่ควรให้ความรู้สึกถึงประสิทธิภาพที่สัมพันธ์กัน (การกำหนดเวลามาจาก Windows 10 VM แบบ single-core:

สำคัญ

  • ประสิทธิภาพสัมพัทธ์จะแตกต่างกันไปขึ้นอยู่กับว่าออบเจ็กต์อินพุตเป็นอินสแตนซ์ของชนิด. NET ปกติ (เช่นเอาต์พุตโดยGet-ChildItem) หรือ[pscustomobject]อินสแตนซ์ (เช่นเอาต์พุตโดยConvert-FromCsv)
    เหตุผลก็คือ[pscustomobject]คุณสมบัติได้รับการจัดการแบบไดนามิกโดย PowerShell และสามารถเข้าถึงได้เร็วกว่าคุณสมบัติทั่วไปของชนิด. NET ปกติ (กำหนดแบบคงที่) สถานการณ์ทั้งสองจะกล่าวถึงด้านล่าง

  • การทดสอบใช้คอลเลกชันที่มีอยู่แล้วในหน่วยความจำเต็มเป็นอินพุตเพื่อมุ่งเน้นไปที่ประสิทธิภาพการสกัดคุณสมบัติบริสุทธิ์ ด้วยการเรียกใช้ cmdlet / function แบบสตรีมเป็นอินพุตความแตกต่างของประสิทธิภาพโดยทั่วไปจะมีความเด่นชัดน้อยกว่ามากเนื่องจากเวลาที่ใช้ในการโทรนั้นอาจเป็นส่วนใหญ่ของเวลาที่ใช้

  • เพื่อความกะทัดรัด%จะใช้นามแฝงสำหรับForEach-Objectcmdlet

ข้อสรุปทั่วไปใช้ได้กับทั้งประเภท. NET ปกติและ[pscustomobject]อินพุต:

  • การแจงนับสมาชิก ( $collection.Name) และforeach ($obj in $collection)โซลูชันเป็นวิธีที่เร็วที่สุดโดยมีค่า 10 หรือเร็วกว่าโซลูชันที่ใช้ไปป์ไลน์ที่เร็วที่สุด

  • น่าแปลกที่% Nameมีประสิทธิภาพมากยิ่งกว่า% { $_.Name }- เห็นปัญหา GitHub นี้

  • PowerShell Core มีประสิทธิภาพเหนือกว่า Windows Powershell ที่นี่อย่างต่อเนื่อง

การกำหนดเวลาด้วยประเภท. NET ปกติ :

  • PowerShell Core v7.0.0-preview.3
Factor Command                                       Secs (10-run avg.)
------ -------                                       ------------------
1.00   $objects.Name                                 0.005
1.06   foreach($o in $objects) { $o.Name }           0.005
6.25   $objects.ForEach('Name')                      0.028
10.22  $objects.ForEach({ $_.Name })                 0.046
17.52  $objects | % { $_.Name }                      0.079
30.97  $objects | Select-Object -ExpandProperty Name 0.140
32.76  $objects | % Name                             0.148
  • Windows PowerShell v5.1.18362.145
Comparing property-value extraction methods with 10000 input objects, averaged over 10 runs...

Factor Command                                       Secs (10-run avg.)
------ -------                                       ------------------
1.00   $objects.Name                                 0.012
1.32   foreach($o in $objects) { $o.Name }           0.015
9.07   $objects.ForEach({ $_.Name })                 0.105
10.30  $objects.ForEach('Name')                      0.119
12.70  $objects | % { $_.Name }                      0.147
27.04  $objects | % Name                             0.312
29.70  $objects | Select-Object -ExpandProperty Name 0.343

สรุป:

  • ใน PowerShell หลัก , มีประสิทธิภาพดีกว่าอย่างเห็นได้ชัด.ForEach('Name') .ForEach({ $_.Name })ใน Windows PowerShell อยากรู้อยากเห็นว่ารุ่นหลังเร็วกว่าแม้ว่าจะเพียงเล็กน้อยก็ตาม

การกำหนดเวลาด้วย[pscustomobject]อินสแตนซ์ :

  • PowerShell Core v7.0.0-preview.3
Factor Command                                       Secs (10-run avg.)
------ -------                                       ------------------
1.00   $objects.Name                                 0.006
1.11   foreach($o in $objects) { $o.Name }           0.007
1.52   $objects.ForEach('Name')                      0.009
6.11   $objects.ForEach({ $_.Name })                 0.038
9.47   $objects | Select-Object -ExpandProperty Name 0.058
10.29  $objects | % { $_.Name }                      0.063
29.77  $objects | % Name                             0.184
  • Windows PowerShell v5.1.18362.145
Factor Command                                       Secs (10-run avg.)
------ -------                                       ------------------
1.00   $objects.Name                                 0.008
1.14   foreach($o in $objects) { $o.Name }           0.009
1.76   $objects.ForEach('Name')                      0.015
10.36  $objects | Select-Object -ExpandProperty Name 0.085
11.18  $objects.ForEach({ $_.Name })                 0.092
16.79  $objects | % { $_.Name }                      0.138
61.14  $objects | % Name                             0.503

สรุป:

  • สังเกตว่าการ[pscustomobject]ป้อนข้อมูล.ForEach('Name')ทำได้ดีกว่าตัวแปรที่ใช้บล็อกสคริปต์.ForEach({ $_.Name })อย่างไร

  • ในทำนองเดียวกันการ[pscustomobject]ป้อนข้อมูลทำให้การใช้ไปป์ไลน์Select-Object -ExpandProperty Nameเร็วขึ้นใน Windows PowerShell แทบจะเทียบเท่ากับ.ForEach({ $_.Name })แต่ใน PowerShell Core ยังคงช้าลงประมาณ 50%

  • กล่าวโดยย่อ: ด้วยข้อยกเว้นที่แปลก% Nameด้วย[pscustomobject]วิธีการอ้างอิงคุณสมบัติแบบสตริงจะมีประสิทธิภาพดีกว่าคุณสมบัติที่ใช้สคริปต์บล็อก


ซอร์สโค้ดสำหรับการทดสอบ :

บันทึก:

  • ดาวน์โหลดฟังก์ชันTime-CommandจากGist นี้เพื่อเรียกใช้การทดสอบเหล่านี้

  • ตั้งค่า$useCustomObjectInputเป็น$trueวัดด้วย[pscustomobject]อินสแตนซ์แทน

$count = 1e4 # max. input object count == 10,000
$runs  = 10  # number of runs to average 

# Note: Using [pscustomobject] instances rather than instances of 
#       regular .NET types changes the performance characteristics.
# Set this to $true to test with [pscustomobject] instances below.
$useCustomObjectInput = $false

# Create sample input objects.
if ($useCustomObjectInput) {
  # Use [pscustomobject] instances.
  $objects = 1..$count | % { [pscustomobject] @{ Name = "$foobar_$_"; Other1 = 1; Other2 = 2; Other3 = 3; Other4 = 4 } }
} else {
  # Use instances of a regular .NET type.
  # Note: The actual count of files and folders in your home dir. tree
  #       may be less than $count
  $objects = Get-ChildItem -Recurse $HOME | Select-Object -First $count
}

Write-Host "Comparing property-value extraction methods with $($objects.Count) input objects, averaged over $runs runs..."

# An array of script blocks with the various approaches.
$approaches = { $objects | Select-Object -ExpandProperty Name },
              { $objects | % Name },
              { $objects | % { $_.Name } },
              { $objects.ForEach('Name') },
              { $objects.ForEach({ $_.Name }) },
              { $objects.Name },
              { foreach($o in $objects) { $o.Name } }

# Time the approaches and sort them by execution time (fastest first):
Time-Command $approaches -Count $runs | Select Factor, Command, Secs*

1

ข้อควรระวังการแจงนับสมาชิกจะใช้ได้ก็ต่อเมื่อคอลเลกชันนั้นไม่มีสมาชิกที่มีชื่อเดียวกัน ดังนั้นหากคุณมีอาร์เรย์ของอ็อบเจ็กต์ FileInfo คุณจะไม่สามารถรับอาร์เรย์ของความยาวไฟล์ได้โดยใช้

 $files.length # evaluates to array length

และก่อนที่คุณจะพูดว่า "ชัดเจนดี" ให้พิจารณาสิ่งนี้ หากคุณมีอาร์เรย์ของวัตถุที่มีคุณสมบัติความจุแล้ว

 $objarr.capacity

หากว่าการทำงานที่ดีเว้นแต่ $ objarr เป็นจริงไม่ได้เป็น [อาร์เรย์] แต่สำหรับตัวอย่างเช่น [ArrayList] ดังนั้นก่อนที่จะใช้การแจงนับสมาชิกคุณอาจต้องดูภายในกล่องดำที่มีคอลเลกชันของคุณ

(หมายเหตุสำหรับผู้ดูแล: นี่ควรเป็นความคิดเห็นเกี่ยวกับคำตอบของ rageandqq แต่ฉันยังไม่มีชื่อเสียงมากพอ)


เป็นจุดที่ดี คำขอคุณลักษณะ GitHub นี้ขอไวยากรณ์แยกต่างหากสำหรับการแจกแจงสมาชิก วิธีแก้ปัญหาสำหรับการชนชื่อคือการใช้.ForEach()เมธอดอาร์เรย์ดังนี้$files.ForEach('Length')
mklement0
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.