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表达式实现,但是还有更好的实现吗?哈哈,这个就交给读者课后思考了,相信聪明的你一定有自己的想法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,226评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,972评论 4 60
  • 曾子曰:'吾日三省吾身;为人谋而不忠乎?与朋友交而不信乎?传不习乎?'”翻成白话是这样:“我每天必定用三件事反省自...
    小绿植物阅读 147评论 0 0
  • 在我们懵懂无知的年纪,就开始了有意无意与他人比较的历程。还记得打开电视,里面就有各种竞赛,各种排名。到学校去看一下...
    wudideqiaoqiao阅读 236评论 3 2
  • 缘起 去年年底因新生大学的元学习课相识了三位共同成长的伙伴,我们在微信群里彼此互相分享自己的成长的路径和困惑,那感...
    李云清阅读 239评论 0 0