'Yield return equivalent for PowerShell Class Method
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?
Demo
Function
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
Class Method
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.
Why does this matter?
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.
What have you tried?
Inline Return:
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}
Error: Not all code path returns value within method.
Inline Return + Final Return:
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}
return
Error: Invalid return statement within non-void method
Inline Return + Final [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.
Yield Return:
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{yield return $_}
Error: The term 'yield' is not recognized ...
Workaround
The simple workaround is to use C# classes with yield return
, or traditional PowerShell functions instead.
Solution 1:[1]
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.
Solution 2:[2]
I have no idea why one would like to involve a class here to begin with here but you can do this without involving the $global:i if that is your ultimate goal. ForEach-Object can take an init script block. Use that for setting up your loop local incrementer $i.
If one need to accumulate in a list I'd guess that $i could be a list as well.
function Get-PowerShellProcesses() {
Get-Process | Where-Object {$_.ProcessName -like '*powershell*'}
}
Get-PowerShellProcesses | ForEach-Object {$i = 1} {"$i - $($_.ProcessName)"; $i++}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | JohnLBevan |
Solution 2 |