Khi nào sử dụng val hoặc def trong đặc điểm Scala?


90

Tôi đã xem qua các slide scala hiệu quả và nó đề cập đến slide 10 để không bao giờ sử dụng valtrong a traitcho các thành viên trừu tượng và sử dụng defthay thế. Trang trình bày không đề cập chi tiết lý do tại sao sử dụng trừu tượng valtrong a traitlà phản mẫu. Tôi sẽ đánh giá cao nếu ai đó có thể giải thích phương pháp hay nhất về việc sử dụng val so với def trong một đặc điểm cho các phương thức trừu tượng

Câu trả lời:


130

A defcó thể được thực hiện bởi a def, a val, a lazy valhoặc an object. Vì vậy, nó là hình thức trừu tượng nhất để xác định một thành viên. Vì các đặc điểm thường là các giao diện trừu tượng, nói rằng bạn muốn a vallà nói cách triển khai nên thực hiện. Nếu bạn yêu cầu a val, một lớp thực thi không thể sử dụng a def.

A valchỉ cần thiết nếu bạn cần một số nhận dạng ổn định, ví dụ: đối với kiểu phụ thuộc vào đường dẫn. Đó là thứ bạn thường không cần.


So sánh:

trait Foo { def bar: Int }

object F1 extends Foo { def bar = util.Random.nextInt(33) } // ok

class F2(val bar: Int) extends Foo // ok

object F3 extends Foo {
  lazy val bar = { // ok
    Thread.sleep(5000)  // really heavy number crunching
    42
  }
}

Nếu bạn có

trait Foo { val bar: Int }

bạn sẽ không thể xác định F1hoặc F3.


Ok, và làm bạn bối rối và hãy trả lời @ om-nom-nom — việc sử dụng trừu tượng valcó thể gây ra sự cố khởi tạo:

trait Foo { 
  val bar: Int 
  val schoko = bar + bar
}

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

Đây là một vấn đề xấu mà theo ý kiến ​​cá nhân của tôi sẽ biến mất trong các phiên bản Scala trong tương lai bằng cách sửa nó trong trình biên dịch, nhưng có, hiện tại đây cũng là lý do tại sao người ta không nên sử dụng trừu tượng vals.

Chỉnh sửa (tháng 1 năm 2016): Bạn được phép ghi đè một valkhai báo trừu tượng bằng một lazy valtriển khai, vì vậy điều đó cũng sẽ ngăn chặn lỗi khởi tạo.


8
từ về thứ tự khởi tạo phức tạp và giá trị rỗng đáng ngạc nhiên?
om-nom-nom

Vâng ... tôi thậm chí sẽ không đến đó. Đúng, đây cũng là những lập luận chống lại val, nhưng tôi nghĩ rằng động cơ cơ bản chỉ nên là để ẩn việc thực hiện.
0__

2
Điều này có thể đã thay đổi trong phiên bản Scala gần đây (2.11.4 kể từ nhận xét này), nhưng bạn có thể ghi đè a valbằng a lazy val. Khẳng định của bạn mà bạn sẽ không thể tạo ra F3nếu barlà một vallà không đúng. Điều đó nói rằng, các thành viên trừu tượng trong những đặc điểm nên luôn luôn def's
mplis

Ví dụ Foo / Fail hoạt động như mong đợi nếu bạn thay thế val schoko = bar + barbằng lazy val schoko = bar + bar. Đó là một cách để có một số quyền kiểm soát thứ tự khởi tạo. Ngoài ra, sử dụng lazy valthay vì deftrong lớp dẫn xuất tránh tính toán lại.
Adrian

2
Nếu bạn thay đổi val bar: Intthành def bar: Int Fail.schokovẫn là số không.
Jasper-M

8

Tôi không thích sử dụng valtrong các đặc điểm vì khai báo val có thứ tự khởi tạo không rõ ràng và không trực quan. Bạn có thể thêm một đặc điểm vào hệ thống phân cấp đã hoạt động và nó sẽ phá vỡ tất cả những thứ đã hoạt động trước đó, hãy xem chủ đề của tôi: tại sao sử dụng val trơn trong các lớp không phải cuối cùng

Bạn nên ghi nhớ tất cả những điều về việc sử dụng khai báo val này, điều này cuối cùng dẫn bạn đến lỗi.


Cập nhật với ví dụ phức tạp hơn

Nhưng có những lúc bạn không thể tránh khỏi việc sử dụng val. Như @ 0__ đã đề cập, đôi khi bạn cần một định danh ổn định và defkhông phải là một.

Tôi sẽ cung cấp một ví dụ để cho thấy những gì anh ấy đang nói về:

trait Holder {
  type Inner
  val init : Inner
}
class Access(val holder : Holder) {
  val access : holder.Inner =
    holder.init
}
trait Access2 {
  def holder : Holder
  def access : holder.Inner =
    holder.init
}

Mã này tạo ra lỗi:

 StableIdentifier.scala:14: error: stable identifier required, but Access2.this.holder found.
    def access : holder.Inner =

Nếu bạn dành một phút để suy nghĩ, bạn sẽ hiểu rằng trình biên dịch có lý do để phàn nàn. Trong Access2.accesstrường hợp nó không thể lấy kiểu trả về bằng bất kỳ cách nào. def holdercó nghĩa là nó có thể được thực hiện theo một cách rộng rãi. Nó có thể trả về các chủ sở hữu khác nhau cho mỗi cuộc gọi và các chủ sở hữu đó sẽ kết hợp các Innerloại khác nhau. Nhưng máy ảo Java mong đợi loại tương tự sẽ được trả lại.


3
Thứ tự khởi tạo sẽ không thành vấn đề, nhưng thay vào đó, chúng tôi nhận được NPE đáng ngạc nhiên trong thời gian chạy, rõ ràng là chống mẫu.
Jonathan Neufeld

scala có cú pháp khai báo ẩn bản chất mệnh lệnh đằng sau. Đôi khi đó imperativeness việc phản trực giác
ayvango

-4

Luôn luôn sử dụng def có vẻ hơi khó xử vì những thứ như thế này sẽ không hoạt động:

trait Entity { def id:Int}

object Table { 
  def create(e:Entity) = {e.id = 1 }  
}

Bạn sẽ gặp lỗi sau:

error: value id_= is not a member of Entity

2
Không có liên quan. Bạn cũng gặp lỗi nếu sử dụng val thay vì def (lỗi: gán lại thành val), và điều đó hoàn toàn hợp lý.
volia17

Không nếu bạn sử dụng var. Vấn đề là, nếu chúng là trường thì chúng phải được chỉ định như vậy. Tôi chỉ nghĩ rằng có mọi thứ như deflà thiển cận.
Dimitry

@Dimitry, chắc chắn, bằng cách sử dụng varcho phép bạn phá vỡ tính đóng gói. Nhưng sử dụng def(hoặc a val) được ưu tiên hơn một biến toàn cục. Tôi nghĩ những gì bạn đang tìm kiếm là một cái gì đó giống như case class ConcreteEntity(override val id: Int) extends Entityvậy để bạn có thể tạo nó từ def create(e: Entity) = ConcreteEntity(1)Điều này sẽ an toàn hơn việc phá vỡ sự đóng gói và cho phép bất kỳ lớp nào thay đổi Thực thể.
Jono
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.