With PowerShell there's historically been no need for yield return
; since that's essentially what the pipeline is.
However, with PS5's classes, methods cannot write to the pipeline. As such, are there any options to mimic yield return
/ pipeline
behaviour from a Powershell class method?
This code returns data to the pipeline; we can see that the variable $global:i is updated by the function, then the value's read by the next step in the pipeline before the next iteration of the function:
[int]$i = 0
function Get-PowerShellProcesses() {
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{$global:i++; $_}
}
Get-PowerShellProcesses | %{"$i - $($_.ProcessName)}
Output:
1 - powershell
2 - powershell_ise
If we do the same with a class's method everything's the same except that the full result set is gathered before being passed on to the pipeline.
[int]$i = 0
class Demo {
Demo(){}
[PSObject[]]GetPowershellProcesses() {
return Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{$Global:i++; $_}
}
}
$demo = New-Object Demo
$demo.GetPowerShellProcesses() | %{"$i - $($_.ProcessName)"}
Output:
2 - powershell
2 - powershell_ise
I'm guessing there's no solution; but hoping there is something.
In the above example obviously it doesn't. However, this does have an impact where we don't need the full result set; e.g. say we had a | Select-Object -First 10
after the function call, but had an expensive operation returning thousands of results, we'd see a significant performance hit.
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}
Error: Not all code path returns value within method.
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}
return
Error: Invalid return statement within non-void method
[void]
/ $null
Return:Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}
return [void] #or return $null
No error; but acts as if only the last return statement were called; so we get no data.
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{yield return $_}
Error: The term 'yield' is not recognized ...
The simple workaround is to use C# classes with yield return
, or traditional PowerShell functions instead.
An answer to this question was shared by SeeminglyScience on GitHub; all credit to them.
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.