2010-08-27 35 views
17

Tôi không hài lòng với quy tắc về phạm vi biến trong khối thử không được chia sẻ với việc bắt liên quan và cuối cùng là chặn. Cụ thể nó dẫn đến mã như sau:thử phạm vi khối

var v: VType = null 

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

Trái ngược với:

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

bất cứ ai có thể vui lòng giải thích hay biện minh cho lý do tại sao quy tắc này từ Java vẫn tồn tại?

và/hoặc có hy vọng rằng điều này có thể thay đổi không?

Cảm ơn!

CẬP NHẬT

Rất cám ơn tất cả các câu trả lời cho đến nay.

Sự đồng thuận dường như ngụ ý "cứ tiếp tục với nó" và tôi bắt đầu kết luận rằng có lẽ về mặt kỹ thuật những gì tôi muốn là không chắc chắn, không đáng nỗ lực hoặc khó đạt được.

Tôi thích câu trả lời của Rex Kerr nhưng làm thế nào mã ban đầu ở trên được gói trong một cuộc gọi phương thức mà không đưa vào một biến cục bộ trong thân phương thức?

Những nỗ lực của riêng tôi không quá tốt, sử dụng thông số bằng tên để trì hoãn việc xây dựng cho đến khi an toàn trong khối thử hoạt động nhưng vẫn không cho tôi truy cập vào đối tượng được xây dựng (hoặc không) khối.

+1

Câu hỏi hay. Có vẻ như bạn phải giới thiệu một 'var' và bạn không thể sử dụng' val' trong 'try-catch-finally'-blocks, nơi bạn cần xóa một số tài nguyên hoặc bất cứ thứ gì. –

+0

Dường như thiết kế bằng câu lệnh bắt không tạo ra lý tưởng ... Việc tạo một đối tượng được xác định trong câu lệnh khai thác lý tưởng vì nó là một quá trình nhanh và cung cấp hồi phục nhanh từ yêu cầu mạng không thành công, v.v. – Alex

+0

ghi chú, rằng nếu constructor VType thất bại bạn làm * không * muốn chạm vào biến v, trừ khi bạn muốn nhìn thấy một null rỗng lớn. Ngoài ra hãy xem scala.util.control.Exception – jsuereth

Trả lời

14

Bạn có thể đang suy nghĩ về sự cố không đúng cách. Tại sao bạn muốn có quá nhiều thứ trong try/catch/finally block? Trong mã của bạn,

try { val v = new VType() } 

ngoại trừ có thể được ném trước khi bạn nhận v lại, vì vậy bạn có thể không an toàn tham khảo v. Nhưng nếu bạn không thể tham chiếu v, thì bạn có thể làm gì ở phía cuối cùng mà sẽ không phá vỡ hoặc ném ngoại lệ của riêng nó hoặc có một số hành vi không xác định khác? Điều gì sẽ xảy ra nếu bạn tạo v nhưng không tạo được w, nhưng việc xử lý yêu cầu phải có w? (Hay không?) Nó kết thúc là một mớ hỗn độn.

Nhưng nếu bạn đến từ Java, có một vài điều có thể giúp bạn viết try/catch/finally chặn theo cách hợp lý.

Một điều bạn có thể làm là nắm bắt các lớp học nhất định ngoại lệ và biến chúng thành các lựa chọn thay vì:

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

Một điều bạn có thể làm là tạo ra quản lý tài nguyên của riêng bạn

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

Hoặc bạn có thể tạo phương pháp tắt và thoát của riêng bạn theo cách khác:

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

Giữa những cách xử lý những thứ khác nhau này, tôi không cần phải tạo ra các vars để giữ các biến mà tôi muốn dọn dẹp sau đó hoặc xử lý bằng cách bắt hoặc cuối cùng là các khối.

+0

