มีทางเลือกการจัดการทรัพยากรอัตโนมัติอะไรบ้างสำหรับ Scala


102

ฉันได้เห็นตัวอย่าง ARM (การจัดการทรัพยากรอัตโนมัติ) มากมายบนเว็บสำหรับ Scala ดูเหมือนว่าจะเป็นพิธีการที่ต้องเขียนเรื่องหนึ่งแม้ว่าส่วนใหญ่จะดูค่อนข้างเหมือนกัน ฉันได้เห็นตัวอย่างที่สวยเย็นโดยใช้ตแม้ว่า

ไม่ว่าในกรณีใด ๆ รหัสจำนวนมากนั้นมีข้อบกพร่องประเภทใดประเภทหนึ่งดังนั้นฉันจึงคิดว่าควรมีการอ้างอิงที่นี่ใน Stack Overflow ซึ่งเราสามารถลงคะแนนเวอร์ชันที่ถูกต้องและเหมาะสมที่สุดได้


คำถามนี้จะสร้างคำตอบได้มากขึ้นหรือไม่หากไม่ใช่วิกิชุมชน โปรดทราบว่าคำตอบที่ได้รับการโหวตในชื่อเสียงรางวัล wiki ของชุมชน ...
huynhjl

2
การอ้างอิงที่ไม่ซ้ำกันสามารถเพิ่มระดับความปลอดภัยอีกระดับให้กับ ARM เพื่อให้แน่ใจว่าการอ้างอิงไปยังทรัพยากรจะถูกส่งกลับไปยังผู้จัดการก่อนที่จะเรียก close () thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
retronym

@retronym ฉันคิดว่าปลั๊กอินที่ไม่เหมือนใครจะเป็นการปฏิวัติมากกว่าความต่อเนื่อง และในความเป็นจริงฉันคิดว่านี่เป็นสิ่งหนึ่งใน Scala ที่ค่อนข้างมีแนวโน้มที่จะพบว่าตัวเองถูกส่งไปยังภาษาอื่น ๆ ในอนาคตอันไม่ไกล เมื่อสิ่งนี้ปรากฏออกมาให้แน่ใจว่าได้แก้ไขคำตอบตามนั้น :-)
Daniel C. Sobral

1
เนื่องจากฉันต้องสามารถซ้อนอินสแตนซ์ java.lang.AutoCloseable ได้หลายตัวซึ่งแต่ละอินสแตนซ์ขึ้นอยู่กับอินสแตนซ์ก่อนหน้านี้ที่ประสบความสำเร็จในที่สุดฉันก็เจอรูปแบบที่มีประโยชน์มากสำหรับฉัน ฉันเขียนมันขึ้นมาเพื่อเป็นคำตอบสำหรับคำถาม StackOverflow ที่คล้ายกัน: stackoverflow.com/a/34277491/501113
dirty3quilibrium

คำตอบ:


10

สำหรับตอนนี้Scala 2.13ได้รับการสนับสนุนในที่สุด: try with resourcesโดยใช้Using :) ตัวอย่าง:

val lines: Try[Seq[String]] =
  Using(new BufferedReader(new FileReader("file.txt"))) { reader =>
    Iterator.unfold(())(_ => Option(reader.readLine()).map(_ -> ())).toList
  }

หรือใช้Using.resourceหลีกเลี่ยงTry

val lines: Seq[String] =
  Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader =>
    Iterator.unfold(())(_ => Option(reader.readLine()).map(_ -> ())).toList
  }

คุณสามารถดูตัวอย่างเพิ่มเติมได้จากการใช้ doc

ยูทิลิตี้สำหรับดำเนินการจัดการทรัพยากรอัตโนมัติ สามารถใช้เพื่อดำเนินการโดยใช้ทรัพยากรหลังจากนั้นจะปล่อยทรัพยากรในลำดับย้อนกลับของการสร้าง


คุณช่วยเพิ่มUsing.resourceตัวแปรด้วยได้ไหม
Daniel C. Sobral

@ DanielC.Sobral แน่เพิ่งเพิ่ม
chengpohi

