Home > database >  What is '<Invoke>d__40'?
What is '<Invoke>d__40'?

Time:05-17

Using Powershell, System.Management.Automation.Cmdlet.Invoke() returns object of type '<Invoke>d__40' rather than specified OutputType.

To reproduce:

  1. Copy SendGreeting example cmdlet to .\ExampleCmdlet.cs
  2. powershell -NoProfile
  3. Add-Type -Path .\ExampleCmdlet.cs
  4. $command = [SendGreeting.SendGreetingCommand]::new()
  5. $command.Name = 'Person'
  6. $invoke = $command.Invoke()
  7. $invoke.GetType()

Expected: [string]
Actual: [<Invoke>d__40]

$PSVersionTable:

Name                           Value
----                           -----
PSVersion                      5.1.19041.1237
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.19041.1237
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

Get-Member -InputObject $invoke reveals this to implement IEnumerator and some playing with .MoveNext() and .Current will sometimes output the expected "Hello Person!" result.

What is this <Invoke>d__40 type?
Why is $command.Invoke() not returning the expected string output directly?

CodePudding user response:

<Invoke>d__40 is name of compiler generated class:

Cmdlet.Invoke/Cmdlet.Invoke<T> returns IEnumerable/IEnumerable<T> and is implemented using yield return which results in compiler generating as special named class (compiler can use < and > symbols in identifiers while developers can't) which implements IEnumerable/IEnumerable<T> (check out for example this decompilation).

CodePudding user response:

To complement Guru Stron's helpful answer, which explains that the name of the specific type returned is just an implementation detail; what matters is that the type implements the System.Collections.IEnumerable interface:

The fact that the type also implements the System.Collections.IEnumerator interface, as you've discovered, makes it a lazy (on-demand) enumerable: that is, the object returned doesn't itself contain data, it retrieves / generates data when enumerated.

If you output $invoke, PowerShell implicitly enumerates the enumerable, and you should see the expected outcome:

PS> $invoke  # enumeration happens here.
Hello Person!

Note that an attempt to access $invoke again produces no output, because the enumeration has completed (and even trying to reset it with .Reset() doesn't work, because the type implementing the interface doesn't support it).

  • Note: It is not unusual for lazy enumerables to support repeat enumeration, despite also not implementing the .Reset() method; e.g., in the following examples $enumerator can be enumerated repeatedly, and yields the same results every time: $enumerator = [System.Linq.Enumerable]::Range(1,10) and $enumerator = [System.IO.File]::ReadLines("$PWD/test.txt")

By contrast, assigning $invoke to a variable does not cause enumeration: $result = $invoke merely creates another reference to the enumerator itself.

In order to capture the actual object(s) to be enumerated, you must force enumeration via $(), the subexpression operator or @(), the array-subexpression operator; e.g.:

# Note: This assumes you haven't output $invoke by itself before.
$result = $($invoke) # force enumeration and store the enumerated object(s)

Taking a step back:

Lazy enumerables aren't that common in normal PowerShell code, and if you use them in an enumeration context - notably in the pipeline or in a foreach statement - they'll work as expected.

When you assign a lazy enumerable to a variable, you need to be aware that you're storing just the enumerator, not the data it will enumerate.

If you use your sample cmdlet as it is meant to be used - by invoking it as command Send-Greeting with a -Name argument - the lazy enumerable is eliminated from the picture, because cmdlets output actual data:

# Directly outputs string 'Hello Person!'
Send-Greeting -Name Person

To make your sample cmdlet callable this way, you need to not only load the implementing type's assembly into your session with Add-Type, you must additionally import it as a PowerShell module, with Import-Module:

# Compile and load the assembly, and also import it as a PowerShell module,
# so the cmdlet that is implemented surfaces as such.
(Add-type -PassThru -LiteralPath .\ExampleCmdlet.cs).Assembly | Import-Module

# Now you can call your Send-Greeting cmdlet.
Send-Greeting -Name Person
  • Related