尝试块作用域

18

我对try块中变量作用域不与相关的catch和finally块共享的规则感到不满意。具体来说,它会导致以下代码:

var v: VType = null

try {
  v = new VType()
}
catch {
  case e => // handle VType constructor failure (can reference v)
}
finally {
  // can reference v.
}

相对于:

try {
  val v = new VType()
}
catch {
  case e => // handle VType constructor failure (can reference v)
}
finally {
  // can reference v.
}

请问有人可以解释或证明为什么这个来自Java的规则一直存在吗?

还有,这个规则是否有望改变?

谢谢!

更新

非常感谢到目前为止所有的回复。

共识似乎是“只管去做”,我开始得出结论,也许从技术上讲,我想要的要么不可靠、不值得付出努力,要么难以实现。

我喜欢Rex Kerr的答案,但如何在方法调用中包装上面的原始代码,而不引入方法体中的局部变量呢?

我的尝试不太好,使用按名称传递的参数推迟构造,直到安全进入try块才进行工作,但仍然无法在catch或finally块中访问已构造(或未构造)的对象。


1
好问题。看起来你需要引入一个 var,在需要清除一些资源或其他操作的 try-catch-finally 块中,你不能使用 val - michael.kebe
似乎使用catch语句设计并不是最理想的...在catch语句中生成一个定义好的对象是理想的,因为它是一个快速的过程,并且可以快速从失败的网络请求中恢复。 - Alex
想要做个注记,如果VType构造函数失败,你不想碰v变量,除非你想看到一个大的空值。同时看看scala.util.control.Exception。 - jsuereth
@jsuereth,这个问题源于尝试使用java.util.zip.GZipInputStream进行工作,它可能在构建过程中抛出IOException,因此它是一个具有潜在构造函数失败的经典资源管理场景。谢谢,我会看一下scala.util.control.Exception。 - Don Mackenzie
哦,太遗憾了,我们缺少.NET的可处理语法。 - schmmd
7个回答

22

试试这个;)

val v = try { new VType() } catch { case e: Exception => /* ... */ }
在Scala中,try是一个表达式,因此它具有一个值。

1
过度的架构是一个独立的敌人,而这个解决方案避免了过度设计的诱惑。赞。 - Ichimonji10

16

你可能在错误地思考问题。为什么你想在 try/catch/finally 块中放置如此多的东西?在你的代码中,

try { val v = new VType() }
在你获取v返回之前,可能会抛出异常,因此你不能安全地引用v。但是如果你不能引用v,那么在finally块中能做些什么不会破坏或抛出自己的异常或具有其他模糊的行为呢?如果您创建v但未能创建w,但处理需要同时拥有w?(还是不需要?)这最终会变成一团糟。

但是如果您来自Java,则有几件事可以帮助您以明智的方式编写try/catch/finally块。

您可以做的一件事是捕获某些类别的异常并将它们转换为选项:

def s2a(s: String) = try { Some(s.toInt) } catch { case nfe: NumberFormatException => None}

你可以做的另一件事是创建自己的资源管理器。

