Tại sao từ khóa tiện lợi thậm chí còn cần thiết trong Swift?


132

Vì Swift hỗ trợ nạp chồng phương thức và trình khởi chạy, bạn có thể đặt nhiều initbên cạnh nhau và sử dụng bất cứ điều gì bạn thấy thuận tiện:

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    init() {
        self.name = "John"
    }
}

Vậy tại sao conveniencetừ khóa thậm chí còn tồn tại? Điều gì làm cho sau đây tốt hơn đáng kể?

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    convenience init() {
        self.init(name: "John")
    }
}

13
Đã đọc qua tài liệu này và cũng bối rối về nó. : /
boidkan

Câu trả lời:


235

Các câu trả lời hiện tại chỉ nói một nửa conveniencecâu chuyện. Nửa còn lại của câu chuyện, một nửa mà không có câu trả lời nào hiện có, trả lời câu hỏi Desmond đã đăng trong các bình luận:

Tại sao Swift buộc tôi phải đặt conveniencetrước trình khởi chạy của mình chỉ vì tôi cần gọi self.inittừ nó? '

Tôi đã chạm vào nó một chút trong câu trả lời này , trong đó tôi trình bày chi tiết một số quy tắc khởi tạo của Swift, nhưng trọng tâm chính là requiredtừ này. Nhưng câu trả lời đó vẫn đang giải quyết một cái gì đó có liên quan đến câu hỏi này và câu trả lời này. Chúng ta phải hiểu cách kế thừa trình khởi tạo Swift hoạt động.

Vì Swift không cho phép các biến chưa được khởi tạo, nên bạn không được đảm bảo kế thừa tất cả (hoặc bất kỳ) trình khởi tạo nào từ lớp mà bạn kế thừa từ đó. Nếu chúng ta phân lớp và thêm bất kỳ biến đối tượng chưa được khởi tạo nào vào lớp con của chúng ta, chúng ta đã dừng kế thừa các công cụ khởi tạo. Và cho đến khi chúng tôi thêm các trình khởi tạo của riêng mình, trình biên dịch sẽ la mắng chúng tôi.

Để rõ ràng, một biến đối tượng chưa được khởi tạo là bất kỳ biến đối tượng nào không được cung cấp một giá trị mặc định (lưu ý rằng các tùy chọn và các tùy chọn không được bao bọc hoàn toàn tự động giả sử giá trị mặc định là nil).

Vì vậy, trong trường hợp này:

class Foo {
    var a: Int
}

alà một biến thể chưa được khởi tạo. Điều này sẽ không biên dịch trừ khi chúng tôi đưa ra amột giá trị mặc định:

class Foo {
    var a: Int = 0
}

hoặc khởi tạo atrong một phương thức khởi tạo:

class Foo {
    var a: Int

    init(a: Int) {
        self.a = a
    }
}

Bây giờ, hãy xem điều gì xảy ra nếu chúng ta phân lớp Foo, phải không?

class Bar: Foo {
    var b: Int

    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }
}

Đúng? Chúng tôi đã thêm một biến và chúng tôi đã thêm một trình khởi tạo để đặt một giá trị để bnó sẽ biên dịch. Tùy thuộc vào ngôn ngữ bạn đến từ ngôn ngữ nào, bạn có thể mong đợi rằng nó Barđã được kế thừa trình Fookhởi tạo , init(a: Int). Nhưng nó không. Và làm thế nào nó có thể? Làm thế nào để Foo's init(a: Int)bí quyết làm thế nào để gán giá trị cho các bbiến mà Barthêm vào? Nó không. Vì vậy, chúng tôi không thể khởi tạo một Barthể hiện bằng một trình khởi tạo không thể khởi tạo tất cả các giá trị của chúng tôi.

Những điều này có liên quan gì convenience?

Chà, hãy xem các quy tắc về thừa kế trình khởi tạo :

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.

Lưu ý Quy tắc 2, trong đó đề cập đến khởi tạo thuận tiện.

Vì vậy, những gì các conveniencetừ khóa không làm là chỉ ra cho chúng ta mà initializers có thể được thừa hưởng bởi lớp con rằng các biến add dụ không có giá trị mặc định.

Hãy lấy Baselớp ví dụ này :

class Base {
    let a: Int
    let b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    convenience init() {
        self.init(a: 0, b: 0)
    }

    convenience init(a: Int) {
        self.init(a: a, b: 0)
    }

    convenience init(b: Int) {
        self.init(a: 0, b: b)
    }
}

Lưu ý rằng chúng tôi có ba conveniencekhởi tạo ở đây. Điều đó có nghĩa là chúng tôi có ba trình khởi tạo có thể được kế thừa. Và chúng tôi có một trình khởi tạo được chỉ định (trình khởi tạo được chỉ định đơn giản là bất kỳ trình khởi tạo nào không phải là trình khởi tạo tiện lợi).

