Lợi thế của việc sử dụng các lớp trừu tượng thay vì các đặc điểm là gì?


371

Lợi thế của việc sử dụng một lớp trừu tượng thay vì một đặc điểm (ngoài hiệu suất) là gì? Có vẻ như các lớp trừu tượng có thể được thay thế bằng các đặc điểm trong hầu hết các trường hợp.

Câu trả lời:


371

Tôi có thể nghĩ về hai sự khác biệt

  1. Các lớp trừu tượng có thể có các tham số constructor cũng như các tham số kiểu. Đặc điểm có thể chỉ có các tham số loại. Có một số cuộc thảo luận rằng trong tương lai thậm chí các đặc điểm có thể có các tham số hàm tạo
  2. Các lớp trừu tượng hoàn toàn tương thích với Java. Bạn có thể gọi chúng từ mã Java mà không cần bất kỳ trình bao bọc nào. Các đặc điểm chỉ có thể tương tác hoàn toàn nếu chúng không chứa bất kỳ mã thực hiện nào

172
Phụ lục rất quan trọng: Một lớp có thể kế thừa từ nhiều đặc điểm nhưng chỉ có một lớp trừu tượng. Tôi nghĩ đây nên là câu hỏi đầu tiên mà nhà phát triển đặt ra khi xem xét sử dụng cái nào trong hầu hết các trường hợp.
BAR

15
phao cứu sinh: "Các đặc điểm chỉ có thể tương tác hoàn toàn nếu chúng không chứa bất kỳ mã thực hiện nào"
Walrus the Cat

2
trừu tượng - khi các hành vi tập thể xác định hoặc dẫn đến một đối tượng (nhánh của đối tượng) nhưng vẫn chưa được cấu thành là đối tượng (sẵn sàng). Đặc điểm, khi bạn cần giới thiệu các khả năng tức là các khả năng không bao giờ bắt nguồn từ việc tạo đối tượng, nó sẽ tiến hóa hoặc bắt buộc khi một đối tượng thoát ra khỏi sự cô lập và phải giao tiếp.
Ramiz Uddin

5
Sự khác biệt thứ hai không tồn tại trong Java8, hãy nghĩ.
Dương Nguyên

14
Per Scala 2.12, một đặc điểm biên dịch sang giao diện Java 8 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Kevin Meredith

209

Có một phần trong Lập trình trong Scala gọi là "Để tính trạng, hay không tính trạng?" giải quyết câu hỏi này Vì phiên bản 1 có sẵn trực tuyến, tôi hy vọng sẽ ổn khi trích dẫn toàn bộ điều ở đây. (Bất kỳ lập trình viên Scala nghiêm túc nào cũng nên mua sách):

Bất cứ khi nào bạn thực hiện một bộ sưu tập hành vi có thể sử dụng lại, bạn sẽ phải quyết định xem bạn muốn sử dụng một đặc điểm hay một lớp trừu tượng. Không có quy tắc vững chắc, nhưng phần này có một vài hướng dẫn để xem xét.

Nếu hành vi sẽ không được sử dụng lại , thì hãy biến nó thành một lớp cụ thể. Đó không phải là hành vi tái sử dụng sau tất cả.

Nếu nó có thể được sử dụng lại trong nhiều lớp , không liên quan , hãy biến nó thành một đặc điểm. Chỉ các đặc điểm có thể được trộn vào các phần khác nhau của hệ thống phân cấp lớp.

Nếu bạn muốn kế thừa từ nó trong mã Java , hãy sử dụng một lớp trừu tượng. Do các đặc điểm với mã không có sự tương tự Java gần gũi, nên có xu hướng lúng túng khi kế thừa từ một đặc điểm trong lớp Java. Kế thừa từ một lớp Scala, trong khi đó, chính xác giống như kế thừa từ một lớp Java. Như một ngoại lệ, một đặc điểm Scala chỉ có các thành viên trừu tượng dịch trực tiếp sang giao diện Java, vì vậy bạn nên thoải mái xác định các đặc điểm đó ngay cả khi bạn mong muốn mã Java được thừa hưởng từ nó. Xem Chương 29 để biết thêm thông tin về cách làm việc với Java và Scala cùng nhau.