cảm ơn rất nhiều vì lời khuyên và ví dụ của bạn, cho đến nay một phong cách có liên quan tôi không thể tranh cãi. Nó chỉ có vẻ như đây là một trong đó đã đi xa như xa như một phiên bản Scala của một thành ngữ Java (nếu bạn biết những gì tôi có ý nghĩa) và thử khối là một cái gì đó để được ẩn đi như từ khóa trong khi. –

+0

Một gợi ý: thay vì sử dụng {def close()} trong phương thức kèm theo(), chúng ta có thể sử dụng java.io.Closeable http://download.oracle.com/javase/6/docs/api/java/io/Closeable .html –

+0

@Marimuthu Sử dụng {def close()} có thể thích hợp hơn vì nó cho phép bạn quản lý tài nguyên thỏa mãn hợp đồng có thể đóng được nhưng không triển khai giao diện 'java.io.Closeable'. –

6

Mã này hoạt động như thế nào?

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. 
} 

Giá trị của i, x và y là gì? Có phải y thậm chí đã được tuyên bố trước khi chúng tôi hạ cánh trong khối đánh bắt?

+0

Thú vị điểm, cảm ơn, tuy nhiên tôi nghĩ phạm vi là một vấn đề thời gian biên dịch, nếu như vậy các vars hoặc vals sẽ được biết ngay cả khi giá trị của họ chưa được xác định. –

+0

@Don - Tôi nghĩ rằng bạn về cơ bản là hiểu sai phạm vi về vấn đề này; phạm vi cho phép trình biên dịch lý do về những gì * phải * được khởi tạo cho một số giá trị tại một điểm trong mã của bạn. Trong ví dụ của Jon B, anh ta chứng minh rằng trình biên dịch không thể chắc chắn rằng bất kỳ 'i',' x' hoặc 'y' nào đã được khởi tạo –

+0

@oxbowlakes, tôi lấy điểm của bạn, các giá trị chưa xác định là không thể chấp nhận đối với trình biên dịch. Tôi đã suy nghĩ dọc theo các dòng mặc định cho các thành viên được khai báo trong Java (tôi biết nó không áp dụng cho người dân địa phương) có thể đã có thể áp dụng trong các khối thử. Tôi cảm thấy nó không phải là không thể đạt được nhưng tôi nhận được ấn tượng nó không được coi là tốt. –

4

Khái niệm ngoại lệ không phải là chương trình con của khối thử, nó là luồng mã thay thế. Điều đó làm cho khối điều khiển thử nắm bắt giống như "nếu có bất kỳ điều gì xảy ra" thì chèn các dòng (bắt) này vào vị trí hiện tại của khối thử, nếu cần. Với điều đó trong tâm trí, nó không rõ ràng nếu Val v = Type(); sẽ được xác định hay không vì ngoại lệ có thể (về mặt lý thuyết) được ném trước khi Val v = Type(); được đánh giá. Có, Val v là dòng đầu tiên trong khối, nhưng có lỗi JVM có thể được ném trước đó.

Cuối cùng là một cấu trúc mã khác bổ sung và thay thế, nhưng bắt buộc, mã sẽ kết thúc khi rời khỏi cấu trúc try-catch. Một lần nữa, chúng tôi không có ý tưởng bao nhiêu (nếu có) khối thử đã được đánh giá trước khi khối finally được gọi, vì vậy chúng tôi không thể phụ thuộc vào các biến được khai báo trong khối đó.

Cách duy nhất còn lại (bây giờ chúng ta không thể dùng biến khối thử, do sự không chắc chắn của chúng) là sử dụng biến ngoài toàn bộ cấu trúc try-catch-end để liên lạc giữa các khối mã riêng lẻ.

Nó có hút không? Có thể một chút. Chúng ta có gì tốt hơn không? Chắc là không. Đặt các khai báo biến bên ngoài khối làm cho nó rõ ràng rằng các biến sẽ được định nghĩa trước bất kỳ cấu trúc điều khiển nào mà bạn xử lý thông qua một kịch bản cố gắng nắm bắt cuối cùng.

+0

