powershell 找不到“ToLookup”和参数计数的重载:“2”LINQ

33qvvth1  于 2022-11-29  发布在  Shell
关注(0)|答案(2)|浏览(151)

我有一个PowerShell脚本,每小时运行一次,它基本上通过drop进程更新产品信息。该脚本在dev环境中失败,但在prod和qual环境中继续工作。失败点在使用LINQ的一行。以下是该脚本:

$productInfos = [pscustomobject]@{
    product_name = 'Television'
    price = 1000
    product_id = 101
    } | ForEach-Object{$_}

$productIdDelegate = [Func[object,int]] {$args[0].product_id}
$productInfos = [Linq.Enumerable]::ToLookup($productInfos, $productIdDelegate)

从技术上讲,它在最后一行失败。但是倒数第二行被包括在内,因为在研究这个问题的时候,我发现了很多其他的帖子,指出如果ToLookup linq函数中的两个参数不是相同的类型,它可能会导致这个问题。倒数第二行是许多用户的“假定”修复,但遗憾的是不是我自己。我仍然收到
找不到“ToLookup”和参数计数的重载:“二”
我检查了所有3个环境的任何依赖关系,一切都是一样的。而且,powershell版本在所有环境上也是一样的。

kmbjn2e3

kmbjn2e31#

尽管这个错误没有给出太多细节,但是从当前可重现的代码中,我们可以看出这个问题是因为$productInfosPSObject类型的单个对象,而这个类没有实现IEnumerable Interface

$productInfos = [pscustomobject]@{
    product_name = 'Television'
    price = 1000
    product_id = 101
}

$productInfos -is [System.Collections.IEnumerable] # => False

要确保$productInfos始终是 * enumberable *,可以使用Array子表达式运算符@( )

@($productInfos) -is [System.Collections.IEnumerable] # => True

铸造[object[]][array]也应该是一个选项:

[object[]] $productInfos -is [System.Collections.IEnumerable] # => True
[array] $productInfos -is [System.Collections.IEnumerable]    # => True

但是,上述方法仅在$productInfos至少有1个元素时才有效,而@(..)始终确保数组:
结果永远是0或多个对象的数组。
总结一下:

$productInfos = [Linq.Enumerable]::ToLookup([object[]] $productInfos, $productIdDelegate)
$productInfos = [Linq.Enumerable]::ToLookup(@($productInfos), $productIdDelegate)
$productInfos = [Linq.Enumerable]::ToLookup([array] $productInfos, $productIdDelegate)

是否所有有效选项都考虑了上面所解释的内容,如果我们不确定变量是否总是被填充,那么最安全的选择总是@(...)

# Returns Null instead of an error:

[Linq.Enumerable]::ToLookup(@(), $productIdDelegate)
zlwx9yxi

zlwx9yxi2#

前面有一个注意事项:
... | ForEach-Object{ $_ },带有一个 single 输入对象,就像您的例子一样,不会输出一个 array(除非输入对象是一个数组 * 作为一个整体 *,这是不典型的),即使输入是一个单元素数组,因为管道的枚举行为。事实上,管道 * 本身 * 从不在输出上创建数组,它 * 一个接一个地 * 流对象,并且只有当该流被 * 捕获 * 时,才必须创建数组--但仅针对 * 两个或更多 * 输出对象;有关背景信息,请参见this answer
Santiago Squarzon提供了关键的指针:通过将传递给[System.Linq.Enumerable]::ToLookup[TSource, TKey]()的第一个参数 Package 在[object[]]数组中,PowerShell可以找到此方法的正确重载和 * 类型参数 *:

$productInfos = [Linq.Enumerable]::ToLookup(
  @($productInfos),   # wrap $productInfos in an [object[]] array with @(...)
  $productIdDelegate
)

与所有LINQ方法一样,ToLookup

  • enumerables(不严格地说,是对象的 * 集合 *)进行操作,
  • 是一个 * 泛型 * 方法,这意味着它必须使用它所操作的类型(绑定到方法的类型 * 参数 * 的类型 * 参数 *)进行示例化。