Chúng ta có thể khởi tạo các thể hiện của lớp cơ sở theo bốn cách khác nhau:

nhập mô tả hình ảnh ở đây

Vì vậy, hãy tạo một lớp con.

class NonInheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }
}

Chúng tôi đang thừa hưởng từ Base. Chúng tôi đã thêm biến cá thể của riêng mình và chúng tôi không cung cấp cho nó một giá trị mặc định, vì vậy chúng tôi phải thêm các công cụ khởi tạo của riêng mình. Chúng tôi đã thêm một, init(a: Int, b: Int, c: Int)nhưng nó không khớp với chữ ký của trình Basekhởi tạo được chỉ định của lớp : init(a: Int, b: Int). Điều đó có nghĩa là, chúng tôi không kế thừa bất kỳ công cụ khởi tạo nào từ Base:

nhập mô tả hình ảnh ở đây

Vậy, điều gì sẽ xảy ra nếu chúng ta được thừa hưởng từ Base, nhưng chúng ta đã đi trước và triển khai một trình khởi tạo khớp với trình khởi tạo được chỉ định từ Base?

class Inheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }

    convenience override init(a: Int, b: Int) {
        self.init(a: a, b: b, c: 0)
    }
}

Bây giờ, ngoài hai trình khởi tạo mà chúng tôi đã triển khai trực tiếp trong lớp này, bởi vì chúng tôi đã triển khai trình khởi tạo Baseđược chỉ định của lớp khởi tạo, chúng tôi có thể kế thừa tất cả các Basetrình conveniencekhởi tạo của lớp :

nhập mô tả hình ảnh ở đây

Thực tế là trình khởi tạo với chữ ký phù hợp được đánh dấu là conveniencekhông có sự khác biệt ở đây. Nó chỉ có nghĩa là chỉ có Inheritormột trình khởi tạo được chỉ định. Vì vậy, nếu chúng ta kế thừa từ đó Inheritor, chúng ta chỉ cần thực hiện một trình khởi tạo được chỉ định đó, và sau đó chúng ta sẽ kế thừa trình Inheritorkhởi tạo tiện lợi, điều đó có nghĩa là chúng ta đã thực hiện tất cả các trình Basekhởi tạo được chỉ định và có thể kế thừa trình conveniencekhởi tạo của nó .


16
Câu trả lời duy nhất thực sự trả lời câu hỏi và làm theo các tài liệu. Tôi sẽ chấp nhận nó nếu tôi là OP.
FreeNickname

12
Bạn nên viết một cuốn sách;)
coolbeet

1
@SLN Câu trả lời này bao gồm rất nhiều về cách kế thừa trình khởi tạo Swift hoạt động.
nhgrif

1
@SLN Bởi vì việc tạo một Thanh với init(a: Int)sẽ bkhông được khởi tạo.
Ian Warburton

2
@IanWarburton Tôi không biết câu trả lời cho "tại sao" cụ thể này. Logic của bạn trong phần thứ hai của bình luận của bạn có vẻ hợp với tôi, nhưng tài liệu nêu rõ rằng đây là cách nó hoạt động và đưa ra một ví dụ về những gì bạn hỏi về Sân chơi xác nhận hành vi phù hợp với những gì được ghi lại.
nhgrif

9

Chủ yếu là sự rõ ràng. Từ ví dụ thứ hai của bạn,

init(name: String) {
    self.name = name
}

được yêu cầu hoặc chỉ định . Nó phải khởi tạo tất cả các hằng và biến của bạn. Công cụ khởi tạo tiện lợi là tùy chọn và thường có thể được sử dụng để giúp khởi tạo dễ dàng hơn. Ví dụ: giả sử lớp Người của bạn có giới tính biến tùy chọn:

var gender: Gender?

Giới tính là một enum

enum Gender {
  case Male, Female
}

bạn có thể có các trình khởi tạo tiện lợi như thế này

convenience init(maleWithName: String) {
   self.init(name: name)
   gender = .Male
}

convenience init(femaleWithName: String) {
   self.init(name: name)
   gender = .Female
}

Người khởi tạo thuận tiện phải gọi người khởi tạo được chỉ định hoặc yêu cầu trong đó. Nếu lớp của bạn là một lớp con, nó phải gọi super.init() trong quá trình khởi tạo.


2
Vì vậy, nó sẽ hoàn toàn rõ ràng đối với trình biên dịch những gì tôi đang cố gắng thực hiện với nhiều trình khởi tạo ngay cả khi không có conveniencetừ khóa nhưng Swift vẫn sẽ bị lỗi về nó. Đó không phải là sự đơn giản mà tôi mong đợi từ Apple =)
Desmond Hume

2
Câu trả lời này không trả lời bất cứ điều gì. Bạn nói "rõ ràng", nhưng không giải thích làm thế nào nó làm cho mọi thứ rõ ràng hơn.
Robo Robok

