【问题标题】:Multiple comparisons in foreach with an array of objectsforeach中与对象数组的多重比较
【发布时间】:2021-06-13 00:24:57
【问题描述】:

我有两个对象数组。

$animals 包含每个动物一个对象,其属性为“id”、“sort”和“alive”(“yes”或“no”)。

$inventory 为 $animals 中的每个动物(在“id”上匹配)包含一个匹配对象,其信息(属性)为“id”、“type”和“validated”($true 或 $false)。

我想在 $animals 中循环遍历我的所有动物,其中 "alive=yes" 并且:

  1. 如果 $inventory 中的“type=reptile”,则不执行任何操作。
  2. 将动物送到 $inventory 中“validated=$false”的兽医处。

代码:

$animals = @(
    [pscustomobject]@{id=1;sort='cat';alive='yes'}
    [pscustomobject]@{id=2;sort='dog';alive='yes'}
    [pscustomobject]@{id=3;sort='mouse';alive='no'}
    [pscustomobject]@{id=4;sort='anaconda';alive='yes'}
    [pscustomobject]@{id=5;sort='cobra';alive='yes'}
    )

$inventory = @(
    [pscustomobject]@{id=1;type='mammal';validated=$false}
    [pscustomobject]@{id=2;type='mammal';validated=$true}
    [pscustomobject]@{id=3;type='mammal';validated=$false}
    [pscustomobject]@{id=4;type='reptile';validated=$false}
    [pscustomobject]@{id=5;type='reptile';validated=$true}
    )


foreach ($animal in $($animals.Where( {$_."alive" -eq "yes"} ))) {
    if ($inventory.Where( { (($_."id" -eq $animal."id") -and ($_."type" -eq "reptile")) } ))  {
        "Skip"
    } elseif ($inventory.Where( { (($_."id" -eq $animal."id") -and ($_."validated" -eq $false)) } ))  {
        "Email vet: $($animal) needs to be validated!"
    }
}

虽然这工作得很好,但循环遍历所有“type=reptile”是非常低效的,即使我不打算对它们做任何事情。如果我能以某种方式直接在我的 foreach 中或在进入 foreach 之前以某种方式整理出“type=reptile”,那将更有效(也更优雅)。有谁知道以比示例更有效的方式直接在 foreach 中或之前整理“type=reptile”的任何方法?

【问题讨论】:

  • 将两个导入的数组合并为一个数组,然后使用复合Where-Object测试根据需要包含或排除。
  • 正如@Lee_Dailey 建议的那样,我打算使用add a Join-Object cmdlet to the standard PowerShell equipment #14994 来促进一个简单的($animals |Join $inventory -on Id |Where alive -eq 'yes' ...) 语法。否则,您可能希望基于id 创建一个哈希表来链接这些表。

标签: arrays powershell object conditional-statements


【解决方案1】:

信不信由你,带有if 条件的foreachfor 循环是Powershell 中过滤数组的最快方法,而where-object 是最慢的。

正如 Lee_Dailey 在评论中指出的那样,在我看来,最优雅的方法是合并两个数组,但在这种情况下,对于您的示例,考虑到 $animals$inventory 的计数将始终为相同,两个数组将始终为 ordered by ID:

$animals = @(
    [pscustomobject]@{id=1;sort='cat';alive='yes'}
    [pscustomobject]@{id=2;sort='dog';alive='yes'}
    [pscustomobject]@{id=3;sort='mouse';alive='no'}
    [pscustomobject]@{id=4;sort='anaconda';alive='yes'}
    [pscustomobject]@{id=5;sort='cobra';alive='yes'}
    )

$inventory = @(
    [pscustomobject]@{id=1;type='mammal';validated=$false}
    [pscustomobject]@{id=2;type='mammal';validated=$true}
    [pscustomobject]@{id=3;type='mammal';validated=$false}
    [pscustomobject]@{id=4;type='reptile';validated=$false}
    [pscustomobject]@{id=5;type='reptile';validated=$true}
    )

for($i=0;$i -lt $inventory.Count;$i++)
{
    if($inventory[$i].Type -ne 'reptile' -and !$inventory[$i].validated)
    {
        "Email vet: $($animals[$i].Sort) needs to be validated!"
    }
}

PS /home/>
Email vet: cat needs to be validated!
Email vet: mouse needs to be validated!

This is the non elegant / ugly way:

$animals.where({$_.ID -in ($inventory.where({$_.Type -ne 'reptile' -and !$_.Validated})).ID}).foreach({
    "Email vet: $($_.Sort) needs to be validated!"
})

PS /home/>
Email vet: cat needs to be validated!
Email vet: mouse needs to be validated!

【讨论】:

    【解决方案2】:

    由于您总是id 上进行过滤,因此您不妨提前进行:

    foreach($animal in $animals |Where-Object alive -eq yes){
      $filteredInventory = $inventory |Where-Object id -eq $animal.id
      # ...
    }
    

    如果您想断言集合中的 any 对象是否满足特定条件,但您实际上并不关心结果对象,您可以做以下几件事:

    ... |Select-Object -First 1

    Select-Object -First $N 能够提前“中止”管道,因此您可以执行以下操作:

    foreach($animal in $animals |Where-Object alive -eq yes){
      $filteredInventory = $inventory |Where-Object id -eq $animal.id
    
      if($filteredInventory |Where-Object type -eq reptile |Select-Object -First 1){
        "Skip"
      }
    }
    

    .Where({...}, 'First')

    如果您想使用Where() 扩展方法,您可以通过指定First 选择模式来要求它类似地“提前中止”:

    if($inventory.Where({$_.type -eq 'reptile'}, 'First')){
      "Skip"
    }
    

    假设您的实际用例在每个集合中有超过 5 个对象,更好的整体改进是使用哈希表对库存进行索引:

    $inventory = @{}
    @(
        [pscustomobject]@{id=1;type='mammal';validated=$false}
        [pscustomobject]@{id=2;type='mammal';validated=$true}
        [pscustomobject]@{id=3;type='mammal';validated=$false}
        [pscustomobject]@{id=4;type='reptile';validated=$false}
        [pscustomobject]@{id=5;type='reptile';validated=$true}
    ) |ForEach-Object {
        # Index all objects in inventory by their id
        $inventory[$_.id] = $_
    }
    

    现在通过 id 进行初始查找将比以前快得多,而且由于我们知道每个字典条目只包含一个对象,所以它会很简单:

    foreach($animal in $animals |Where-Object alive -eq yes){
      $inventoryRecord = $inventory[$animal.id]
    
      if($null -eq $inventoryRecord){
        Write-Warning "No inventory record exists for animal with id $($animal.id)"
        continue
      }
    
      if($inventoryRecord.type -eq 'reptile'){
        # skip
        continue
      }
    
      "Email vet: $($animal) needs to be validated!"
    }
    

    【讨论】:

    • 感谢@MathiasR.Jessen 的建议,它帮助我在现实世界的用例中节省了宝贵的时间(超过一分钟)。
    【解决方案3】:

    我会首先创建一个没有爬行动物的过滤库存列表。我还会将其设为查找表,以便更轻松/更快地引用。

    $inventorylookup = $inventory | Where-Object type -ne 'reptile' | Group-Object -Property Id -AsHashTable
    
    $animals | Where-Object {$_.alive -eq 'yes' -and $_.id -in $inventorylookup.values.id} | ForEach-Object {
        if($inventorylookup[$_.id].validated -eq $true){
            Write-Host skip
        }
        else {
            Write-Host Email vet: $_.sort, ID $_.id, needs to be validated!
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多