2012-01-29 34 views
7

Viết một ví dụ đơn giản từ cuốn sách Odersky của dẫn đến các vấn đề sau:scala vấn đề thừa kế: val vs def

// AbstractElement.scala 
abstract class AbstractElement { 
    val contents: Array[String] 
    val height: Int = contents.length // line 3 
} 

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends AbstractElement { // line 6 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} 

object AbstractElement { 
    def create(ch: Char): AbstractElement = { 
    new UnifiedElement(ch, 1, 1) // line 12 
    } 
} 

,

// ElementApp.scala 
import AbstractElement.create 

object ElementApp { 

    def main(args: Array[String]): Unit = { 
    val e1 = create(' ') // line 6 
    println(e1.height) 
    } 
} 

Trình biên dịch ném dấu vết sau:

Exception in thread "main" java.lang.NullPointerException 
    at AbstractElement.<init>(AbstractElement.scala:3) 
    at UnifiedElement.<init>(AbstractElement.scala:6) 
    at AbstractElement$.create(AbstractElement.scala:12) 
    at ElementApp$.main(ElementApp.scala:6) 
    at ElementApp.main(ElementApp.scala) 

Vì vậy trình biên dịch nghĩ rằng nội dung vẫn là null, nhưng tôi đã định nghĩa nó trong UnifiedContainer!

Mọi thứ trở nên kỳ lạ hơn khi tôi thay thế val bằng def và evrth hoạt động hoàn hảo!

Bạn có thể xplain hành vi này không?

Trả lời

13

Here là một bài viết tuyệt vời của Paul P giải thích sự phức tạp về trật tự khởi tạo trong Scala. Theo quy tắc chung, bạn nên không bao giờ sử dụng tóm tắt val s. Luôn sử dụng số trừu tượng def s và lazy val s.

+2

Bạn có thể làm rõ 'Luôn sử dụng abstract 'def' và 'lazy val's không? Trình biên dịch không cho phép bạn đặt 'lazy val' vào một lớp trừu tượng. – jbx

+1

@jbx, vâng, bạn nói đúng. Điều đó chỉ áp dụng cho các đặc điểm. – missingfaktor

+0

Một số ví dụ tôi thấy sử dụng 'val' trong lớp trừu tượng và sau đó' lazy val' trong lớp bê tông mở rộng nó. Đây có phải là cách chính xác không? (Tôi vẫn đang học Scala vì vậy bị lẫn lộn một chút) – jbx

4

Trong định nghĩa AbstractElement, bạn đang thực hành xác định hàm tạo khởi tạo nội dung thành rỗng và tính nội dung.length. Phương thức khởi tạo của UnifiedElement gọi hàm tạo của AbstractElement và chỉ sau đó khởi tạo nội dung.

EDIT: nói cách khác, chúng ta có một ví dụ mới của một vấn đề đã tồn tại trong Java (và bất kỳ ngôn ngữ OOP): các nhà xây dựng của một lớp cha gọi một phương pháp thực hiện trong một lớp con, nhưng sau này không thể một cách an toàn được gọi vì lớp con chưa được xây dựng. Tóm tắt vals chỉ là một trong những cách để kích hoạt nó.

Giải pháp đơn giản nhất ở đây là chỉ cần thực hiện height một def, tốt hơn là anwyay và nhận thức được các quy tắc khởi tạo được liên kết trong câu trả lời khác.

abstract class AbstractElement { 
    val contents: Array[String] 
    def height: Int = contents.length //Make this a def 
} 

Giải pháp hơi phức tạp hơn, thay vào đó, là để buộc contents được khởi tạo trước chiều cao, mà bạn có thể làm với cú pháp sau:

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends { 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} with AbstractElement { 
    //... 
} 

Lưu ý rằng thành phần mixin, đó là with , không đối xứng - nó hoạt động từ trái sang phải. Và lưu ý rằng {} ở cuối có thể được bỏ qua, nếu bạn xác định không có thành viên nào khác.

Vals lười cũng là một giải pháp, nhưng chúng phải chịu một số chi phí thời gian chạy - bất cứ khi nào bạn đọc biến, mã được tạo sẽ đọc bitmap dễ bay hơi để kiểm tra xem trường đã được khởi tạo chưa.

Làm contents một def đây có vẻ là một ý tưởng tồi, bởi vì nó sẽ được tính toán lại thường xuyên.

Cuối cùng, tránh lừa đảo trừu tượng là IMHO một biện pháp khắc nghiệt. Đôi khi chúng chỉ là điều đúng - bạn chỉ nên cẩn thận với các vals cụ thể đề cập đến các vals trừu tượng.

EDIT: Có vẻ như thay vì một val trừu tượng, người ta có thể sử dụng định nghĩa trừu tượng và ghi đè nó bằng một giá trị cụ thể. Điều đó thực sự có thể, nhưng nó không giúp đỡ nếu có những vals cụ thể đề cập đến định nghĩa trừu tượng. Cân nhắc biến thể này của đoạn mã trên, và chú ý đến cách thành viên được định nghĩa:

abstract class AbstractElement { 
    def contents: Array[String] 
    val height: Int = contents.length // line 3 
} 

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends AbstractElement { 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} 

Mã này có hành vi gian chạy giống như mã số được đưa ra bởi OP, ngay cả khi AbstractElement.contents bây giờ là một def: cơ thể của accessor đọc một trường được khởi tạo chỉ bởi hàm tạo lớp con. Sự khác biệt duy nhất giữa giá trị trừu tượng và định nghĩa trừu tượng có vẻ là giá trị trừu tượng chỉ có thể bị ghi đè bởi giá trị cụ thể, vì vậy có thể hữu ích để hạn chế hành vi của các lớp con nếu đó là những gì bạn muốn.

+0

Bạn có thể giải thích cách 'val' trừu tượng đôi khi là điều đúng không? def' overridden bởi một cách tiếp cận cụ thể 'val'"? – missingfaktor

+0

Tôi nghĩ về điều đó, nhưng nó không giải quyết vấn đề, như tôi bây giờ giải thích. – Blaisorblade

Các vấn đề liên quan