从一个功能出发,聊聊PowerShell的trap和trick

以前提到“取多行文本的某列文字再连成一行”这样一个功能,用PowerShell实现虽然不算什么难事,但其中的坑还请多加注意。另外其实这也有奇技淫巧的用武之地,值不值得就看情况了。

缘起

参见PowerShell代码备忘录里的“取多行文本的某列文字再连成一行”。抱歉,又说了一次w

环境

哎,OS是Windows 10 20H2,$PSCulture$PSUICulture均为ja-JP,提到PowerShell 5的时候用的是版本5.1,而pwsh 7的话则是7.1

参考

正文

前提

<#
过去存的一个巨大的输入法码表。多达28127行,内容大概是中古汉语拼音什么的,不是很懂。
#>
$path = 'D:\ime\zyenpheng.dict.yaml'

顺带一提,虽然现在说有点早,没在用char[]或者CharEnumerator的原因是,这边真的想要得到“字”——或者说grapheme——而不是能用16位char就能搞定的。

现在用[Globalization.StringInfo]::GetTextElementEnumerator()就OK。如果需求比较特别,.EnumerateRunes()也可以用。虽然其实这里好像真的用不上w

执行时间测试

把代码段打成scriptblock……就是用{}包起来,用Measure-Command调一下就行。

比如说,

Measure-Command {
  (1..1KB).ForEach{ $_ }
}

会返回一个TimeSpan,挺容易读的所以不用多说也行。

利用array的方法

(Get-Content $path).ForEach{
  [System.Globalization.StringInfo]::GetTextElementEnumerator(
    ($_, ' ')[!$_] # (1)
  ).Where(
    { $true },
    'First'
  )
} -join ''

这个,PowerShell 5也可以用。如果在用pwsh 7及以上的话,(1)那边可以用[string]::IsNullOrEmpty($_) ? ' ' : $_这样的三目运算符表达式来替换……看起来更严谨了是吧,其实在这里没必要,看情况用吧w

再提一嘴,(1)这个可以说是pwsh 7以下(不含),三目运算符(ternary operator)表达式的一个代用办法(PowerShellTraps/Basic/Missing-ternary-operator),莫名很爽,建议用哦~

这边,执行时间的话,PowerShell 5和pwsh 7两边都试了,大概在500~600ms这样。似乎pwsh 7更快一些。

这个说实在的有点厉害。

多说点儿

pwsh的array自带的这两个方法:ForEach()Where(),不仅用途广泛,而且效率着实可以。如果只要传一个scriptblock参数,完全可以不用写小括号,直接接上大括号就好——不过左大括号必须紧贴方法名,或许看着没那么好看吧。

不过,稍等,Get-Content $path返回的是一个array,这没有问题;但[System.Globalization.StringInfo]::GetTextElementEnumerator()却返回一个TextElementEnumerator——它实现了IEnumerator哦,是一个enumerator。既然它可以使用Where(),恐怕是作成了一个类似array的东西。那,它是不是提前求出来了结果呢?还没调查到这一步,先用着就是。

另外,这两个方法都返回一个Collection,尽管一般来说没什么困扰,但类型确实不同,注意一下为妙。

List<T>这样的对象也可以用这两个方法,但我只好说,并不好用。为了减轻心智负担,对它们直接用Linq处理会好些。

差点忘了,有Clear()方法可以用。不过这是后话。

利用管道

参见PowerShellTraps/Cmdlets/ForEach-Object以及PowerShellTraps/Cmdlets/Where-Object。其中提到了ForEach-ObjectWhere-Object的性能问题,并提供了解决方案:.{ process { ... } }之类。但是吧,这也是要利用管道的,所以不要太指望性能多好就是。

{
  param (
    $Source,
    $Target
  )
  $idx = 0
  $Source |
    . {
      process {
        $idx = 0
        [System.Globalization.StringInfo]::GetTextElementEnumerator(
          ($_, ' ')[!$_]
        )
      }
    } |
    . {
      process {
        if ($idx -eq $Target) {
          $_
        }
        $idx++
      }
    }
}.Invoke((Get-Content $path), 0) -join ''

说实在的,我跑通的时候已经傻了。

执行时间,这边大概在800~900ms,依旧是pwsh 7稍微快一些。

使用ForEach-ObjectWhere-Object的办法我就不测了哦w

多说点儿

用了临时的变量用来计数,算不上是很好的办法,只是能用了而已。

. { process {} },这个结构,.是dot source,process {}造了一个能接受管道输入的代码块。

考虑到[System.Globalization.StringInfo]::GetTextElementEnumerator()返回的是一个enumerator,直接拿它操作也不错,或许之后会尝试。

呃,对了,话说为什么非要自己得出TextElementEnumerator不可呢?——因为当时没看到其他的方法。

直接取值

(Get-Content $path).ForEach{
  [System.Globalization.StringInfo]::GetNextTextElement(($_, ' ')[!$_])
} -join ''

……为什么没早点儿发现这个呢w

执行时间,PowerShell 5和pwsh 7都在400ms上下,pwsh 7看似稳定一些。相当不错哦。

好耶,不用劳烦了ww

后记

pwsh是真的weird嚯w

总之注意躲坑哟。

InSb

InSb

只是跟工作和生活相关的记录