คุณจะเขียนสิ่งนี้สำหรับ Scala 2.12 อย่างไร นี่คือusingวิธีการที่คล้ายกัน:def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Mike Slinn

75

คริสแฮนเซนรายการบล็อก 'ARM บล็อกใน Scala: เยือนจาก 3/26/09การเจรจาเกี่ยวกับเกี่ยวกับภาพนิ่ง 21 จากมาร์ตินโอเดอร์ สกี ของการนำเสนอ FOSDEM บล็อกถัดไปนี้นำมาจากสไลด์ 21 โดยตรง (โดยได้รับอนุญาต):

def using[T <: { def close() }]
    (resource: T)
    (block: T => Unit) 
{
  try {
    block(resource)
  } finally {
    if (resource != null) resource.close()
  }
}

- ส่งใบเสนอราคา -

จากนั้นเราสามารถเรียกสิ่งนี้ว่า

using(new BufferedReader(new FileReader("file"))) { r =>
  var count = 0
  while (r.readLine != null) count += 1
  println(count)
}

อะไรคือข้อเสียของแนวทางนี้? รูปแบบนั้นดูเหมือนจะระบุถึง 95% ของที่ที่ฉันต้องการการจัดการทรัพยากรอัตโนมัติ ...

แก้ไข:เพิ่มข้อมูลโค้ด


Edit2: การขยายรูปแบบการออกแบบ - โดยได้รับแรงบันดาลใจจากwithคำสั่งpython และการกำหนดที่อยู่:

  • คำสั่งที่จะทำงานก่อนบล็อก
  • ข้อยกเว้นในการโยนซ้ำขึ้นอยู่กับทรัพยากรที่มีการจัดการ
  • จัดการทรัพยากรสองรายการด้วยคำสั่งเดียวโดยใช้
  • การจัดการเฉพาะทรัพยากรโดยจัดเตรียมการแปลงโดยนัยและManagedคลาส

นี่คือกับ Scala 2.8

trait Managed[T] {
  def onEnter(): T
  def onExit(t:Throwable = null): Unit
  def attempt(block: => Unit): Unit = {
    try { block } finally {}
  }
}

def using[T <: Any](managed: Managed[T])(block: T => Unit) {
  val resource = managed.onEnter()
  var exception = false
  try { block(resource) } catch  {
    case t:Throwable => exception = true; managed.onExit(t)
  } finally {
    if (!exception) managed.onExit()
  }
}

def using[T <: Any, U <: Any]
    (managed1: Managed[T], managed2: Managed[U])
    (block: T => U => Unit) {
  using[T](managed1) { r =>
    using[U](managed2) { s => block(r)(s) }
  }
}

class ManagedOS(out:OutputStream) extends Managed[OutputStream] {
  def onEnter(): OutputStream = out
  def onExit(t:Throwable = null): Unit = {
    attempt(out.close())
    if (t != null) throw t
  }
}
class ManagedIS(in:InputStream) extends Managed[InputStream] {
  def onEnter(): InputStream = in
  def onExit(t:Throwable = null): Unit = {
    attempt(in.close())
    if (t != null) throw t
  }
}

implicit def os2managed(out:OutputStream): Managed[OutputStream] = {
  return new ManagedOS(out)
}
implicit def is2managed(in:InputStream): Managed[InputStream] = {
  return new ManagedIS(in)
}

def main(args:Array[String]): Unit = {
  using(new FileInputStream("foo.txt"), new FileOutputStream("bar.txt")) { 
    in => out =>
    Iterator continually { in.read() } takeWhile( _ != -1) foreach { 
      out.write(_) 
    }
  }
}

2
มีทางเลือกอื่น แต่ฉันไม่ได้ตั้งใจจะบอกเป็นนัยว่ามีบางอย่างผิดปกติ ฉันต้องการคำตอบทั้งหมดที่นี่ใน Stack Overflow :-)
Daniel C. Sobral

5
คุณรู้หรือไม่ว่ามีสิ่งนี้ใน API มาตรฐานหรือไม่? ดูเหมือนเป็นงานบ้านที่ต้องเขียนสิ่งนี้ให้ตัวเองตลอดเวลา
Daniel Darabos

