Có hai thông tin cực kỳ quan trọng về thông tin cụ thể của Swift bị thiếu trong các câu trả lời hiện có mà tôi nghĩ giúp loại bỏ hoàn toàn vấn đề này.
- Nếu một giao thức chỉ định một trình khởi tạo là một phương thức bắt buộc, thì trình khởi tạo đó phải được đánh dấu bằng
required
từ khóa của Swift .
- Swift có một bộ quy tắc thừa kế đặc biệt liên quan đến
init
các phương thức.
Các tl; dr là đây:
Nếu bạn triển khai bất kỳ trình khởi tạo nào, bạn không còn kế thừa bất kỳ trình khởi tạo nào được chỉ định của lớp cha.
Các trình khởi tạo duy nhất, nếu có, mà bạn sẽ thừa kế, là các trình khởi tạo tiện lợi siêu hạng chỉ đến một trình khởi tạo được chỉ định mà bạn tình cờ ghi đè.
Vậy ... đã sẵn sàng cho phiên bản dài chưa?
Swift có một bộ quy tắc thừa kế đặc biệt liên quan đến init
các phương thức.
Tôi biết đây là điểm thứ hai trong hai điểm tôi đã thực hiện, nhưng chúng tôi không thể hiểu điểm đầu tiên, hoặc tại sao required
từ khóa thậm chí tồn tại cho đến khi chúng tôi hiểu điểm này. Một khi chúng ta hiểu điểm này, một điểm khác trở nên khá rõ ràng.
Tất cả thông tin tôi trình bày trong phần này của câu trả lời này là từ tài liệu của Apple được tìm thấy ở đây .
Từ các tài liệu của Apple:
Không giống như các lớp con trong Objective-C, các lớp con Swift không kế thừa các trình khởi tạo siêu lớp của chúng theo mặc định. Cách tiếp cận của Swift ngăn chặn tình huống trong đó một trình khởi tạo đơn giản từ siêu lớp được kế thừa bởi một lớp con chuyên biệt hơn và được sử dụng để tạo một thể hiện mới của lớp con không được khởi tạo đầy đủ hoặc chính xác.
Nhấn mạnh mỏ.
Vì vậy, trực tiếp từ các tài liệu của Apple ngay tại đó, chúng ta thấy rằng các lớp con Swift sẽ không luôn luôn (và thường không) thừa hưởng siêu lớp của chúng init
phương thức .
Vì vậy, khi nào họ thừa hưởng từ siêu lớp của họ?
Có hai quy tắc xác định khi một lớp con kế thừa init
các phương thức từ cha của nó. Từ các tài liệu của Apple:
Quy tắc 1
Nếu lớp con của bạn không xác định bất kỳ trình khởi tạo được chỉ định nào, nó sẽ tự động kế thừa tất cả các trình khởi tạo được chỉ định của lớp cha.
Quy tắc 2
Nếu lớp con của bạn cung cấp một triển khai của tất cả các trình khởi tạo được chỉ định của siêu lớp của nó bằng cách kế thừa chúng theo quy tắc 1 hoặc bằng cách cung cấp một triển khai tùy chỉnh như một phần của định nghĩa của nó thì nó sẽ tự động kế thừa tất cả các trình khởi tạo tiện lợi của siêu lớp.
Quy tắc 2 không phải là đặc biệt có liên quan đến chuyện này vì SKSpriteNode
's init(coder: NSCoder)
là không phải là một phương pháp tiện lợi.
Vì vậy, InfoBar
lớp của bạn đã kế thừa trình required
khởi tạo cho đến khi bạn thêm vào init(team: Team, size: CGSize)
.
Nếu bạn đã chưa cung cấp này init
phương pháp và thay vào đó làm bạn InfoBar
'tính s thêm tùy chọn hoặc cung cấp cho họ với giá trị mặc định, sau đó bạn muốn có vẫn được kế thừa SKSpriteNode
' s init(coder: NSCoder)
. Tuy nhiên, khi chúng tôi thêm trình khởi tạo tùy chỉnh của riêng mình, chúng tôi đã ngừng kế thừa trình khởi tạo được chỉ định của siêu lớp của chúng tôi (và trình khởi tạo tiện lợi không trỏ đến trình khởi tạo mà chúng tôi đã triển khai).
Vì vậy, như một ví dụ đơn giản, tôi trình bày điều này:
class Foo {
var foo: String
init(foo: String) {
self.foo = foo
}
}
class Bar: Foo {
var bar: String
init(foo: String, bar: String) {
self.bar = bar
super.init(foo: foo)
}
}
let x = Bar(foo: "Foo")
Mà trình bày các lỗi sau:
Thiếu đối số cho tham số 'bar' trong cuộc gọi.
Nếu đây là Objective-C, nó sẽ không gặp vấn đề gì khi kế thừa. Nếu chúng ta khởi tạo a Bar
với initWithFoo:
Objective-C, thuộc self.bar
tính sẽ đơn giản là nil
. Nó có thể không tuyệt vời, nhưng nó là một trạng thái hoàn toàn hợp lệ cho đối tượng ở đó. Nó không phải là trạng thái hoàn toàn hợp lệ cho đối tượng Swift ở. self.bar
Không phải là một tùy chọn và không thể lànil
.
Một lần nữa, cách duy nhất chúng ta kế thừa các công cụ khởi tạo là không cung cấp riêng của chúng tôi. Vì vậy, nếu chúng ta cố gắng kế thừa bằng cách xóa Bar
's init(foo: String, bar: String)
, như vậy:
class Bar: Foo {
var bar: String
}
Bây giờ chúng tôi quay lại kế thừa (loại), nhưng điều này sẽ không được biên dịch ... và thông báo lỗi giải thích chính xác lý do tại sao chúng tôi không kế thừa init
các phương thức siêu lớp :
Vấn đề: Class 'Bar' không có trình khởi tạo
Fix-It: Thuộc tính được lưu trữ 'thanh' mà không có trình khởi tạo ngăn chặn trình khởi tạo tổng hợp
Nếu chúng tôi đã thêm các thuộc tính được lưu trữ trong lớp con của chúng tôi, không có cách Swift nào có thể để tạo một phiên bản hợp lệ của lớp con của chúng tôi với các trình khởi tạo siêu lớp mà không thể biết về các thuộc tính được lưu trữ của lớp con của chúng tôi.
Được rồi, tốt, tại sao tôi phải thực hiện init(coder: NSCoder)
tất cả? Tại sao vậy required
?
Các init
phương thức của Swift có thể chơi theo một bộ quy tắc thừa kế đặc biệt, nhưng sự tuân thủ giao thức vẫn được kế thừa trong chuỗi. Nếu một lớp cha tuân thủ một giao thức, các lớp con của nó phải tuân theo giao thức đó.
Thông thường, đây không phải là vấn đề, vì hầu hết các giao thức chỉ yêu cầu các phương thức không chơi theo quy tắc thừa kế đặc biệt trong Swift, vì vậy nếu bạn thừa hưởng từ một lớp phù hợp với giao thức, bạn cũng sẽ thừa hưởng tất cả các giao thức các phương thức hoặc thuộc tính cho phép lớp thỏa mãn giao thức.
Tuy nhiên, hãy nhớ rằng, init
các phương thức của Swift chơi theo một bộ quy tắc đặc biệt và không phải lúc nào cũng được kế thừa. Do đó, một lớp phù hợp với một giao thức yêu cầu các init
phương thức đặc biệt (như NSCoding
) yêu cầu lớp đánh dấu các init
phương thức đó là required
.
Xem xét ví dụ này:
protocol InitProtocol {
init(foo: Int)
}
class ConformingClass: InitProtocol {
var foo: Int
init(foo: Int) {
self.foo = foo
}
}
Điều này không biên dịch. Nó tạo ra cảnh báo sau:
Vấn đề: Yêu cầu khởi tạo 'init (foo :)' chỉ có thể được thỏa mãn bởi trình khởi tạo 'bắt buộc' trong lớp không hoàn thành 'ConformingClass'
Fix-It: Yêu cầu chèn
Nó muốn tôi thực hiện init(foo: Int)
khởi tạo yêu cầu. Tôi cũng có thể làm cho nó hạnh phúc bằng cách tạo ra lớp final
(có nghĩa là lớp không thể được kế thừa từ đó).
Vì vậy, điều gì xảy ra nếu tôi phân lớp? Từ thời điểm này, nếu tôi phân lớp, tôi ổn. Nếu tôi thêm bất kỳ trình khởi tạo nào, tôi đột nhiên không còn kế thừa init(foo:)
. Đây là vấn đề vì bây giờ tôi không còn tuân thủ InitProtocol
. Tôi không thể phân lớp từ một lớp phù hợp với giao thức và sau đó đột nhiên quyết định tôi không còn muốn tuân thủ giao thức đó nữa. Tôi đã thừa hưởng sự tuân thủ giao thức, nhưng do cách Swift làm việc với init
kế thừa phương thức, tôi đã không được thừa hưởng một phần của những gì bắt buộc phải tuân thủ giao thức đó và tôi phải thực hiện nó.
Được rồi, tất cả điều này có ý nghĩa. Nhưng tại sao tôi không thể nhận được thông báo lỗi hữu ích hơn?
Có thể cho rằng, thông báo lỗi có thể rõ ràng hơn hoặc tốt hơn nếu nó chỉ định rằng lớp của bạn không còn tuân thủ NSCoding
giao thức được kế thừa và để khắc phục nó, bạn cần phải thực hiện init(coder: NSCoder)
. Chắc chắn rồi.
Nhưng Xcode đơn giản là không thể tạo ra thông báo đó vì thực sự đó sẽ không phải là vấn đề thực sự khi không thực hiện hoặc kế thừa một phương thức bắt buộc. Có ít nhất một lý do khác để tạo ra init
các phương thức required
bên cạnh việc tuân thủ giao thức và đó là các phương thức xuất xưởng.
Nếu tôi muốn viết một phương thức xuất xưởng phù hợp, tôi cần chỉ định loại trả về là Self
(tương đương với Objective-C của Swift instanceType
). Nhưng để làm điều này, tôi thực sự cần phải sử dụng một required
phương thức khởi tạo.
class Box {
var size: CGSize
init(size: CGSize) {
self.size = size
}
class func factory() -> Self {
return self.init(size: CGSizeZero)
}
}
Điều này tạo ra lỗi:
Xây dựng một đối tượng của loại lớp 'Tự' với giá trị siêu dữ liệu phải sử dụng trình khởi tạo 'bắt buộc'
Về cơ bản là cùng một vấn đề. Nếu chúng ta phân lớp Box
, các lớp con của chúng ta sẽ kế thừa phương thức lớp factory
. Vì vậy, chúng tôi có thể gọi SubclassedBox.factory()
. Tuy nhiên, nếu không có sự required
từ khóa trên init(size:)
phương pháp, Box
lớp con 's không đảm bảo kế thừa self.init(size:)
đó factory
đang gọi.
Vì vậy, chúng ta phải tạo phương thức đó required
nếu chúng ta muốn một phương thức xuất xưởng như thế này và điều đó có nghĩa là nếu lớp của chúng ta thực hiện một phương thức như thế này, chúng ta sẽ có một required
phương thức khởi tạo và chúng ta sẽ gặp phải những vấn đề chính xác mà bạn gặp phải ở đây với NSCoding
giao thức.
Cuối cùng, tất cả tập trung vào sự hiểu biết cơ bản rằng các trình khởi tạo của Swift chơi theo một bộ quy tắc thừa kế hơi khác một chút, điều đó có nghĩa là bạn không được đảm bảo thừa kế các trình khởi tạo từ siêu lớp của bạn. Điều này xảy ra vì các trình khởi tạo siêu lớp không thể biết về các thuộc tính được lưu trữ mới của bạn và chúng không thể khởi tạo đối tượng của bạn sang trạng thái hợp lệ. Nhưng, vì nhiều lý do, một siêu lớp có thể đánh dấu một trình khởi tạo là required
. Khi thực hiện, chúng ta có thể sử dụng một trong những kịch bản rất cụ thể mà theo đó chúng ta thực sự kế thừa required
phương thức hoặc chúng ta phải tự thực hiện nó.
Điểm chính ở đây là nếu chúng ta gặp lỗi bạn thấy ở đây, điều đó có nghĩa là lớp của bạn không thực sự thực hiện phương thức nào cả.
Vì có lẽ là một ví dụ cuối cùng để đi sâu vào thực tế là các lớp con Swift không luôn kế thừa các init
phương thức của cha mẹ chúng (mà tôi nghĩ là hoàn toàn trung tâm để hiểu đầy đủ vấn đề này), hãy xem xét ví dụ này:
class Foo {
init(a: Int, b: Int, c: Int) {
// do nothing
}
}
class Bar: Foo {
init(string: String) {
super.init(a: 0, b: 1, c: 2)
// do more nothing
}
}
let f = Foo(a: 0, b: 1, c: 2)
let b = Bar(a: 0, b: 1, c: 2)
Điều này không biên dịch.
Thông báo lỗi mà nó đưa ra là một chút sai lệch:
Đối số thêm 'b' trong cuộc gọi
Nhưng điểm được, Bar
không thừa hưởng bất kỳ Foo
's init
phương pháp bởi vì nó chưa thỏa mãn một trong hai trường hợp đặc biệt cho kế thừa init
phương pháp từ lớp cha của nó.
Nếu đây là Objective-C, chúng tôi sẽ thừa hưởng điều đó mà init
không có vấn đề gì, vì Objective-C hoàn toàn hạnh phúc khi không khởi tạo các thuộc tính của đối tượng (mặc dù là nhà phát triển, bạn không nên hài lòng với điều này). Trong Swift, điều này chỉ đơn giản là không làm được. Bạn không thể có trạng thái không hợp lệ và kế thừa các trình khởi tạo siêu lớp chỉ có thể dẫn đến trạng thái đối tượng không hợp lệ.
init(collection:MPMediaItemCollection)
. Bạn phải cung cấp một bộ sưu tập vật phẩm truyền thông thực sự; đó là điểm của lớp này Lớp này chỉ đơn giản là không thể được khởi tạo mà không có một. Nó sẽ phân tích bộ sưu tập và khởi tạo một tá biến thể hiện. Đó là toàn bộ quan điểm của việc này là trình khởi tạo được chỉ định một và duy nhất! Do đó,init(coder:)
không có MPMediaItemCollection có ý nghĩa (hoặc thậm chí vô nghĩa) để cung cấp ở đây; chỉ cófatalError
cách tiếp cận là đúng.