Nếu bạn có kế hoạch phân phối nó ở dạng biên dịch và bạn mong đợi các nhóm bên ngoài viết các lớp kế thừa từ nó, bạn có thể nghiêng về việc sử dụng một lớp trừu tượng. Vấn đề là khi một đặc điểm được hoặc mất một thành viên, bất kỳ lớp nào được thừa hưởng từ nó phải được biên dịch lại, ngay cả khi chúng không thay đổi. Nếu khách hàng bên ngoài sẽ chỉ gọi vào hành vi, thay vì thừa hưởng từ nó, thì sử dụng một đặc điểm là tốt.

Nếu hiệu quả là rất quan trọng , hãy nghiêng về việc sử dụng một lớp học. Hầu hết các thời gian chạy Java làm cho một lời gọi phương thức ảo của một thành viên lớp hoạt động nhanh hơn so với một lời gọi phương thức giao diện. Các đặc điểm được biên dịch sang các giao diện và do đó có thể trả một khoản chi phí hiệu năng thấp. Tuy nhiên, bạn chỉ nên đưa ra lựa chọn này nếu bạn biết rằng đặc điểm trong câu hỏi tạo thành một nút cổ chai hiệu năng và có bằng chứng cho thấy việc sử dụng một lớp thay vì thực sự giải quyết vấn đề.

Nếu bạn vẫn chưa biết , sau khi xem xét những điều trên, thì hãy bắt đầu bằng cách biến nó thành một đặc điểm. Bạn luôn có thể thay đổi nó sau này và nói chung, sử dụng một đặc điểm sẽ mở ra nhiều tùy chọn hơn.

Như @Mushtaq Ahmed đã đề cập, một đặc điểm không thể có bất kỳ tham số nào được chuyển đến hàm tạo chính của một lớp.

Một sự khác biệt là điều trị super.

Sự khác biệt khác giữa các lớp và các đặc điểm là trong khi trong các lớp, supercác lệnh gọi bị ràng buộc tĩnh, trong các tính trạng, chúng bị ràng buộc động. Nếu bạn viết super.toStringtrong một lớp, bạn sẽ biết chính xác phương thức thực hiện nào sẽ được gọi. Tuy nhiên, khi bạn viết điều tương tự trong một đặc điểm, việc triển khai phương thức để gọi siêu cuộc gọi là không xác định khi bạn xác định đặc điểm đó.

Xem phần còn lại của Chương 12 để biết thêm chi tiết.

Chỉnh sửa 1 (2013):

Có một sự khác biệt tinh tế trong cách hành xử của các lớp trừu tượng so với các đặc điểm. Một trong những quy tắc tuyến tính hóa là nó bảo toàn hệ thống phân cấp kế thừa của các lớp, chúng có xu hướng đẩy các lớp trừu tượng về sau trong chuỗi trong khi các đặc điểm có thể được trộn lẫn một cách vui vẻ. , vì vậy các lớp trừu tượng có thể được sử dụng cho điều đó. Xem ràng buộc tuyến tính hóa lớp (thứ tự mixin) trong Scala .

Chỉnh sửa 2 (2018):

Kể từ Scala 2.12, hành vi tương thích nhị phân của đặc điểm đã thay đổi. Trước 2.12, việc thêm hoặc xóa thành viên vào tính trạng yêu cầu biên dịch lại tất cả các lớp kế thừa tính trạng, ngay cả khi các lớp không thay đổi. Điều này là do cách các đặc điểm được mã hóa trong JVM.

Kể từ Scala 2.12, các đặc điểm biên dịch thành các giao diện Java , do đó, yêu cầu này đã giảm bớt một chút. Nếu đặc điểm thực hiện bất kỳ điều nào sau đây, các lớp con của nó vẫn yêu cầu biên dịch lại:

  • xác định các trường ( valhoặc var, nhưng hằng số là ok - final valkhông có loại kết quả)
  • gọi super
  • báo cáo khởi tạo trong cơ thể
  • mở rộng một lớp học
  • dựa vào tuyến tính hóa để tìm ra các triển khai ở vùng bên phải

Nhưng nếu đặc điểm không có, bây giờ bạn có thể cập nhật nó mà không phá vỡ tính tương thích nhị phân.


2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine- Ai đó có thể giải thích sự khác biệt ở đây là gì? extendsvs with?
0fnt