เป็นเวลานานแล้วที่มีการโพสต์สิ่งนี้ แต่วิธีแก้ปัญหาแรกจะไม่ปิดสตรีมภายในหากตัวสร้าง out พ่นซึ่งอาจจะไม่เกิดขึ้นที่นี่ แต่มีกรณีอื่นที่อาจไม่ดี ปิดยังสามารถโยน ไม่มีความแตกต่างระหว่างข้อยกเว้นร้ายแรงอย่างใดอย่างหนึ่ง อันที่สองมีกลิ่นโค้ดทุกที่และไม่มีข้อได้เปรียบมากกว่าอันแรก คุณยังหลวมประเภทที่แท้จริงดังนั้นจะไม่มีประโยชน์สำหรับบางอย่างเช่น ZipInputStream
steinybot

คุณแนะนำให้ทำอย่างไรหากบล็อกส่งคืนตัวทำซ้ำ
Jorge Machado

62

แดเนียล

ฉันเพิ่งปรับใช้ไลบรารี scala-arm สำหรับการจัดการทรัพยากรอัตโนมัติ คุณสามารถดูเอกสารได้ที่นี่: https://github.com/jsuereth/scala-arm/wiki

ไลบรารีนี้รองรับการใช้งานสามรูปแบบ (ปัจจุบัน):

1) ความจำเป็น / สำหรับการแสดงออก:

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

2) สไตล์โมนาดิก

import resource._
import java.io._
val lines = for { input <- managed(new FileInputStream("test.txt"))
                  val bufferedReader = new BufferedReader(new InputStreamReader(input)) 
                  line <- makeBufferedReaderLineIterator(bufferedReader)
                } yield line.trim()
lines foreach println

3) สไตล์การต่อเนื่องแบบคั่น

นี่คือเซิร์ฟเวอร์ tcp "echo":

import java.io._
import util.continuations._
import resource._
def each_line_from(r : BufferedReader) : String @suspendable =
  shift { k =>
    var line = r.readLine
    while(line != null) {
      k(line)
      line = r.readLine
    }
  }
reset {
  val server = managed(new ServerSocket(8007)) !
  while(true) {
    // This reset is not needed, however the  below denotes a "flow" of execution that can be deferred.
    // One can envision an asynchronous execuction model that would support the exact same semantics as below.
    reset {
      val connection = managed(server.accept) !
      val output = managed(connection.getOutputStream) !
      val input = managed(connection.getInputStream) !
      val writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output)))
      val reader = new BufferedReader(new InputStreamReader(input))
      writer.println(each_line_from(reader))
      writer.flush()
    }
  }
}

รหัสใช้ประโยชน์จากลักษณะประเภททรัพยากรดังนั้นจึงสามารถปรับให้เข้ากับประเภททรัพยากรส่วนใหญ่ได้ มีทางเลือกที่จะใช้การพิมพ์โครงสร้างกับคลาสด้วยวิธีปิดหรือทิ้ง โปรดตรวจสอบเอกสารและแจ้งให้เราทราบหากคุณคิดว่าจะเพิ่มคุณสมบัติที่มีประโยชน์ใด ๆ


1
ใช่ฉันเห็นสิ่งนี้ ฉันต้องการดูโค้ดเพื่อดูว่าคุณทำบางอย่างสำเร็จได้อย่างไร แต่ตอนนี้ฉันยุ่งมาก อย่างไรก็ตามเนื่องจากเป้าหมายของคำถามคือการอ้างอิงถึงรหัส ARM ที่เชื่อถือได้ฉันจึงทำให้สิ่งนี้เป็นคำตอบที่ยอมรับ
Daniel C. Sobral

18

นี่คือวิธีแก้ปัญหาของJames Iryโดยใช้ความต่อเนื่อง:

// standard using block definition
def using[X <: {def close()}, A](resource : X)(f : X => A) = {
   try {
     f(resource)
   } finally {
     resource.close()
   }
}

// A DC version of 'using' 
def resource[X <: {def close()}, B](res : X) = shift(using[X, B](res))

// some sugar for reset
def withResources[A, C](x : => A @cps[A, C]) = reset{x}

