处理 StreamWriter 而无需在一行中声明变量

Dispose of a StreamWriter without declaring a variable in one line

以下Powershell命令无法复制整个文件;结尾总是少了几个字符。

[System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8).Write([System.IO.StreamReader]::new('C:\Temp\a.csv', [System.Text.Encoding]::GetEncoding('iso-8859-1')).ReadToEnd())

我怀疑是因为作者没有刷新最后一位,因为这确实复制了整个文件:

$X = [System.IO.StreamReader]::new('C:\Temp\a.csv', [System.Text.Encoding]::GetEncoding('iso-8859-1'))
$Y = [System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)
$Y.Write($X.ReadAll())
$X.Dispose()
$Y.Dispose()

是否可以在没有创建变量来引用它们的情况下处理(和刷新)reader & writer?

编辑: 我使用 streamreader/writer 尝试了这个单行,希望 reader 的读取缓冲区将直接传输到编写器的写入缓冲区,而不是等待 reader 将整个文件读入内存然后写。什么技术可以实现这一目标?

我个人发现不声明一次性对象的代码通常更清晰/更简洁,但我的重点是理解 whether/how 对象自行处理,而不是样式。

没有需要避开变量或写在一行上,但这种行为不是我所期望的。在 VBA 中,可以像这样复制文件并相信它会正确处理自身,而无需声明变量并显式刷新(我认为)。

Sub Cpy()
With New Scripting.FileSystemObject
    .CreateTextFile("c:\Temp\Out.txt").Write .OpenTextFile("C:\Temp\In.txt", ForReading).ReadAll
End With
End Sub

通过在 Class_Terminate() 过程中编写适当的 'clean-up' 代码,可以在自定义 VBA class 中实现类似的行为。我假设一旦该行执行并且不再有与之关联的变量,Streamwriter 会在终止时通过垃圾收集类似地刷新数据。

我还注意到该文件仍处于锁定状态,在关闭 powershell 会话之前我无法将其删除。有没有办法在没有声明要使用的变量的情况下刷新内容并释放文件?

只是为了向您展示使用 System.IO.File, WriteAllText() and ReadAllText().

的静态方法这是可能的,而且更容易做到

以下查询 https://loripsum.net/ API 以获取随机段落并使用 iso-8859-1 编码写入文件。然后读取该文件并使用相同的编码写入副本,最后比较两个文件的哈希值。如您所见,阅读和写作都是一行完成的。

可以删除 using 语句,但您需要使用完整类型名称。

将位置设置为用于测试的临时文件夹。

using namespace System.IO
using namespace System.Text

$fileRead = [Path]::Combine($pwd.Path, 'test.txt')
$fileWrite = [Path]::Combine($pwd.Path, 'test-copy.txt')

$loremIpsum = Invoke-RestMethod 'https://loripsum.net/api/5/short/headers/plaintext'
[File]::WriteAllText($fileWrite, $loremIpsum, [Encoding]::GetEncoding('iso-8859-1'))

[File]::WriteAllText(
    $fileWrite,
    [File]::ReadAllText($fileRead, [Encoding]::GetEncoding('iso-8859-1')),
    [Encoding]::GetEncoding('iso-8859-1')
)

(Get-FileHash $fileRead).Hash -eq
(Get-FileHash $fileWrite).Hash # => Should be True
  • 对于给定的特定用例 is indeed the best solution: using the static methods of the static System.IO.Fileclass避免了对实例 表示需要调用 .Close() 方法或显式处理的文件。

    • 读取lazily因此支持重叠读写,逐行,您可以使用静态 [System.IO.File]::ReadLines() and [System.IO.File]::WriteAllLines() 方法,但请注意,这种方法 (a) 总是在输出文件中使用平台原生 [Environment]::NewLine 格式的换行符,而不管输入的换行符格式如何文件使用,并且 (b) 总是以这种格式添加一个 尾随换行符,即使输入文件没有尾随换行符。

    • 克服这些限制将需要使用较低级别的原始字节 API、System.IO.FileStream - 这又需要明确处理(见底部)。

  • 鉴于您的方法首先将整个输入文件读入内存,然后然后写入,您甚至可以使用 PowerShell cmdlet,假设您是 运行 PowerShell (Core) 7+,默认写入无BOM的UTF-8文件,其-Encoding参数接受任何支持的编码,例如ISO- 8859-1 在你的情况下:

    # PowerShell (Core) 7+ only
    Get-Content -Raw -Encoding iso-8859-1 C:\TEMP\a.csv |
      Set-Content -NoNewLine C:\TEMP\b.csv                            
    

至于你的一般问题

从 PowerShell(核心)7.2.1 开始:

  • PowerShell 有 no 构造等效于 C# 的 using 语句 允许 自动 处理类型实现 System.IDisposable 接口的对象(在文件 I/O APIs 的情况下,隐式关闭文件)。

    • GitHub issue #9886 讨论了添加这样的声明,但讨论表明它可能不会被实施。

    • 注意:虽然 PowerShell 确实有一系列以关键字 using 开头的语句,但它们有不同的用途 - 请参阅概念性 about_Using 帮助主题。

  • A 未来 PowerShell版本将支持clean { ... }(或cleanup { ... })块advanced function or script terminates, which allows performing any necessary function-script-level cleanup (disposing of objects) - see RFC #294.

    时自动调用

是否从终结器 调用.Dispose() 方法 取决于实现IDisposable 接口的每个类型。只有这样,一个对象才会自动被垃圾收集器最终处理掉

对于System.IO.StreamWriter and also the lower-level System.IO.FileStream class, this appears not to be the case, so in PowerShell you must call .Close() or .Dispose() explicitly, which is best done from the finally block of a try / catch / finally statement

你可以通过结合对象构造和变量赋值方面来减少一些仪式,但是一个健壮的习语仍然需要很多仪式:

$x = $y = $null
try {
  ($y = [System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)).
    Write(
      ($x = [System.IO.StreamReader]::new('C:\Temp\a.csv', [System.Text.Encoding]::GetEncoding('iso-8859-1'))).
        ReadToEnd()
    )
} finally {
  if ($x) { $x.Dispose() }
  if ($y) { $y.Dispose() }
}

辅助函数,Use-Object(下面的源代码)可以稍微缓解这个问题:

Use-Object 
  ($x = [System.IO.StreamReader]::new('C:\Temp\a.csv',[System.Text.Encoding]::GetEncoding('iso-8859-1'))), 
  ($y = [System.IO.StreamWriter]::new('C:\TEMP\b.csv', [System.Text.Encoding]::UTF8)) `
  { $y.Write($x.ReadToEnd()) }

Use-Object源代码:

function Use-Object {
  param( 
    [Parameter(Mandatory)] [array] $ObjectsToDispose,
    [Parameter(Mandatory)] [scriptblock] $ScriptBlock
  )

  try {

    . $ScriptBlock

  } finally {
    foreach ($o in $ObjectsToDispose) {
      if ($o -is [System.IDisposable]) {
        $o.Dispose()
      }
    }
  }
  
}