如何在powershell中使用linq explict或类似于SQL中的“NOT IN”

mcdcgff0  于 12个月前  发布在  Shell
关注(0)|答案(2)|浏览(169)

我有一个关于在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"的其他快速模拟。我希望得到帮助。谢谢。

64jmpszr

64jmpszr1#

为了使(通用的)set-difference .Except() LINQ method工作,作为参数传递的两个枚举(IEnumerable<T>)必须:

  • 枚举相同类型的示例T
  • 但是,可以将Object用于T,从而有效地支持PowerShell的 * 潜在混合类型 * 常规Object[]数组(PowerShell的类型文字表示法中的[object[]])。
  • 并且,如果该类型是 * 引用类型 *,其示例应该基于示例的 * 内容 * 进行有意义的比较(而不是仅仅通过 * 引用相等 *,即标识),则必须实现IEquatable<T>接口和/或覆盖.Equals()方法(因此也覆盖.GetHashCode()方法)。

PowerShell似乎无法为.Except()找到正确的重载,[object[]]数组由$Table1.FindName1$Table2.FindName1返回(也请参阅下面的注解re v7.3+),尽管这些数组在技术上满足上述要求-我不知道为什么。
然而,简单地将其中一个数组强制转换为它已经是的[object[]]-解决了这个问题:

[Linq.Enumerable]::Except([object[]] $Table1.FindName1, $Table2.FindName1)

字符串
注意事项:

  • 如上所示,为了让PowerShell推断出方法的正确泛型类型参数,将枚举中的 one 强制转换为[object[]]就足够了。
  • PowerShell (Core)7.3+中,现在可以调用带有 explicit 类型参数的泛型方法(参见about_Calling_Generic_Methods),这允许简化解决方案:
# Note the '[object]' right after 'Except',
# specifying the generic type argument.
[Linq.Enumerable]::Except[object]($Table1.FindName1, $Table2.FindName1)

  • 假设.FindName1列最终包含 strings,您还可以将- both - enumerables转换为[string[]],尽管这隐式地创建了每个数组的 * 副本 *,但在这里没有必要。

现在,如果你想**返回 * 整行 *,而只使用.FindName1列进行 * 比较 *,事情会变得复杂得多:

  • 必须实现一个自定义比较器类,实现IEqualityComparer[T]接口。
  • 您必须将数据表的.Rows集合强制转换为IEnumerable[DataRow],这需要通过反射调用System.Linq.Enumerable.Cast()方法 *(同样,请参阅下面更简单的v7.3+解决方案)。
  • 注意:虽然可以直接强制转换为[DataRow[]],但这会导致将行集合转换为数组的效率低下。

下面是一个PSv 5+解决方案,它将自定义比较器类实现为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()
)

PowerShell 7.3+简化

直接指定泛型方法类型参数的能力使基于反射的方法变得不必要,并简化了方法:

[Linq.Enumerable]::Except[Data.DataRow](
  [Linq.Enumerable]::Cast[Data.DataRow]($Table1.Rows), 
  [Linq.Enumerable]::Cast[Data.DataRow]($Table2.Rows), 
  [CustomTableComparer]::new()
)


注意:仍然需要[Linq.Enumerable]::Cast[Data.DataRow]()调用,因为System.Data.DataTable示例的.Rows属性仅实现IEnumerable,而不是IEnumerable[System.Data.DataRow]
GitHub issue #2226建议让LINQ成为一级PowerShell公民。

zzlelutf

zzlelutf2#

要使用本机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[]]-从rows集合创建一个新数组-似乎需要Compare-Object将行识别为可重复的。
  • 调用.GetEnumerator()或转换为Collections.IEnumerable没有帮助,转换为Collections.Generic.IEnumerable[Data.DataRow]]失败。
  • -Property FindName1指定比较属性,即用来比较行的属性。
  • -PassThru需要使Compare-Object按原样输出输入对象,而不是只包含-Property指定的属性的自定义对象。
  • 请注意,这些对象是用.SideIndicator NoteProperty成员装饰的,但是,使用PowerShell的ETS(扩展类型系统)-请参阅下文。
  • 假设Compare-Object输出的输入对象对于 either 集合是唯一的,则Where-Object SideIndicator -eq '<='必须用于将结果限制为对于LHS输入集合是唯一的那些差异对象(通过'<='.SideIndicator属性值表示-箭头指向对象唯一的那一侧)。

GitHub issue #4316提出了对Compare-Object小程序的许多改进,这有助于简化和加速上述解决方案。
也就是说,make LINQ a first-class PowerShell citizen, #2226的提议有更多的希望。

相关问题