นี่คือวิธีแก้ปัญหาที่มีและไม่มีความต่อเนื่องสำหรับการเปรียบเทียบ:

def copyFileCPS = using(new BufferedReader(new FileReader("test.txt"))) {
  reader => {
   using(new BufferedWriter(new FileWriter("test_copy.txt"))) {
      writer => {
        var line = reader.readLine
        var count = 0
        while (line != null) {
          count += 1
          writer.write(line)
          writer.newLine
          line = reader.readLine
        }
        count
      }
    }
  }
}

def copyFileDC = withResources {
  val reader = resource[BufferedReader,Int](new BufferedReader(new FileReader("test.txt")))
  val writer = resource[BufferedWriter,Int](new BufferedWriter(new FileWriter("test_copy.txt")))
  var line = reader.readLine
  var count = 0
  while(line != null) {
    count += 1
    writer write line
    writer.newLine
    line = reader.readLine
  }
  count
}

และนี่คือคำแนะนำในการปรับปรุงของ Tiark Rompf:

trait ContextType[B]
def forceContextType[B]: ContextType[B] = null

// A DC version of 'using'
def resource[X <: {def close()}, B: ContextType](res : X): X @cps[B,B] = shift(using[X, B](res))

// some sugar for reset
def withResources[A](x : => A @cps[A, A]) = reset{x}

// and now use our new lib
def copyFileDC = withResources {
 implicit val _ = forceContextType[Int]
 val reader = resource(new BufferedReader(new FileReader("test.txt")))
 val writer = resource(new BufferedWriter(new FileWriter("test_copy.txt")))
 var line = reader.readLine
 var count = 0
 while(line != null) {
   count += 1
   writer write line
   writer.newLine
   line = reader.readLine
 }
 count
}

ไม่ได้ใช้ (BufferedWriter ใหม่ (FileWriter ใหม่ ("test_copy.txt"))) ประสบปัญหาเมื่อตัวสร้าง BufferedWriter ล้มเหลว? ทรัพยากรทั้งหมดควรถูกรวมไว้ในบล็อกที่ใช้ ...
Jaap

@Jaap นี่คือสไตล์ที่Oracleแนะนำ BufferedWriterไม่โยนข้อยกเว้นที่ตรวจสอบแล้วดังนั้นหากมีข้อยกเว้นใด ๆ เกิดขึ้นโปรแกรมไม่คาดว่าจะกู้คืนจากมัน
Daniel C. Sobral

7

ฉันเห็นวิวัฒนาการ 4 ขั้นตอนทีละน้อยสำหรับการทำ ARM ใน Scala:

  1. ไม่มี ARM: สิ่งสกปรก
  2. การปิดเท่านั้น: ดีกว่า แต่มีหลายบล็อกที่ซ้อนกัน
  3. Monad ต่อเนื่อง: ใช้สำหรับเพื่อทำให้รังเรียบ แต่แยกออกจากกันอย่างผิดธรรมชาติใน 2 ช่วงตึก
  4. ความต่อเนื่องของรูปแบบโดยตรง: Nirava, aha! นี่เป็นทางเลือกที่ปลอดภัยที่สุดเช่นกัน: ทรัพยากรภายนอก withResource block จะเป็นข้อผิดพลาดประเภท

1
โปรดทราบว่า CPS ใน Scala ถูกนำไปใช้ผ่าน monads :-)
Daniel C. Sobral

1
Mushtaq, 3) คุณสามารถจัดการทรัพยากรใน monad ที่ไม่ใช่ monad ของความต่อเนื่อง 4) การจัดการทรัพยากรโดยใช้รหัสต่อเนื่อง withResources / resource delimited จะไม่เกิน (และไม่น้อยกว่า) ประเภทที่ปลอดภัยกว่า "using" ยังคงเป็นไปได้ที่จะลืมจัดการทรัพยากรที่ต้องการ เปรียบเทียบโดยใช้ (ทรัพยากรใหม่ ()) {first => val second = new Resource () // อ๊ะ! // ใช้ทรัพยากร} // ปิดครั้งแรกด้วยResources {val first = resource (new Resource ()) val second = new Resource () // อ๊ะ! // ใช้ทรัพยากร ... } // เท่านั้นที่ได้รับครั้งแรก
James Iry

2
Daniel, CPS ใน Scala ก็เหมือนกับ CPS ในภาษาที่ใช้งานได้ เป็นความต่อเนื่องที่คั่นด้วยการใช้ monad
James Iry

เจมส์ขอบคุณที่อธิบายได้ดี นั่งอยู่ในอินเดียฉันทำได้แค่หวังว่าจะได้อยู่ที่นั่นเพื่อพูดคุยกับคุณ รอดูเมื่อคุณใส่สไลด์เหล่านั้นออนไลน์ :)
Mushtaq Ahmed

