能否在PowerShell中使用LINQ?

76

我想在PowerShell中使用LINQ。由于PowerShell基于.NET Framework构建,因此理论上完全可行,但我无法让它正常运行。例如,当我尝试以下(人为制造的)代码时:

$data = 0..10

[System.Linq.Enumerable]::Where($data, { param($x) $x -gt 5 })

我遇到了以下错误:

找不到“Where”的重载方法,参数数量为“2”。

请注意,虽然可以使用 Where-Object 完成此操作,但这个问题的要点不是要在 PowerShell 中找到一种惯用的方法来完成此操作。如果能够使用 LINQ,有些任务将会更加容易实现。


这个问题的最高投票答案会建议你不能在PowerShell中使用LINQ运算符。https://dev59.com/YHE85IYBdhLWcg3wPBF8 - DeanOC
我觉得你说“不要紧”来表示对Where-Object的态度,然后又谈论“惯用”的方式,这有些奇怪——Where-Object PowerShell 的事实上的惯用方式。如果你真的想要 LINQ(而不是类似 LINQ 的 cmdlet),我建议你研究二进制 cmdlet;这样你就可以将集合传递给它,并在本应使用 LINQ 的地方使用它(C# 或 VB.NET)。 - carrvo
4个回答

91

你的代码问题在于PowerShell无法确定ScriptBlock实例({ ... })应转换为哪种特定的委托类型。因此,它无法为Where方法的通用第二个参数选择一个类型具体的委托实例。同时,也没有语法明确指定通用参数。要解决这个问题,你需要自己将ScriptBlock实例转换为正确的委托类型:

$data = 0..10
[System.Linq.Enumerable]::Where($data, [Func[object,bool]]{ param($x) $x -gt 5 })
为什么[Func[object, bool]]可以正常工作,但[Func[int, bool]]不能?因为你的$data[object[]],而不是[int[]]。 PowerShell默认创建[object[]]数组;但是,你可以显式构造[int[]]实例:
$intdata = [int[]]$data
[System.Linq.Enumerable]::Where($intdata, [Func[int,bool]]{ param($x) $x -gt 5 })

54
为了补充PetSerAl的有帮助的回答,以适应问题的通用标题,以下是更广泛的回答:
注:以下内容适用至少到PowerShell 7.3.x版本。关于直接支持LINQ的讨论,其语法与C#中的语法相似,将在未来的PowerShell Core版本中进行,详见GitHub问题#2226。
在PowerShell中使用LINQ:
你需要 PowerShell v3 或更高版本。 你不能直接在集合实例上调用 LINQ 扩展方法,而是必须将 LINQ 方法作为 [System.Linq.Enumerable] 类型的静态方法调用,将输入集合作为第一个参数传递给它。 这样做会削弱 LINQ API 的流畅性,因为方法链不再可行。相反,你必须以相反的顺序嵌套静态调用。 例如,你必须写成 [Linq.Enumerable]::OrderBy([Linq.Enumerable]::Where($inputCollection, ...), ...),而不是 $inputCollection.Where(...).OrderBy(...)
辅助函数和类: 一些方法(如 .Select())具有接受泛型 Func<> 委托的参数。可以使用 PowerShell 代码通过将脚本块应用于转换来创建 Func<T,TResult>,例如:[Func[object, bool]] { $Args[0].ToString() -eq 'foo' }Func<> 委托的第一个泛型类型参数必须与输入集合的元素类型匹配;请记住,PowerShell 默认创建 [object[]] 数组。
一些方法(如 .Contains().OrderBy())具有接受实现特定接口的对象的参数,例如 IEqualityComparer<T>IComparer<T>;此外,输入类型可能需要实现 IEquatable<T> 以使比较按预期工作,例如使用 .Distinct();所有这些都需要编写编译类,通常是使用 C#(尽管你可以通过将包含嵌入 C# 代码的字符串传递给 Add-Type cmdlet 来从 PowerShell 中创建它们);然而,在 PSv5+ 中,你也可以使用自定义 PowerShell,但有一些限制。
通用方法: 一些 LINQ 方法本身是泛型的,因此需要一个或多个类型参数。 在 PowerShell (Core) 7.2- 和 Windows PowerShell 中,PowerShell 不能直接调用这些方法,而必须使用反射,因为它只支持推断类型参数,而在这种情况下无法完成;例如: # 获取 OfType<T> 的 [string] 实例化方法。 $ofTypeString = [Linq.Enumerable].GetMethod("OfType").MakeGenericMethod([string])
# 仅输出集合中的 [string] 元素。 # 注意数组必须嵌套以识别方法签名。 PS> $ofTypeString.Invoke($null, (, ('abc', 12, 'def'))) abc def
在 PowerShell (Core) 7.3+ 中,你现在可以选择显式指定类型参数(请参阅关于_Calling_Generic_Methods_帮助主题);例如: # 仅输出集合中的 [string] 元素。 # 注意需要将输入数组括在 (...) # -> 'abc', 'def' [Linq.Enumerable]::OfType[string](('abc', 12, 'def'))
LINQ 方法返回的是一个惰性可枚举项,而不是实际的集合;也就是说,返回的不是实际的数据,而是在枚举时产生数据的东西。 在自动执行枚举的上下文中,特别是在管道中,你可以像使用集合一样使用可枚举项。 然而,由于可枚举项本身不是集合,因此无法通过调用 .Count 来获取结果计数,也无法对迭代器进行索引;但是,你可以使用 成员访问枚举(提取正在枚举的对象的属性值)。 如果确实需要将结果作为静态数组获取以获得通常的集合行为,请将调用包装在 [Linq.Enumerable]::ToArray(...) 中。 还存在返回不同数据结构的类似方法,例如 ::ToList()

要查看一个高级示例,请参见此答案
要了解所有LINQ方法的概述和示例,请参见这篇精彩的文章


简而言之:在PowerShell中使用LINQ很麻烦,只有在以下情况下才值得付出努力:
  • 您需要PowerShell的cmdlet无法提供的高级查询功能
  • 性能至关重要-请参阅本文

1
对于像Contains和Distinct这样的方法,如果你想忽略字符串的大小写,你可以使用[StringComparer]::InvariantCultureIgnoreCase(PowerShell操作符如-eq使用不变的文化)和[StringComparer]::OrdinalIgnoreCase - 这些不应与[StringComparison]类中的枚举混淆,那些不起作用。我花了一些时间才弄清楚这一点,希望能帮到别人。 - undefined
1
谢谢,@Vopel - 我已经将这个信息添加到答案中。 - undefined
@mkelement0 这么快!顺便说一下,我正在尝试定义一个自定义的PowerShell类,它与[StringComparer]::InvariantCultureIgnoreCase执行相同的操作,以建立一个更高级、高性能的比较函数,需要不区分大小写。像SequenceEqual这样的方法可以正常工作,但是尽管我可以让Distinct方法接受我的自定义类作为参数,但无论我尝试什么,它仍然不会忽略大小写。我一直在尝试按照你提到的方法实现IEquatable<T>。在5.1中,如何做到这一点而不使用C#代码呢? - undefined
@Vopel:这应该可以解决问题;也许你忘记重写.GetHashCode()方法,以确保对于任何比较相等的对象对返回相同的哈希码?如果这没有帮助,我建议你提出一个新的问题;完成后随时在这里提醒我。 - undefined

25

如果您想要实现类似于LINQ的功能,PowerShell有一些命令和函数可供使用,例如:Select-ObjectWhere-ObjectSort-ObjectGroup-Object。它具有大多数LINQ特性的命令,如投影、限制、排序、分组、分区等。

请参阅Powershell One-Liners: Collections and LINQ了解更多信息。

如果您需要更多关于使用LINQ以及如何使其更加容易的详细信息,可以参考文章LINQ Through Powershell


2
我在 PowerShell 中想要进行稳定排序(即:如果要排序的属性在两个或多个元素上具有相同的值,则保留它们的顺序),于是我接触到了 LINQ。虽然 Sort-Object 有一个 -Stable 开关,但仅适用于 PS 6.1+。此外,在 .NET 的通用集合中,Sort() 实现并不稳定。因此,我找到了 LINQ,其中文档称其为稳定的。
以下是我的(测试)代码:
# Getting a stable sort in PowerShell, using LINQs OrderBy

# Testdata
# Generate List to Order and insert Data there. o will be sequential Number (original Index), i will be Property to sort for (with duplicates)
$list = [System.Collections.Generic.List[object]]::new()
foreach($i in 1..10000){
    $list.Add([PSCustomObject]@{o=$i;i=$i % 50})
}

# Sort Data
# Order Object by using LINQ. Note that OrderBy does not sort. It's using Delayed Evaluation, so it will sort only when GetEnumerator is called.
$propertyToSortBy = "i" # if wanting to sort by another property, set its name here
$scriptBlock = [Scriptblock]::Create("param(`$x) `$x.$propertyToSortBy")
$resInter = [System.Linq.Enumerable]::OrderBy($list, [Func[object,object]]$scriptBlock )
# $resInter.GetEnumerator() | Out-Null

# $resInter is of Type System.Linq.OrderedEnumerable<...>. We'll copy results to a new Generic List
$res = [System.Collections.Generic.List[object]]::new()
foreach($elem in $resInter.GetEnumerator()){
    $res.Add($elem)
}

# Validation
# Check Results. If PropertyToSort is the same as in previous record, but previous sequence-number is higher, than the Sort has not been stable
$propertyToSortBy = "i" ; $originalOrderProp = "o"
for($i = 1; $i -lt $res.Count ; $i++){
    if(($res[$i-1].$propertyToSortBy -eq $res[$i].$propertyToSortBy) -and ($res[$i-1].$originalOrderProp -gt $res[$i].$originalOrderProp)){
        Write-host "Error on line $i - Sort is not Stable! $($res[$i]), Previous: $($res[$i-1])"
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接