产量 return 相当于 PowerShell Class 方法

Yield return equivalent for PowerShell Class Method

对于 PowerShell,历史上不需要 yield return;因为这本质上就是管道。 但是,对于 PS5 的 classes,方法无法写入管道。因此,是否有任何选项可以模仿 Powershell class 方法的 yield return / pipeline 行为?

演示

函数

此代码returns数据到管道;我们可以看到变量 $global:i 被函数更新,然后在函数的下一次迭代之前管道中的下一步读取该值:

[int]$i = 0
function Get-PowerShellProcesses() {
    Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{$global:i++; $_}
}
Get-PowerShellProcesses |  %{"$i - $($_.ProcessName)}
输出:
1 - powershell 
2 - powershell_ise

Class 方法

如果我们用 class 的方法做同样的事情,除了在传递到管道之前收集完整的结果集之外,一切都一样。

[int]$i = 0
class Demo {
    Demo(){}
    [PSObject[]]GetPowershellProcesses() {
        return Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{$Global:i++; $_} 
    }
}
$demo = New-Object Demo
$demo.GetPowerShellProcesses() | %{"$i - $($_.ProcessName)"}
输出:
2 - powershell 
2 - powershell_ise

我猜没有解决办法;但希望有所作为。

为什么这很重要?

在上面的例子中显然不是。但是,这确实会对我们不需要完整结果集的情况产生影响;例如假设我们在函数调用后有一个 | Select-Object -First 10,但是有一个昂贵的操作 return 成千上万的结果,我们会看到显着的性能下降。

你试过什么?

内联Return:

Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}

错误:Not all code path returns value within method.

内联 Return + 最终 Return:

Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_} 
return

错误:Invalid return statement within non-void method

内联 Return + 最终 [void] / $null Return:

Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_} 
return [void] #or return $null

没有错误;但就像只调用了最后一个 return 语句一样;所以我们没有数据。

产量Return:

Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{yield return $_}

错误:The term 'yield' is not recognized ...

解决方法

简单的解决方法是将 C# classes 与 yield return 一起使用,或者改为使用传统的 PowerShell 函数。

我不明白为什么有人想在这里开始使用 class,但如果这是您的最终目标,您可以在不涉及 $global:i 的情况下执行此操作。 ForEach-Object 可以使用一个初始化脚本块。使用它来设置循环本地增量器 $i.

如果需要在列表中累积,我猜 $i 也可以是一个列表。

function Get-PowerShellProcesses() {
    Get-Process | Where-Object {$_.ProcessName -like '*powershell*'} 
}

Get-PowerShellProcesses | ForEach-Object {$i = 1} {"$i - $($_.ProcessName)"; $i++}

SeeminglyScience on GitHub 分享了此问题的答案;全部归功于他们。

You can use LINQ as a sort of work around. Here's how you could do your stack overflow example.

using namespace System.Collections.Generic
using namespace System.Diagnostics

[int]$i = 0

class Demo {
    [IEnumerable[Process]] GetPowershellProcesses() {
        return [Linq.Enumerable]::Select(
            [Process[]](Get-Process *powershell*),
            [Func[Process, Process]]{ param($p) $global:i++; $p })
    }
}

$demo = New-Object Demo
$demo.GetPowerShellProcesses() | %{ "$i - $($_.ProcessName)" }

However, variables from that scope may or may not be available depending on if the SessionStateScope that the enumerable was created in is still active during enumeration. This would likely also be an issue in any implementation of yield in PowerShell with the way script blocks currently work.