2
@ 0fnt Sự khác biệt của anh ấy không phải là về việc kéo dài so với. Những gì anh ấy nói là nếu bạn chỉ trộn lẫn vào đặc điểm trong cùng một biên dịch, thì các vấn đề tương thích nhị phân không được áp dụng. Tuy nhiên, nếu API của bạn được thiết kế để cho phép người dùng trộn lẫn vào đặc điểm đó, thì bạn sẽ phải lo lắng về khả năng tương thích nhị phân.
John Colanduoni

2
@ 0fnt: Hoàn toàn không có sự khác biệt về ngữ nghĩa giữa extendswith. Nó hoàn toàn là cú pháp. Nếu bạn thừa kế từ nhiều mẫu, mẫu đầu tiên extend, tất cả các mẫu khác nhận được with, đó là mẫu đó. Hãy nghĩ về withdấu phẩy : class Foo extends Bar, Baz, Qux.
Jörg W Mittag

77

Đối với bất cứ điều gì nó có giá trị, Lập trình của Oderky et al trong Scala khuyên rằng, khi bạn nghi ngờ, bạn sử dụng các đặc điểm. Bạn luôn có thể thay đổi chúng thành các lớp trừu tượng sau này nếu cần.


20

Khác với thực tế là bạn không thể trực tiếp mở rộng nhiều lớp trừu tượng, nhưng bạn có thể trộn nhiều đặc điểm vào một lớp, điều đáng nói là các đặc điểm này có thể xếp chồng lên nhau, vì các siêu lệnh trong một đặc điểm bị ràng buộc động (nó đang đề cập đến một lớp hoặc tính trạng được trộn lẫn trước đó hiện tại).

Từ câu trả lời của Thomas về sự khác biệt giữa lớp trừu tượng và đặc điểm :

trait A{
    def a = 1
}

trait X extends A{
    override def a = {
        println("X")
        super.a
    }
}  


trait Y extends A{
    override def a = {
        println("Y")
        super.a
    }
}

scala> val xy = new AnyRef with X with Y
xy: java.lang.Object with X with Y = $anon$1@6e9b6a
scala> xy.a
Y
X
res0: Int = 1

scala> val yx = new AnyRef with Y with X
yx: java.lang.Object with Y with X = $anon$1@188c838
scala> yx.a
X
Y
res1: Int = 1

9

Khi mở rộng một lớp trừu tượng, điều này cho thấy rằng lớp con là loại tương tự. Đây không phải là trường hợp cần thiết khi sử dụng đặc điểm, tôi nghĩ.


Điều này có bất kỳ ý nghĩa thực tế nào không, hay nó chỉ làm cho mã dễ hiểu hơn?
Ralf

8

Trong Lập trình Scala, các tác giả nói rằng các lớp trừu tượng tạo ra một mối quan hệ "is-a" theo hướng đối tượng cổ điển trong khi các đặc điểm là một cách sáng tác.


5

Các lớp trừu tượng có thể chứa hành vi - Chúng có thể được tham số hóa với các hàm tạo (các đặc điểm không thể) và đại diện cho một thực thể làm việc. Thay vào đó, các đặc điểm chỉ đại diện cho một tính năng duy nhất, một giao diện của một chức năng.


8
Hy vọng bạn không ngụ ý rằng các đặc điểm không thể chứa hành vi. Cả hai đều có thể chứa mã thực hiện.
Mitch Blevins

1
@Mitch Blevins: Tất nhiên là không. Chúng có thể chứa mã, nhưng khi bạn xác định trait Enumerablecó nhiều hàm trợ giúp, tôi sẽ không gọi chúng là hành vi mà chỉ là chức năng được kết nối với một tính năng.
Dario

4
@Dario Tôi thấy "hành vi" và "chức năng" là từ đồng nghĩa, vì vậy tôi thấy câu trả lời của bạn rất khó hiểu.
David J.

3
  1. Một lớp có thể kế thừa từ nhiều đặc điểm nhưng chỉ có một lớp trừu tượng.
  2. Các lớp trừu tượng có thể có các tham số constructor cũng như các tham số kiểu. Đặc điểm có thể chỉ có các tham số loại. Ví dụ: bạn không thể nói đặc điểm t (i: Int) {}; tham số i là bất hợp pháp.
  3. Các lớp trừu tượng hoàn toàn tương thích với Java. Bạn có thể gọi chúng từ mã Java mà không cần bất kỳ trình bao bọc nào. Các đặc điểm chỉ có thể tương tác hoàn toàn nếu chúng không chứa bất kỳ mã thực hiện nào.
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.