7

Vâng, điều đầu tiên tôi nghĩ đến là nó được sử dụng trong kế thừa lớp để tổ chức mã và dễ đọc. Tiếp tục với Personlớp học của bạn , hãy nghĩ về một kịch bản như thế này

class Person{
    var name: String
    init(name: String){
        self.name = name
    }

    convenience init(){
        self.init(name: "Unknown")
    }
}


class Employee: Person{
    var salary: Double
    init(name:String, salary:Double){
        self.salary = salary
        super.init(name: name)
    }

    override convenience init(name: String) {
        self.init(name:name, salary: 0)
    }
}

let employee1 = Employee() // {{name "Unknown"} salary 0}
let john = Employee(name: "John") // {{name "John"} salary 0}
let jane = Employee(name: "Jane", salary: 700) // {{name "Jane"} salary 700}

Với trình khởi tạo tiện lợi, tôi có thể tạo một Employee()đối tượng không có giá trị, do đó từ nàyconvenience


2
Với conveniencecác từ khóa bị lấy đi, Swift sẽ không có đủ thông tin để hành xử theo cùng một cách chính xác chứ?
Desmond Hume

Không, nếu bạn lấy đi conveniencetừ khóa, bạn không thể khởi tạo Employeeđối tượng mà không có bất kỳ đối số nào.
u54r

Cụ thể, gọi Employee()các trình conveniencekhởi tạo (kế thừa, do ) init(), mà gọi self.init(name: "Unknown"). init(name: String), cũng là một trình khởi tạo tiện lợi cho Employee, gọi trình khởi tạo được chỉ định.
BallpointBen

1

Ngoài những điểm người dùng khác đã giải thích ở đây là chút hiểu biết của tôi.

Tôi cảm thấy mạnh mẽ sự kết nối giữa tiện ích khởi tạo và tiện ích mở rộng. Đối với tôi, trình khởi tạo tiện lợi là hữu ích nhất khi tôi muốn sửa đổi (trong hầu hết các trường hợp làm cho việc khởi tạo ngắn hoặc dễ dàng) của một lớp hiện có.

Ví dụ, một số lớp bên thứ ba mà bạn sử dụng có initbốn tham số nhưng trong ứng dụng của bạn, hai lớp cuối cùng có cùng giá trị. Để tránh gõ nhiều hơn và làm cho mã của bạn sạch sẽ, bạn có thể xác định một convenience initchỉ có hai tham số và bên trong nó gọi self.initcuối cùng với các tham số có giá trị mặc định.


1
Tại sao Swift buộc tôi phải đặt conveniencetrước trình khởi chạy của mình chỉ vì tôi cần gọi self.inittừ nó? Điều này có vẻ dư thừa và hơi bất tiện.
Desmond Hume

1

Theo tài liệu Swift 2.1 , người conveniencekhởi tạo phải tuân thủ một số quy tắc cụ thể:

  1. Một trình conveniencekhởi tạo chỉ có thể gọi các intializer trong cùng một lớp, không phải trong các siêu lớp (chỉ ngang, không lên)

  2. Trình conveniencekhởi tạo phải gọi trình khởi tạo được chỉ định ở đâu đó trong chuỗi

  3. Trình conveniencekhởi tạo không thể thay đổi BẤT K property thuộc tính nào trước khi nó gọi một trình khởi tạo khác - trong khi đó trình khởi tạo được chỉ định phải khởi tạo các thuộc tính được giới thiệu bởi lớp hiện tại trước khi gọi trình khởi tạo khác.

Bằng cách sử dụng conveniencetừ khóa, trình biên dịch Swift biết rằng nó phải kiểm tra các điều kiện này - nếu không thì không thể.


Có thể cho rằng, trình biên dịch có thể sắp xếp thứ này mà không cần conveniencetừ khóa.
nhgrif

Hơn nữa, điểm thứ ba của bạn là sai lệch. Trình khởi tạo tiện lợi chỉ có thể thay đổi thuộc tính (và không thể thay đổi letthuộc tính). Nó không thể khởi tạo thuộc tính. Công cụ khởi tạo được chỉ định có trách nhiệm khởi tạo tất cả các thuộc tính được giới thiệu trước khi gọi tới công cụ superkhởi tạo được chỉ định.
nhgrif

1
Ít nhất là từ khóa tiện lợi cho nhà phát triển thấy rõ, nó cũng dễ đọc (cộng với việc kiểm tra trình khởi tạo so với mong đợi của nhà phát triển). Điểm thứ hai của bạn là một điểm tốt, tôi đã thay đổi câu trả lời của tôi cho phù hợp.
TheEye

1

Một lớp có thể có nhiều hơn một bộ khởi tạo được chỉ định. Trình khởi tạo tiện lợi là trình khởi tạo thứ cấp phải gọi trình khởi tạo được chỉ định cùng loại.

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.