Scala中資源關閉的小技巧

by 壯衣

在處理數據庫連接或者輸入輸出流等場景時,我們經常需要寫一些非常繁瑣又枯燥乏味的代碼來關閉數據庫連接或輸入輸出流。

例如數據庫操作:

  def update(sql: String)(conn: Connection): Int = {
    var statement: Statement = null
    try {
      statement = conn.createStatement()
      statement.executeUpdate(sql)
    } finally {
      if (conn != null) conn.close()
      if (statement != null) statement.close()
    }
  }

例如文本操作:

  def save(sql: String, path: String): Unit = {
    var pw: PrintWriter = null
    try {
      pw = new PrintWriter(path)
      pw.println(sql)
    } finally {
      if (pw != null) pw.close()
    }
  }

從上面的兩個例子中(這樣的例子還有很多)可以發現兩者存在相同模式的代碼:

var a: A = null
try {
  a = xxx
  //對a進行操作
}  finally {
  if(a != null) a.close()
}

那能不能把這些相同模式的代碼抽象出來呢?答案是肯定的,我們可以利用Scala的泛型和高階函數來完成。先寫一個高階函數:

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

using函數有兩個參數a: A、f: A => B,其中a是需要關閉的資源,f是一個輸入為A輸出為B的函數。現在我們可以利用using函數來重寫數據庫操作和文本操作了。

數據庫操作:

  def update0(sql: String)(conn: Connection): Int = {
    using(conn) {
      conn => {
        using(conn.createStatement()) {
          statement => statement.executeUpdate(sql)
        }
      }
    }
  }

文本操作:

  def save0(sql: String, path: String): Unit = {
      using(new PrintWriter(path)) {
        pw => pw.println(sql)
      }
  }

可以看出重寫后的函數相比之前更加精煉,函數只需要關心實現自己的邏輯而不用關心資源關閉操作,這些就交給using函數來處理吧。目前看來using函數似乎可以滿足我們的需求了,真的是這樣嗎?我們再看一個例子:

  def query[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
    using(conn) {
      conn => {
        using(conn.createStatement()) {
          statement => {
            using(statement.executeQuery(sql)){
              resultSet => f(resultSet)
            }             
          }
        }
      }
    }
  }

可以看到上面的例子用到了3次using,嵌套了3層函數,代碼的可讀性變差。而且一旦需要關閉的資源變多,嵌套函數的層數也將相應增加。代碼又陷入另一個繁瑣枯燥的模式。有什么更好的辦法嗎?也許可以試一下for表達式這個語法糖,那么代碼將如下表示:

  def query0[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
    for {
      conn <- Closable(conn)
      stmt <- Closable(conn.createStatement())
      rs <- Closable(stmt.executeQuery(sql))
    } yield f(rs)
  }

這樣沒有了復雜的嵌套函數,代碼的可讀性更好了。可是Closable是什么類型?別急,我們可以從上面的代碼分析出Closable類型是什么結構,首先Closable類型有一個可關閉資源的屬性;然后Closable類型可以使用for表達式語法糖,那么Closable類型需要實現map和flatMap函數。Closable類型實現如下:

case class Closable[A <: { def close(): Unit }](a: A) {

  def map[B](f: A => B): B =
    try f(a)
    finally {
      if(a != null) a.close()
    }

  def flatMap[B](f: A => B): B = map(f)

}

到此代碼已經滿足我們的需求了,但是還是很想探究其中的魔法。我們知道for表達式其實就是flatMap加map的語法糖,那么讓我們剝開糖衣看看這塊糖真實的模樣:

  def query1[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
    Closable(conn).flatMap {
      conn => Closable(conn.createStatement()).flatMap {
        stmt => Closable(stmt.executeQuery(sql)).map {
          rs => f(rs)
        }
      }
    }
  }

讓我們接著剝開flatMap

  def query2[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
    Closable(conn).map {
      conn => Closable(conn.createStatement()).map {
        stmt => Closable(stmt.executeQuery(sql)).map {
          rs => f(rs)
        }
      }
    }
  }

最后剝開map

  def query3[A](sql: String, conn: Connection)(implicit f: ResultSet => List[A]): List[A] = {
    Closable(conn).map {
      conn => try {
        Closable(conn.createStatement()).map {
          stmt => try {
            Closable(stmt.executeQuery(sql)).map {
              rs => try {
                f(rs)
              } finally {
                if(rs != null) rs.close()
              }
            }
          } finally {
            if (stmt != null) stmt.close()
          }
        }
      } finally {
        if (conn != null) conn.close()
      }
    }
  }

到此Closable類型的神秘面紗已經完全揭開,希望Closable類型可以在各位讀者工作中在處理一些需要關閉資源的時候提供一種選擇,最后再多說兩句。有時我們只需要處理兩個可關閉資源,而且這兩個資源之間沒有關聯。例如文本操作有一個輸入流一個輸出流,那么我們使用Closable類型代碼將會如下:

  def save1(inPath: String, outPath: String): Unit = {
    for {
      in <- Closable(new BufferedReader(new FileReader(inPath)))
      out <- Closable(new PrintWriter(outPath))
    } yield out.println(in.readLine())
  }

不是說上面的代碼不好,而是我們可以做到更加簡練,代碼如下:

  def save2(inPath: String, outPath: String): Unit = {
    Closable(new BufferedReader(new FileReader(inPath)))
      .map2(new PrintWriter(outPath)){ (in, out) => out.println(in.readLine()) }
  }

讓我們來看下 map2的實現:

  def map2[B <: { def close(): Unit }, C](b: B)(f: (A, B) => C): C =
    for {
      ac <- Closable(a)
      bc <- Closable(b)
    } yield f(ac, bc)

聰明的讀者可能還會有疑惑,map2用于關閉兩個資源,那關閉3個資源我們需要實現一個map3,關閉N個資源豈不是要實現一個mapN。當然我們可以使用for表達式實現,但是還有更好的實現嗎?哈哈,這個就交給讀者課后思考了,相信聰明的你一定有自己的想法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,552評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,255評論 4 61
  • 曾子曰:'吾日三省吾身;為人謀而不忠乎?與朋友交而不信乎?傳不習乎?'”翻成白話是這樣:“我每天必定用三件事反省自...
    小綠植物閱讀 162評論 0 0
  • 在我們懵懂無知的年紀,就開始了有意無意與他人比較的歷程。還記得打開電視,里面就有各種競賽,各種排名。到學校去看一下...
    wudideqiaoqiao閱讀 275評論 3 2
  • 緣起 去年年底因新生大學的元學習課相識了三位共同成長的伙伴,我們在微信群里彼此互相分享自己的成長的路徑和困惑,那感...
    李云清閱讀 252評論 0 0