cảm ơn cho câu trả lời, tôi vẫn không tin rằng các khối thử và vars thử không có sẵn, giá trị của chúng có thể hoặc không được xác định, nhưng tôi nghĩ rằng đó là một giả định hữu ích để thực hiện trong xử lý ngoại lệ trừ khi trạng thái biến được biết đến. Theo như tôi biết, khối catch đạt được bằng một bước nhảy không phải cục bộ từ vị trí trong khối thử, nơi ngoại lệ được nâng lên. Khối catch có quyền truy cập vào phạm vi đầy đủ tối đa và bao gồm cả khối có chứa khối thử, vậy tại sao khối thử cũng không? –

+0

Làm thế nào để kiểm tra trình biên dịch cho "có thể hoặc có thể không" có sẵn? Tùy chọn duy nhất là xem xét các biến không có sẵn, vì bất kỳ xem xét nào về các biến có sẵn không được đảm bảo. –

3

Nếu mối quan tâm chính của bạn là v nên bất biến, bạn có thể nhận được gần với những gì bạn muốn với:

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 
} 

nơi LazyVal được định nghĩa là

/** 
* 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() 
} 

Nó sẽ được tốt đẹp nếu chúng ta có thể sử dụng lazy val v = new VType(), nhưng AFAIK không có cơ chế để xác định xem liệu lazy val đã được khởi tạo chưa.

+0

Cảm ơn bạn đã trả lời, kỹ thuật thanh lịch này giải quyết những gì khiến tôi không hài lòng về vấn đề này ngay từ đầu nhưng tôi tò mò về giới hạn phạm vi. –

+0

Thay đổi các quy tắc phạm vi cho các khối try-catch cuối cùng sẽ làm cho nó không phù hợp với các khối khác (nếu có, các hàm lồng nhau, vv) và, như được chỉ ra, sẽ dẫn đến những bất ngờ tiềm ẩn khi truy cập một biến có thể không có đã được khởi tạo. –

+0

Không cần sao chép cơ chế khởi tạo lười biếng, chỉ cần sử dụng 'lazy val v = f' và nếu bạn vẫn muốn,' def apply(): T = v'. –

2

Ví dụ của bạn không cụ thể hóa tại sao bạn cần mệnh đề cuối cùng. Nếu VType là ví dụ: một nguồn lực cần được đóng lại, bạn có thể thực hiện theo một trong các cách sau.

1) Bạn muốn tham khảo v sau khi sử dụng nó ném một ngoại lệ:

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) Bạn không quan tâm đến v trong mệnh đề catch:

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

Trong cả hai trường hợp, bạn thoát khỏi var.

3

Đây là một thay thế:

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} 
    } 
} 

Bạn có thể sử dụng nó như thế này:

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 
} 

mà kết quả trong

Thêm Set

Đóng

đóng file

đóng file

java.lang.Exception: BadFile thất bại

Vì vậy, hai tập tin đầu tiên được tạo ra, sau đó khi các nhà xây dựng thứ ba thất bại, lần đầu tiên hai sẽ được tự động đóng lại . Tất cả các tệp là giá trị.

+0

điều này thật ấn tượng và rất sáng tạo. Tôi đặc biệt thích kỹ thuật vắt để thu được các vals và cách bất kỳ ngoại lệ nào lan truyền ra một cách cẩn thận. –

+0

Cảm ơn, một báo trước; nó không phải là luồng an toàn như được viết, vì vậy hãy xử lý cẩn thận trong một ứng dụng sử dụng các tác nhân chẳng hạn. – Magnus

20

Chỉ cần "thử" này;)

val v = try { new VType() } catch { case e: Exception => /* ... */ } 

Trong Scala, try là một biểu hiện, vì vậy nó có giá trị.

+1

Kiến trúc quá mức là một kẻ thù tất cả của riêng nó, và giải pháp này tránh được sự cám dỗ để vượt qua một giải pháp. Thanh danh. – Ichimonji10