Search code examples
classpowershellyield-return

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

  • 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.