我有一个关于在PowerShell中使用Linq的问题。我无法弄清楚如何正确使用Except
方法
示例表:
$Arr = 1..1000
$Props = ("employeeID","FindName1","FindName2")
$Table1 = New-Object System.Data.DataTable "Table1"
$Props | ForEach-Object { $Table1.Columns.Add( $_ , [String]) | Out-Null }
ForEach ($Record in $Arr ) {
$Row = $Table1.NewRow()
$Row.employeeID = $Record.ToString("00000")
$Row.FindName1 = "UserName_" + $Record.ToString()
$Row.FindName2 = "String_" + $Record.ToString("00000000")
$Table1.Rows.Add($Row)
}
$Arr2 = 980..1111
$Props = ("employeeID","FindName1")
$Table2 = New-Object System.Data.DataTable "Table2"
$Props | ForEach-Object { $Table2.Columns.Add( $_ , [String]) | Out-Null }
ForEach ($Record in $Arr2 ) {
$Row = $Table2.NewRow()
$Row.employeeID = $Record.ToString("00000")
$Row.FindName1 = "UserName_" + $Record.ToString()
$Table2.Rows.Add($Row)
}
作为工作的结果,我想从$table1
获取记录,其中FindName1不在$Table2.FindName1
中,保留所有标题
尝试执行不会产生预期结果。
$ExceptOut = [System.Linq.Enumerable]::Except($Table1.FindName1, $Table2.FindName1)
正如我从article所理解的那样,我需要使用允许我在表中使用LINQ的方法创建自己的类。但我离编程太远了。或许在SQL中还有其他一些"NOT IN"
的快速模拟。我希望能得到帮助。谢谢。
为了(通用).Except()
LINQ method工作,作为参数传递的两个枚举(IEnumerable<T>
)必须:
T
的实例IEquatable<T>
接口。PowerShell似乎无法使用.Except()
和[object[]]
返回的$Table1.FindName1
数组找到$Table2.FindName1
的正确重载,尽管这些数组技术上满足上述要求 - 我不知道为什么。
但是,简单地将这些数组转换为已经存在的数据 - [object[]]
- 解决了这个问题:
[Linq.Enumerable]::Except([object[]] $Table1.FindName1, [object[]] $Table2.FindName1)
鉴于.FindName1
列最终包含字符串,您也可以转换为[string[]]
,尽管在我的非正式测试中,这样做并没有再提供性能,至少在您的示例数据中。
现在,如果您只想在使用.FindName1
列进行比较时返回整行,则事情会变得复杂得多:
IEqualityComparer[T]
interface的自定义比较器类。.Rows
数据表集合强制转换为IEnumerable[DataRow]
,这需要通过反射调用System.Linq.Enumerable.Cast()方法。
注意:虽然您可以直接转换为[DataRow[]]
,但这会导致将行集合转换为数组效率低下。这是一个PSv5 +解决方案,它将自定义比较器类实现为PowerShell类:
# A custom comparer class that compares two DataRow instances by their
# .FindName1 column.
class CustomTableComparer : Collections.Generic.IEqualityComparer[Data.DataRow] {
[bool] Equals([Data.DataRow] $x, [Data.DataRow] $y) {
return [string]::Equals($x.FindName1, $y.FindName1, 'Ordinal')
}
[int] GetHashCode([Data.DataRow] $row) {
# Note: Any two rows for which Equals() returns $true must return the same
# hash code. Because *ordinal, case-sensitive* string comparison is
# used above, it's sufficient to simply call .GetHashCode() on
# the .FindName1 property value, but that would have to be tweaked
# for other types of string comparisons.
return $row.FindName1.GetHashCode();
}
}
# Use reflection to get a reference to a .Cast() method instantiation
# that casts to IEnumerable<DataRow>.
$toIEnumerable = [Linq.Enumerable].GetMethod('Cast').MakeGenericMethod([Data.DataRow])
# Call .Except() with the casts and the custom comparer.
# Note the need to wrap the .Rows value in an aux. single-element
# array - (, ...) - for it to be treated as a single argument.
[Linq.Enumerable]::Except(
$toIEnumerable.Invoke($null, (, $Table1.Rows)),
$toIEnumerable.Invoke($null, (, $Table2.Rows)),
[CustomTableComparer]::new()
)
This GitHub issue建议让LINQ成为一流的PowerShell公民。
使用本机PowerShell解决方案补充LINQ-based answer:
Compare-Object
cmdlet允许您比较集合,但请注意,虽然它更简洁,但它也比基于LINQ的解决方案慢得多:
Compare-Object -PassThru -Property FindName1 `
([Data.DataRow[]] $Table1.Rows) `
([Data.DataRow[]] $Table2.Rows) | Where-Object SideIndicator -eq '<='
[Data.DataRow[]]
- 从行集合创建一个新数组 - 似乎需要Compare-Object
将行识别为可枚举。
调用.GetEnumerator()
或者投掷到Collections.IEnumerable
没有帮助,并且投射到Collections.Generic.IEnumerable[Data.DataRow]]
失败了。-Property FindName1
指定比较属性,即用于比较行的属性。-PassThru
需要使Compare-Object
按原样输出输入对象,而不是仅包含用-Property
指定的属性的自定义对象。
请注意,对象使用.SideIndicator
NoteProperty成员进行修饰,但是,使用PowerShell的ETS(扩展类型系统) - 请参阅下文。Compare-Object
输出对任一集合都是唯一的输入对象,必须使用Where-Object SideIndicator -eq '<='
将结果限制为LHS输入集合唯一的差异对象(通过.SideIndicator
的'<='
属性值发出信号 - 箭头指向对象是唯一的)。This GitHub issue提出了对Compare-Object
cmdlet的一些改进,这有助于简化和加快上述解决方案。
也就是说,对make LINQ a first-class PowerShell citizen的提议更有希望。