def enclosed[C <: { def close() }](c: C)(f: C => Unit) {
  try { f(c) } finally { c.close() }
}
enclosed(new FileInputStream(myFile))(fis => {
  fis.read...
}

或者您可以在另一个方法中创建自己的安全关闭和退出方法:

val r = valuableOpenResource()
def attempt[F](f: => F) = {
  try { f } catch { case re: ReasonableException => r.close() throw re }
}
doSomethingSafe()
attempt( doSomethingDangerous() )
doSomethingElseSafe()
r.close()

在这些不同的处理方式中,我没有太多需要创建变量以保存我想要在catch或finally块中进行清理或处理的变量。


非常感谢您的建议和示例,就风格而言,我无法反驳。只是似乎这是一个遗漏的部分,就像Scala版本的Java习惯用语(如果您知道我的意思)一样,而且try块就像while关键字一样需要被隐藏起来。 - Don Mackenzie
一个建议:在enclosed()方法中,我们可以使用java.io.Closeable而不是使用{def close()}。http://download.oracle.com/javase/6/docs/api/java/io/Closeable.html - Marimuthu Madasamy
@Marimuthu 使用 { def close() } 可能更好,因为它允许您管理满足可关闭合同但不实现 java.io.Closeable 接口的资源。 - Aaron Novstrup
3
无需自己编写资源管理器,可参考:http://github.com/jsuereth/scala-arm 根据需要和类型,它还会正确地使用java.io.Closeable或反射。 - jsuereth

6
这段代码如何运作?
try
{
    int i = 0;

    // Do stuff...

    Foo x = new Foo();

    // Do more stuff...

    Bar y = new Bar();
}
catch
{
    // Print the values of i, x, and y.
}
i,x和y的值是什么?我们在进入catch块之前是否声明了y?

有趣的观点,谢谢。然而我认为作用域是一个编译时的问题,如果是这样的话,变量或常量即使它们的值未确定也是已知的。 - Don Mackenzie
@Don - 我认为你在这方面基本上误解了作用域;作用域允许编译器推断出在代码某个点上必须初始化的值。在Jon B的示例中,他证明编译器无法确定ixy是否已经被初始化。 - oxbow_lakes
@oxbowlakes,我明白你的意思,不确定的值是编译器无法接受的。我只是在考虑Java中声明成员变量的默认值(我知道这不适用于局部变量),或许可以在try块中应用这种默认值。我觉得这并非不可能实现,但我有种印象,它并不被认为是很好的做法。 - Don Mackenzie

4
异常概念不是try块的子例程,而是一种备选代码流。这使得try-catch控制块更像一个“如果发生任何不良情况”则根据需要将这些(catch)行插入到try块的当前位置。
考虑到这一点,不清楚是否会定义 Val v = Type(); ,因为异常可能(理论上)在评估 Val v = Type();之前抛出。是的, Val v 是块中的第一行,但是在它之前可能会抛出JVM错误。
最后,是另一种代码结构,它在离开try-catch结构的末尾添加了一种替代但必需的代码流。同样,我们不知道在调用 finally 块之前已经执行了多少(如果有任何)try块,因此我们不能依赖该块内声明的变量。
现在唯一留下的选择(由于无法使用try块变量,因为它们的存在性不确定)是在整个try-catch-finally结构的外部使用变量以在各个代码块之间进行通信。
它很糟糕吗?也许有一点。我们有更好的选择吗?可能没有。将变量声明放在块外面可以明确地表明变量将在你处理try-catch-finally情况的任何控制结构之前定义。

谢谢你的回答,但我仍然不确定try块中的vals和vars是否可用,它们的值可能已经确定或未确定,但我认为在异常处理中做出这个有用的假设,除非变量状态可以被证明已知。据我所知,catch块是通过从引发异常的try块位置进行非局部跳转来到达的。catch块可以访问包括try块所在块在内的完整作用域,那么为什么try块不能呢? - Don Mackenzie
编译器如何检查“可能存在或可能不存在”的变量?唯一的选择是将这些变量视为不可用,因为考虑它们是否可用并不保证。 - Edwin Buck

3
如果您的主要关注点是 v 应该是不可变的,您可以使用以下代码来实现您想要的效果:
case class VType(name: String) { 
   // ... maybe throw an exception ...
}

val v = LazyVal(() => new VType())
try {
   // do stuff with v
   println(v.name) // implicitly converts LazyVal[VType] to VType

   // do other unsafe stuff
} catch {
   case e => // handle VType constructor failure
   // can reference v after verifying v.isInitialized
} finally {
   // can reference v after verifying v.isInitialized
   if (v.isInitialized) v.safelyReleaseResources
}

其中LazyVal的定义如下:

/**
 * Based on DelayedLazyVal in the standard library
 */
class LazyVal[T](f: () => T) {
   @volatile private[this] var _inited = false
   private[this] lazy val complete = {
      val v = f()
      _inited = true
      v
   }

   /** Whether the computation is complete.
    *
    *  @return true if the computation is complete.
    */
   def isInitialized = _inited

   /** The result of f().
    *
    *  @return the result
    */
   def apply(): T = complete
}

object LazyVal {
   def apply[T](f: () => T) = new LazyVal(f)
   implicit def lazyval2val[T](l: LazyVal[T]): T = l()
}

如果我们能使用lazy val v = new VType(),那就太好了,但据我所知,没有安全的机制来确定lazy val是否已经初始化。


谢谢回复,这种优雅的技术解决了我一开始对这个问题不满意的地方,但我对作用域限制很感兴趣。 - Don Mackenzie
更改try-catch-finally块的作用域规则将使其与其他块(if-else、嵌套函数等)不一致,并且正如Rex指出的那样,可能会导致访问未初始化变量时出现潜在的意外情况。 - Aaron Novstrup
不需要复制懒加载机制,只需使用lazy val v = f,如果仍需要的话,可以使用def apply(): T = v - Randall Schulz
@Randall 是否有内置机制可以安全地测试lazy val是否已经实例化?我在考虑一个情况,即当f抛出异常时,因此需要isDone方法才能在catchfinally块中安全访问延迟值。或者我误解了你的建议? - Aaron Novstrup
不,lazy val 是不透明的。我忽略了你使用 LazyVal 的目的。 - Randall Schulz

3
这里有另一种选择:
object Guard {
    type Closing = {def close:Unit}

    var guarded: Stack[Set[Closing]] = Stack()
    def unapply(c: Closing) = {
      guarded.push(guarded.pop + c)
      Some(c)
    }
    
    private def close {println("Closing"); guarded.head.foreach{c => c.close}}
    private def down {println("Adding Set"); guarded.push(Set())}
    private def up {println("Removing Set"); guarded.pop}
    
    def carefully(f: => Unit) {
      down
      try {f}
      finally {close; up}
    }
}

您可以像这样使用它:
import Guard.carefully

class File {def close {println("Closed File")}}
class BadFile {def close {println("Closed Bad File")}; throw new Exception("BadFile failed")}

carefully {
  val Guard(f) = new File
  val Guard(g) = new File
  val Guard(h) = new BadFile
}

这导致

添加Set

关闭

关闭文件

关闭文件

java.lang.Exception: BadFile 失败

因此,前两个文件被创建,然后当第三个构造函数失败时,前两个文件会自动关闭。所有文件都是值。


1
这真令人印象深刻,非常富有创意。我特别喜欢通过提取器技术来获取值,并且任何异常都会谨慎地传播出去的方式。 - Don Mackenzie
谢谢,有一个注意事项;它的编写方式不是线程安全的,因此在使用演员的应用程序中要小心处理。 - Magnus

2

您的示例没有具体说明为什么需要finally子句。如果VType是需要关闭的资源,您可以通过以下方式之一来关闭它。

1)在使用后抛出异常时仍要引用v:

try {
  val v = new VType // may throw
  try {
    v.someThing  // may throw
  }
  catch {
    case ex => println("Error on doing something with v :" + v + ex) // or whatever
  }
  finally {
    v.close()
  }
}
catch {
  case ex => println("Error on getting or closing v: " + ex)  // v might not be constructed
}

2) 在 catch 子句中,您不需要关心 v:

try {
  val v = new VType // may throw
  try {
    v.someThing  // may throw
  }
  finally {
    v.close()
  }
}
catch {
  case ex => println("Error on either operation: " + ex)
}

在任何情况下,您都可以摆脱变量。

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