在处理数据库连接或者输入输出流等场景时,我们经常需要写一些非常繁琐又枯燥乏味的代码来关闭数据库连接或输入输出流。
例如数据库操作:
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表达式实现,但是还有更好的实现吗?哈哈,这个就交给读者课后思考了,相信聪明的你一定有自己的想法。