PowerShell必须在调用中 * 推断 * 类型参数,因为您没有 * 显式 * 提供类型参数,而在C#中则需要这种方式(例如Array.Empty<int>()

  • 注意:在即将发布的PowerShell(核心)7.3版本之前,PowerShell甚至不 * 支持 * 显式指定类型参数-请参阅下一节。

您所针对的ToLookup多载的签章为(您可以执行[Linq.Enumerable]::ToLookup,也就是 * 不含() *,就可以看到这个签章,不过为了便于阅读,下列签章已经过简化和重新格式化):

# TSource and TKey are the generic type parameters.
static ... ToLookup[TSource, TKey](
  IEnumerable[TSource] source, 
  Func[TSource,TKey] keySelector
)

由于您传递给keySelector参数的参数$productIdDelegate[Func[object,int]]的示例,因此PowerShell可以推断类型参数TSource的参数是[object],而TKey的参数是[int]
由于TSource[object],因此传递给source参数的参数必须是IEnumerable[object]类型。
您的$productInfos值,由于是一个单一对象,* 不 * 实现IEnumerable[object]接口,但是[object[]]类型的 * 数组 * 实现了。
如果数组子表达式运算符@(...)总是返回[object[]]类型的数组,则使用它 Package $productInfos就足够了。
请注意,如果您使用 * 特定 * 类型来代替[object],例如[Func[datetime, int]]@(...)将 * 无法 * 工作:那么您将需要一个特定于类型的 cast,例如本例中的[datetime[]]
相反,在您的情况下,您可以使用[object[]] $productInfos(或其等效项[Array] $productInfos)作为@($productInfos)的替代项。
另请参阅:

  • This answer概述如何通过PowerShell使用LINQ,这有其局限性,因为即使在7.3中PowerShell也不支持 * 扩展方法 *(还没有?),而扩展方法是获得流畅体验所必需的。
  • 正在讨论为GitHub issue #2226中的PowerShell(核心)的 * 未来 * 版本添加对扩展方法的支持,以及可能的LINQ query syntx 等效项。
    PowerShell 7.3及更高版本的替代产品:

7.3及以上版本具有- optional -支持显式*指定类型参数,这是一个好消息,原因有二:

  • 新语法使给定的方法调用清楚地传达它所使用的类型参数。
  • 新语法使得直接调用泛型方法成为可能,其中类型参数不能被推断。
  • 例如,这适用于 * 无参数 * 的泛型方法,以前需要繁琐的解决方法;请参阅this post
  • 静态System.Array.Empty[T]方法就是一个简单的示例,现在可以将其称为[Array]::Empty[int]()

如果将此语法应用于调用,则可以省略将脚本块强制转换为[Func[object, int]]的操作,因为此强制转换是由显式类型参数 * 隐含 * 的;使用简单、独立的呼叫版本:

# PS v7.3+ only

# Note the explicit type arguments ([object, int])
# and the use of the script block without a [Funct[object, int]] cast.
$lookup = [Linq.Enumerable]::ToLookup[object, int](
  @([pscustomobject] @{ foo = 1 } , [pscustomobject] @{ foo = 2 }), 
  { $args[0].foo }
)

$lookup[2] # -> [pscustomobject] @{ foo = 2 }

inferred 类型参数的情况一样,如果第一个类型参数不是[object],比如[pscustomobject],则需要类型特定的数组转换(仅@(...)是不够的):

# PS v7.3+ only

# Note the [pscustomobject] type argument and the
# [pscustomobject[]] cast.
$lookup = [Linq.Enumerable]::ToLookup[pscustomobject, int](
  [pscustomobject[]] ([pscustomobject] @{ foo = 1 } , [pscustomobject] @{ foo = 2 }), 
  { $args[0].foo }
)

相关问题