Powershell: The code that creates delegates for scriptblocks can't handle certain return types.

Created on 28 Aug 2018  路  5Comments  路  Source: PowerShell/PowerShell

In ScriptBlock.InvokeAsDelegateHelper() we pass the result of the execution through GetRawResult() which results in a scalar value if the return pipe contains one object. The type conversion on the return value can't deal with the scalar in some case resulting in a run time error. Making this work is important for methods like [Linq.Enumerable]::SelectMany() which takes a delegate [System.Func[TSource,int,System.Collections.Generic.IEnumerable[TResult]]]

Steps to reproduce

([func[system.collections.generic.ienumerable[object]]] { 1 }).Invoke()

Expected behavior

1

Actual behavior

Exception calling "Invoke" with "0" argument(s): "Cannot convert the "1" value of type "System.Int32" to type "System.Collections.Generic.IEnumerable`1[System.Object]"."
At line:1 char:1
+ ([func[system.collections.generic.ienumerable[object]]] { 1 }).Invoke ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : PSInvalidCastException

Environment data

PSCore (1:102) >  $PSVersionTable

Name                           Value
----                           -----
PSVersion                      6.1.0-preview.2
PSEdition                      Core
GitCommitId                    v6.1.0-preview.2
OS                             Microsoft Windows 10.0.17134
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Issue-Bug WG-Engine

Most helpful comment

@IISResetMe it isn't surprising behavior, but I'd argue that it's not desired behavior.

The example given only works because the default array return type object[] is already IEnumerable<object> so it doesn't need to do anything extra.

For example:

# Fails
([func[system.collections.generic.ienumerable[int]]] { Write-Output @(1) -NoEnumerate }).Invoke()

# Works
([func[int[]]] { Write-Output @(1) -NoEnumerate }).Invoke()

# Fails
([func[System.Collections.Generic.IEnumerable[object]]] { ,[int[]](1) }).Invoke()

There's a few interfaces that are often used as return types in abstract classes or delegates. Some extra conversion paths would be a nice bit of quality of life.

All 5 comments

I think part of the problem is there's no conversion path for T to IEnumerable<T>. I've had similar issues inheriting classes with abstract methods that return an IEnumerable<>.

A shorter repro is that this doesn't work:

[System.Collections.Generic.IEnumerable[object]]1

But this does

[System.Collections.Generic.IEnumerable[object]][object[]]1

In classes the workaround is to force the return value to something already inheriting IEnumerable<> like above, but that doesn't seem to work here.

@SeeminglyScience In this specific case, the code returns a collection, checks to see if it's length 1 then turns it into a scalar. If the cast worked, we'd then take that scalar and turn it back into a collection of 1 element which is not desirable. More generally, you can't cast a scalar to IEnumerable[T] because converting from a scalar to a collection requires creating an instance of the collection and you can't create an instance of IEnumerable[T], only instances of concrete types that implement IEnumerable[T}. Now we could add a special case (hack but one of many) in the type converter logic that picked a specific concrete type (probably array of T) to do the conversion but I'm not sure it's desirable. Thoughts?

@BrucePay I think the pattern is common enough that it makes sense. Maybe even the other collection-like interfaces that T[] would implement like IList<T>, ICollection<T>, etc.

I don't think it would be desirable to always force the result to be an array (you wouldn't want to cast a LINQ method result unnecessarily) but if the target object is not already of the specified cast type, then forcing to T[] makes a lot of sense to me.

Isn't this... expected behavior?

([func[system.collections.generic.ienumerable[object]]] { Write-Output @(1) -NoEnumerate }).Invoke() works fine

@IISResetMe it isn't surprising behavior, but I'd argue that it's not desired behavior.

The example given only works because the default array return type object[] is already IEnumerable<object> so it doesn't need to do anything extra.

For example:

# Fails
([func[system.collections.generic.ienumerable[int]]] { Write-Output @(1) -NoEnumerate }).Invoke()

# Works
([func[int[]]] { Write-Output @(1) -NoEnumerate }).Invoke()

# Fails
([func[System.Collections.Generic.IEnumerable[object]]] { ,[int[]](1) }).Invoke()

There's a few interfaces that are often used as return types in abstract classes or delegates. Some extra conversion paths would be a nice bit of quality of life.

Was this page helpful?
0 / 5 - 0 ratings