6

มี ARM น้ำหนักเบา (รหัส 10 บรรทัด) มาพร้อมกับไฟล์ที่ดีกว่า ดู: https://github.com/pathikrit/better-files#lightweight-arm

import better.files._
for {
  in <- inputStream.autoClosed
  out <- outputStream.autoClosed
} in.pipeTo(out)
// The input and output streams are auto-closed once out of scope

นี่คือวิธีการใช้งานหากคุณไม่ต้องการทั้งไลบรารี:

  type Closeable = {
    def close(): Unit
  }

  type ManagedResource[A <: Closeable] = Traversable[A]

  implicit class CloseableOps[A <: Closeable](resource: A) {        
    def autoClosed: ManagedResource[A] = new Traversable[A] {
      override def foreach[U](f: A => U) = try {
        f(resource)
      } finally {
        resource.close()
      }
    }
  }

สวยดี ฉันใช้สิ่งที่คล้ายกับวิธีนี้ แต่กำหนด a mapและflatMapวิธีการสำหรับ CloseableOps แทนที่จะเป็น foreach เพื่อที่ความเข้าใจจะไม่ส่งผลให้ข้ามผ่านได้
EdgeCaseBerg

1

วิธีการใช้คลาส Type

trait GenericDisposable[-T] {
   def dispose(v:T):Unit
}
...

def using[T,U](r:T)(block:T => U)(implicit disp:GenericDisposable[T]):U = try {
   block(r)
} finally { 
   Option(r).foreach { r => disp.dispose(r) } 
}

1

อีกทางเลือกหนึ่งคือ Monad Lazy TryClose ของ Choppy ค่อนข้างดีกับการเชื่อมต่อฐานข้อมูล:

val ds = new JdbcDataSource()
val output = for {
  conn  <- TryClose(ds.getConnection())
  ps    <- TryClose(conn.prepareStatement("select * from MyTable"))
  rs    <- TryClose.wrap(ps.executeQuery())
} yield wrap(extractResult(rs))

// Note that Nothing will actually be done until 'resolve' is called
output.resolve match {
    case Success(result) => // Do something
    case Failure(e) =>      // Handle Stuff
}

และด้วยสตรีม:

val output = for {
  outputStream      <- TryClose(new ByteArrayOutputStream())
  gzipOutputStream  <- TryClose(new GZIPOutputStream(outputStream))
  _                 <- TryClose.wrap(gzipOutputStream.write(content))
} yield wrap({gzipOutputStream.flush(); outputStream.toByteArray})

output.resolve.unwrap match {
  case Success(bytes) => // process result
  case Failure(e) => // handle exception
}

ข้อมูลเพิ่มเติมที่นี่: https://github.com/choppythelumberjack/tryclose


0

นี่คือคำตอบของ @ chengpohi ซึ่งได้รับการแก้ไขเพื่อให้ใช้งานได้กับ Scala 2.8+ แทนที่จะเป็นเพียง Scala 2.13 (ใช่มันใช้ได้กับ Scala 2.13 ด้วย):

def unfold[A, S](start: S)(op: S => Option[(A, S)]): List[A] =
  Iterator
    .iterate(op(start))(_.flatMap{ case (_, s) => op(s) })
    .map(_.map(_._1))
    .takeWhile(_.isDefined)
    .flatten
    .toList

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

val lines: Seq[String] =
  using(new BufferedReader(new FileReader("file.txt"))) { reader =>
    unfold(())(_ => Option(reader.readLine()).map(_ -> ())).toList
  }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.