Normally, I'm able to use .ForEach({ ... })
on a System.Array
, which yields a System.Object
with name Collection'1
, that I'm again able to use ForEach({ ... })
on:
@(1,2,3).ForEach({ $_ }).ForEach({ $_ })
1
2
3
However, in the following code, I'm able to use .ForEach({ ... })
on the System.Object
with name List'1
, but it returns $null
. It took me long time to realize this, but I don't understand why it returns $null
.
Why does (ConvertFrom-Yaml $yaml).anArray.ForEach({ $_ })
return $null
?
Code:
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name powershell-yaml
$yaml = @"
anArray:
- 1
- 2
- 3
"@
(ConvertFrom-Yaml $yaml).anArray
1
2
3
(ConvertFrom-Yaml $yaml).anArray.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
# returns $null
(ConvertFrom-Yaml $yaml).anArray.ForEach({ $_ })
@(1,2,3).ForEach({ $_ })
1
2
3
This is unrelated to ConvertFrom-Yaml
and related to List<T>
, when you do .ForEach
on a list you're actually calling its .ForEach(Action<T>)
Method instead of the intrinsic PowerShell .ForEach
Method and Action<T>
is a void
delegate, thus you see no output from it but if you added console output to your delegate you'd see it is actually enumerating:
(ConvertFrom-Yaml $yaml).anArray.ForEach({param($s) [System.Console]::WriteLine($s) })
So the suggestion in this case is to not use .ForEach
(use ForEach-Object
or foreach
) or convert the List<T>
to an Array
:
(ConvertFrom-Yaml $yaml).anArray.ToArray().ForEach({ $_ })
Addressing the questions in comments:
So does List overwrite the intrinsic PowerShell
ForEach
? Why does the intrinsic version exist on arrays and collections, when it doesn’t exist on lists?
Both exists, just that the list's instance method takes priority over the intrinsic one.
In your code, you supply a lambda function? Why am I able to supply a script block?
No, we're supplying a scriptblock, it happens to be that a scriptblock can be coerced to be an Action
delegate without problems.
$delegate = [System.Action[int, int]] { param($x, $y) $x + $y | Write-Host }
$delegate.Invoke(1, 1) # 2
Coercion of a scriptblock to a delegate isn't applicable only to Action
, it can work with any delegate, try this example if you're running PowerShell 7+:
Add-Type '
using System.Collections.Generic;
using System.Management.Automation;
public static class Test
{
public delegate object MyCustomDelegate(object x);
public static IEnumerable<object> ForEach2(
MyCustomDelegate myDelegate,
object x)
{
if (!LanguagePrimitives.IsObjectEnumerable(x))
{
yield return myDelegate(x);
yield break;
}
foreach (object i in LanguagePrimitives.GetEnumerable(x))
{
yield return myDelegate(i);
}
}
}'
Update-TypeData -TypeName System.Object -Value {
param([Test+MyCustomDelegate] $delegate)
[Test]::ForEach2($delegate, $this)
} -MemberName ForEach2 -MemberType ScriptMethod
'hello'.ForEach2({param($i) $i + ' world!' })
(0..10).ForEach2({param($i) "[$i]" })
Is there any way to call the intrinsic version on lists?
I don't think it's possible. Unfortunately the logic used for the .Where
and .ForEach
methods is hard to find in the PowerShell source to give a deeper explanation as to why they have less priority.