"使用"函数

35

我定义了以下 'using' 函数:

def using[A, B <: {def close(): Unit}] (closeable: B) (f: B => A): A =
  try { f(closeable) } finally { closeable.close() }

我可以这样使用:

using(new PrintWriter("sample.txt")){ out =>
  out.println("hellow world!")
}

现在我很好奇如何定义一个“using”函数以接受任意数量的参数,并能够单独访问它们:

using(new BufferedReader(new FileReader("in.txt")), new PrintWriter("out.txt")){ (in, out) =>
  out.println(in.readLIne)
}

1
重复:https://dev59.com/tHE95IYBdhLWcg3wrP0o - retronym
1
try 语句块内执行 closeable.close(),否则可能会在 f(closeable) 中掩盖异常。 - Daniel C. Sobral
相关问题 https://dev59.com/22sz5IYBdhLWcg3w3r0J - Jus12
通常,我将其命名为“closing”,因为它不仅使用流,而且更重要的是关闭流,而真正的“using”保留给简单的def using[T](t: T)(f: T => Unit): T = {f(t) ; t},在你获取某个值并希望将其作为结果返回之前做一些事情的情况下非常有用,例如val v = obtain; printlnt(v) ; v。所以,你可以这样写,passThrough(obtain){println}。 - Valentin Tihomirov
9个回答

20

从Scala 2.13开始,标准库提供了一个专门的资源管理工具:Using

更具体地说,当处理多个资源时,可以使用Using#Manager

在我们的情况下,我们可以管理不同的资源,例如您的PrintWriterBufferedReader,因为它们都实现了AutoCloseable,以便从一个文件读取并写入另一个文件,无论如何,都会在之后关闭输入和输出资源:

import scala.util.Using
import java.io.{PrintWriter, BufferedReader, FileReader}

Using.Manager { use =>

  val in  = use(new BufferedReader(new FileReader("input.txt")))
  val out = use(new PrintWriter("output.txt"))

  out.println(in.readLine)
}
// scala.util.Try[Unit] = Success(())

14

已经有人做过这个了 - 它被称为Scala ARM

从自述文件中可以看到:

import resource._
for(input <- managed(new FileInputStream("test.txt")) {
  // Code that uses the input as a FileInputStream
}

6

我一直在思考这个问题,想到了另外一种解决方案。以下是我对支持“任意数量”参数的看法(受元组提供的限制):

object UsingTest {

  type Closeable = {def close():Unit }

  final class CloseAfter[A<:Product](val x: A) {
    def closeAfter[B](block: A=>B): B = {
      try {
        block(x);
      } finally {
        for (i <- 0 until x.productArity) {
          x.productElement(i) match { 
            case c:Closeable => println("closing " + c); c.close()
            case _ => 
          }
        }
      }
    }
  }

  implicit def any2CloseAfter[A<:Product](x: A): CloseAfter[A] = 
    new CloseAfter(x)

  def main(args:Array[String]): Unit = {
    import java.io._

    (new BufferedReader(new FileReader("in.txt")), 
     new PrintWriter("out.txt"),
     new PrintWriter("sample.txt")) closeAfter {case (in, out, other) => 
      out.println(in.readLine) 
      other.println("hello world!")
    }
  }
}

我认为我正在重复使用库中已经编写的22个元组/产品类的事实... 我不认为这种语法比使用嵌套的using(无意冒犯)更清晰,但这是一个有趣的难题。


1
这是一个不错的技巧,可惜你在元组内容的类型安全性方面做出了牺牲。如果你用case (in, out, other) =>替换x => val (in, out, other) = x,闭包会更加简洁。 - retronym
谢谢!我在想是否有比val赋值更好的方法。我会更新答案。 - huynhjl

3

使用结构类型似乎有点过头了,因为 java.lang.AutoCloseable 已经注定要被使用:

  def using[A <: AutoCloseable, B](resource: A)(block: A => B): B =
    try block(resource) finally resource.close()

或者,如果你更喜欢扩展方法:

  implicit class UsingExtension[A <: AutoCloseable](val resource: A) extends AnyVal {
    def using[B](block: A => B): B = try block(resource) finally resource.close()
  }

可以使用using2:

  def using2[R1 <: AutoCloseable, R2 <: AutoCloseable, B](resource1: R1, resource2: R2)(block: (R1, R2) => B): B =
    using(resource1) { _ =>
      using(resource2) { _ =>
        block(resource1, resource2)
      }
    }

但在我看来,这样做很丑陋 - 我更喜欢在客户端代码中直接嵌套这些使用语句。

标准库自原始发布以来已经发生了变化,但这对于现代Scala仍然相关,因此我会点赞。 - Ryan Leach

2

将清理算法从程序路径中分离出来是一个好主意。

这种解决方案允许您在作用域内累积可关闭的内容。
作用域清理将在块执行后发生,或者可以将作用域分离。然后稍后可以完成作用域的清理。

这样我们就可以获得相同的便利,而不受单线程编程的限制。

实用类:

import java.io.Closeable

object ManagedScope {
  val scope=new ThreadLocal[Scope]();
  def managedScope[T](inner: =>T):T={
    val previous=scope.get();
    val thisScope=new Scope();
    scope.set(thisScope);
    try{
      inner
    } finally {
      scope.set(previous);
      if(!thisScope.detatched) thisScope.close();
    }
  }

  def closeLater[T <: Closeable](what:T): T = {
    val theScope=scope.get();
    if(!(theScope eq null)){
      theScope.closeables=theScope.closeables.:+(what);
    }
    what;
  }

  def detatchScope(): Scope={
    val theScope=scope.get();
    if(theScope eq null) null;
    else {
      theScope.detatched=true;
      theScope;
    }
  }
}

class Scope{
  var detatched=false;
  var closeables:List[Closeable]=List();

  def close():Unit={
    for(c<-closeables){
      try{
        if(!(c eq null))c.close();
      } catch{
        case e:Throwable=>{};
      }
    }
  }
}

使用方法:
  def checkSocketConnect(host:String, portNumber:Int):Unit = managedScope {
    // The close later function tags the closeable to be closed later
    val socket = closeLater( new Socket(host, portNumber) );
    doWork(socket);
  }

  def checkFutureConnect(host:String, portNumber:Int):Unit = managedScope {
    // The close later function tags the closeable to be closed later
    val socket = closeLater( new Socket(host, portNumber) );
    val future:Future[Boolean]=doAsyncWork(socket);

    // Detatch the scope and use it in the future.
    val scope=detatchScope();
    future.onComplete(v=>scope.close());
  }

2

不幸的是,在标准的Scala中,不支持任意类型和任意长度参数列表。

通过一些语言的改变,也许你可以实现类似这样的功能(允许可变参数列表作为HLists传递;参见 这里 所需的大约1/3内容)。

现在,最好的方法就是像Tuple和Function一样,使用需要的N来进行实现。

当然,其中2个是足够简单的:

def using2[A, B <: {def close(): Unit}, C <: { def close(): Unit}](closeB: B, closeC: C)(f: (B,C) => A): A = {
  try { f(closeB,closeC) } finally { closeB.close(); closeC.close() }
}

如果您需要更多功能,编写一些生成源代码的程序可能是值得的。

finally 中的两个操作不安全。 - zinking

2
这是一个示例,可以让您使用Scala for comprehension作为任何java.io.Closeable项的自动资源管理块,但它可以很容易地扩展到适用于具有关闭方法的任何对象。
这种用法似乎非常接近using语句,并允许您轻松地在一个块中定义尽可能多的资源。
object ResourceTest{
  import CloseableResource._
  import java.io._

  def test(){
    for( input <- new BufferedReader(new FileReader("/tmp/input.txt")); output <- new FileWriter("/tmp/output.txt") ){
      output.write(input.readLine)
    }
  }
}

class CloseableResource[T](resource: =>T,onClose: T=>Unit){
  def foreach(f: T=>Unit){
    val r = resource
    try{
      f(r)
    }
    finally{
      try{
        onClose(r)
      }
      catch{
        case e =>
          println("error closing resource")
          e.printStackTrace
      }
    }
  }
}

object CloseableResource{
  implicit def javaCloseableToCloseableResource[T <: java.io.Closeable](resource:T):CloseableResource[T] = new CloseableResource[T](resource,{_.close})
}

1

这是我在Scala中解决资源管理的方案:

  def withResources[T <: AutoCloseable, V](r: => T)(f: T => V): V = {
    val resource: T = r
    require(resource != null, "resource is null")
    var exception: Throwable = null
    try {
      f(resource)
    } catch {
      case NonFatal(e) =>
        exception = e
        throw e
    } finally {
      closeAndAddSuppressed(exception, resource)
    }
  }

  private def closeAndAddSuppressed(e: Throwable,
                                    resource: AutoCloseable): Unit = {
    if (e != null) {
      try {
        resource.close()
      } catch {
        case NonFatal(suppressed) =>
          e.addSuppressed(suppressed)
      }
    } else {
      resource.close()
    }
  }

我在多个Scala应用程序中使用过这个,包括管理Spark执行器中的资源。需要注意的是,我们还有其他更好的资源管理方式,例如在CatsIO中:https://typelevel.org/cats-effect/datatypes/resource.html。如果您对Scala中的纯FP感到满意,那么可以考虑使用这种方式。
回答您最后一个问题,您绝对可以像这样嵌套资源:
withResource(r: File)(
  r => {
    withResource(a: File)(
      anotherR => {
        withResource(...)(...)
      }
    )
  }
)

这样做不仅可以保护资源不泄漏,还可以按正确的顺序释放它们(例如堆栈)。与CatsIO中的Resource Monad具有相同的行为。

1
这个解决方案的语法不完全符合您的要求,但我认为它已经足够接近了 :)
def using[A <: {def close(): Unit}, B](resources: List[A])(f: List[A] => B): B =
    try f(resources) finally resources.foreach(_.close())

using(List(new BufferedReader(new FileReader("in.txt")), new PrintWriter("out.txt"))) {
  case List(in: BufferedReader, out: PrintWriter) => out.println(in.readLine())
}

当然,缺点是你必须在使用块中键入类型BufferedReaderPrintWrter。通过在使用块中为类型A使用多ORed类型边界,你可以添加一些魔法,以便只需要List(in,out)
通过定义一些相当糟糕和危险的隐式转换,您可以避免输入List(另一种避免指定资源类型的方法),但我没有记录详细信息,因为我认为它太危险了。

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