Các phương thức ghi đè trong tiện ích mở rộng Swift


133

Tôi có xu hướng chỉ đưa các nhu yếu phẩm (thuộc tính được lưu trữ, bộ khởi tạo) vào các định nghĩa lớp của tôi và chuyển mọi thứ khác thành của riêng chúng extension, giống như một extensionkhối logic mà tôi sẽ nhóm cùng // MARK:.

Ví dụ, đối với một lớp con UIView, tôi sẽ kết thúc bằng một phần mở rộng cho các nội dung liên quan đến bố cục, một phần để đăng ký và xử lý các sự kiện, v.v. Trong các tiện ích mở rộng này, tôi chắc chắn phải ghi đè một số phương thức UIKit, vd layoutSubviews. Tôi không bao giờ nhận thấy bất kỳ vấn đề với phương pháp này - cho đến ngày hôm nay.

Lấy hệ thống phân cấp lớp này làm ví dụ:

public class C: NSObject {
    public func method() { print("C") }
}

public class B: C {
}
extension B {
    override public func method() { print("B") }
}

public class A: B {
}
extension A {
    override public func method() { print("A") }
}

(A() as A).method()
(A() as B).method()
(A() as C).method()

Đầu ra là A B C. Điều đó làm cho rất ít ý nghĩa với tôi. Tôi đọc về Tiện ích mở rộng Giao thức được gửi tĩnh, nhưng đây không phải là giao thức. Đây là một lớp thông thường và tôi hy vọng các lệnh gọi phương thức sẽ được gửi động khi chạy. Rõ ràng cuộc gọi trên Cít nhất nên được gửi đi năng động và sản xuất C?

Nếu tôi loại bỏ sự kế thừa từ NSObjectvà tạo Cmột lớp gốc, trình biên dịch sẽ phàn nàn rằng declarations in extensions cannot override yettôi đã đọc về nó. Nhưng làm thế nào để có NSObjectmột lớp gốc thay đổi mọi thứ?

Di chuyển cả hai phần ghi đè vào khai báo lớp của chúng tạo ra A A Anhư mong đợi, chỉ di chuyển Bsản phẩm A B B, chỉ di chuyển Asản phẩm C B C, điều cuối cùng hoàn toàn không có ý nghĩa với tôi: thậm chí không phải là người được nhập tĩnh để Atạo ra thông số Anữa!

Việc thêm dynamictừ khóa vào định nghĩa hoặc ghi đè dường như mang lại cho tôi hành vi mong muốn 'từ thời điểm đó trong hệ thống phân cấp lớp trở xuống' ...

Hãy thay đổi ví dụ của chúng tôi thành một cái gì đó ít được xây dựng hơn, những gì thực sự khiến tôi đăng câu hỏi này:

public class B: UIView {
}
extension B {
    override public func layoutSubviews() { print("B") }
}

public class A: B {
}
extension A {
    override public func layoutSubviews() { print("A") }
}


(A() as A).layoutSubviews()
(A() as B).layoutSubviews()
(A() as UIView).layoutSubviews()

Bây giờ chúng tôi nhận được A B A. Ở đây tôi không thể làm cho layout của UIView trở nên linh hoạt bằng mọi cách.

Di chuyển cả hai phần ghi đè vào khai báo lớp của chúng sẽ đưa chúng ta A A Atrở lại, chỉ A hoặc chỉ B vẫn nhận được chúng ta A B A. dynamicmột lần nữa giải quyết vấn đề của tôi.

Về lý thuyết tôi có thể thêm dynamicvào tất cả những overridegì tôi từng làm nhưng tôi cảm thấy như mình đang làm gì đó sai ở đây.

Có thực sự sai khi sử dụng extensions để nhóm mã như tôi không?


Sử dụng tiện ích mở rộng theo cách này là quy ước cho Swift. Ngay cả Apple cũng làm điều đó trong thư viện tiêu chuẩn.
Alexander - Hồi phục lại


1
@AMomchilov Tài liệu bạn liên kết nói về các giao thức, tôi có thiếu thứ gì không?
Christian Schnorr

Tôi nghi ngờ đó là cùng một cơ chế hoạt động cho cả hai
Alexander - Tái lập Monica

3
Xuất hiện để nhân đôi công văn Swift đến các phương thức được ghi đè trong các phần mở rộng của lớp con . Câu trả lời của matt là có một lỗi (và anh ấy trích dẫn các tài liệu để hỗ trợ điều đó).
jscs

Câu trả lời:


229

Phần mở rộng không thể / không nên ghi đè.

Không thể ghi đè chức năng (như thuộc tính hoặc phương thức) trong các tiện ích mở rộng như được ghi trong Hướng dẫn Swift của Apple.

Tiện ích mở rộng có thể thêm chức năng mới vào một loại, nhưng chúng không thể ghi đè chức năng hiện có.

Hướng dẫn dành cho nhà phát triển Swift

Trình biên dịch đang cho phép bạn ghi đè trong phần mở rộng để tương thích với Objective-C. Nhưng nó thực sự vi phạm chỉ thị ngôn ngữ.

At Điều đó làm tôi nhớ đến " Ba định luật robot " của Isaac Asimov

Phần mở rộng ( đường cú pháp ) xác định các phương thức độc lập nhận các đối số của riêng chúng. Hàm được gọi là tức là layoutSubviewsphụ thuộc vào ngữ cảnh mà trình biên dịch biết về khi mã được biên dịch. UIView kế thừa từ UIResponder kế thừa từ NSObject nên việc ghi đè trong tiện ích mở rộng được cho phép nhưng không nên .

Vì vậy, không có gì sai khi nhóm nhưng bạn nên ghi đè trong lớp không có trong phần mở rộng.

Ghi chú chỉ thị

Bạn chỉ có thể overridemột phương thức siêu lớp tức là load() initialize()trong phần mở rộng của lớp con nếu phương thức đó tương thích với Objective-C.

Do đó, chúng tôi có thể xem lý do tại sao nó cho phép bạn biên dịch bằng cách sử dụng layoutSubviews.

Tất cả các ứng dụng Swift thực thi bên trong thời gian chạy Objective-C ngoại trừ khi sử dụng các khung chỉ dành cho Swift thuần cho phép thời gian chạy chỉ dành cho Swift.

Như chúng tôi đã tìm ra thời gian chạy Objective-C thường gọi hai phương thức chính của lớp load()initialize()tự động khi khởi tạo các lớp trong các quy trình của ứng dụng của bạn.

Về công cụ dynamicsửa đổi

Từ Thư viện nhà phát triển của Apple (archive.org)

Bạn có thể sử dụng công cụ dynamicsửa đổi để yêu cầu quyền truy cập vào các thành viên được gửi tự động thông qua thời gian chạy Objective-C.

Khi API Swift được nhập bởi thời gian chạy Objective-C, không có đảm bảo nào về công văn động cho các thuộc tính, phương thức, đăng ký hoặc bộ khởi tạo. Trình biên dịch Swift vẫn có thể truy cập sai hoặc truy cập thành viên nội tuyến để tối ưu hóa hiệu suất mã của bạn, bỏ qua thời gian chạy Objective-C. 😳

Vì vậy, dynamiccó thể được áp dụng cho layoutSubviews-> UIView Classvì nó được đại diện bởi Objective-C và quyền truy cập vào thành viên đó luôn được sử dụng bằng thời gian chạy Objective-C.

Đó là lý do tại sao trình biên dịch cho phép bạn sử dụng overridedynamic.


6
mở rộng không thể ghi đè chỉ các phương thức được xác định trong lớp. Nó có thể ghi đè các phương thức được định nghĩa trong lớp cha.
RJE

-Swift3-Chà, thật lạ, bởi vì bạn cũng có thể ghi đè (và bằng cách ghi đè ở đây tôi có nghĩa là một phương thức như Swizzling) từ các khung bạn bao gồm. ngay cả khi các khung đó được viết bằng swift thuần túy .... có thể các khung cũng bị ràng buộc với objc và đó là lý do
farzadshbfn

@tymac Mình không hiểu. Nếu thời gian chạy Objective-C cần một cái gì đó để tương thích với Objective-C, tại sao trình biên dịch Swift vẫn cho phép ghi đè trong các tiện ích mở rộng? Làm cách nào việc đánh dấu ghi đè trong tiện ích mở rộng Swift là lỗi cú pháp có thể gây hại cho thời gian chạy Objective-C?
Alexander Vasenin

1
Thật là bực bội, vì vậy về cơ bản khi bạn muốn tạo một khung với một mã đã có trong một dự án, bạn sẽ phải phân lớp và đổi tên mọi thứ ...
thibaut noah

3
@AuRis Bạn có tham khảo gì không?
ricardopereira

18

Một trong những mục tiêu của Swift là gửi tĩnh, hay đúng hơn là giảm việc gửi động. Obj-C tuy nhiên là một ngôn ngữ rất năng động. Tình huống bạn đang gặp phải xuất phát từ mối liên kết giữa 2 ngôn ngữ và cách chúng hoạt động cùng nhau. Nó không nên thực sự biên dịch.

Một trong những điểm chính về tiện ích mở rộng là chúng dành cho mở rộng, không phải để thay thế / ghi đè. Rõ ràng từ cả tên và tài liệu cho rằng đây là ý định. Thật vậy, nếu bạn lấy liên kết đến Obj-C từ mã của bạn (loại bỏ NSObjectnhư siêu lớp), nó sẽ không biên dịch.

Vì vậy, trình biên dịch đang cố gắng quyết định những gì nó có thể gửi tĩnh và những gì nó phải gửi động, và nó rơi vào một khoảng trống do liên kết Obj-C trong mã của bạn. Lý do dynamic'hoạt động' là vì nó buộc Obj-C liên kết trên mọi thứ để tất cả luôn luôn năng động.

Vì vậy, không sai khi sử dụng các tiện ích mở rộng để nhóm, điều đó thật tuyệt, nhưng việc ghi đè lên các tiện ích mở rộng là sai. Bất kỳ phần ghi đè nào cũng phải nằm trong lớp chính và gọi ra các điểm mở rộng.


Điều này cũng áp dụng cho các biến? Ví dụ, nếu bạn muốn ghi đè supportedInterfaceOrientationstrong UINavigationController(cho mục đích thể hiện quan điểm khác nhau trong định hướng khác nhau), bạn nên sử dụng một lớp tùy chỉnh không phải là một phần mở rộng? Nhiều câu trả lời đề nghị sử dụng một phần mở rộng để ghi đè supportedInterfaceOrientationsnhưng sẽ thích làm rõ. Cảm ơn!
Crashalot

10

Có một cách để đạt được sự phân tách rõ ràng giữa chữ ký lớp và triển khai (trong phần mở rộng) trong khi duy trì khả năng ghi đè trong các lớp con. Bí quyết là sử dụng các biến thay cho các hàm

Nếu bạn đảm bảo xác định từng lớp con trong một tệp nguồn swift riêng biệt, bạn có thể sử dụng các biến được tính toán cho các phần ghi đè trong khi giữ cho việc triển khai tương ứng được tổ chức sạch sẽ trong các phần mở rộng. Điều này sẽ phá vỡ "quy tắc" của Swift và sẽ làm cho API / chữ ký của lớp bạn được sắp xếp gọn gàng ở một nơi:

// ---------- BaseClass.swift -------------

public class BaseClass
{
    public var method1:(Int) -> String { return doMethod1 }

    public init() {}
}

// the extension could also be in a separate file  
extension BaseClass
{    
    private func doMethod1(param:Int) -> String { return "BaseClass \(param)" }
}

...

// ---------- ClassA.swift ----------

public class A:BaseClass
{
   override public var method1:(Int) -> String { return doMethod1 }
}

// this extension can be in a separate file but not in the same
// file as the BaseClass extension that defines its doMethod1 implementation
extension A
{
   private func doMethod1(param:Int) -> String 
   { 
      return "A \(param) added to \(super.method1(param))" 
   }
}

...

// ---------- ClassB.swift ----------
public class B:A
{
   override public var method1:(Int) -> String { return doMethod1 }
}

extension B
{
   private func doMethod1(param:Int) -> String 
   { 
      return "B \(param) added to \(super.method1(param))" 
   }
}

Mỗi phần mở rộng của mỗi lớp có thể sử dụng cùng tên phương thức để triển khai vì chúng là riêng tư và không hiển thị với nhau (miễn là chúng nằm trong các tệp riêng biệt).

Như bạn có thể thấy tính kế thừa (sử dụng tên biến) hoạt động đúng bằng super.variablename

BaseClass().method1(123)         --> "BaseClass 123"
A().method1(123)                 --> "A 123 added to BaseClass 123"
B().method1(123)                 --> "B 123 added to A 123 added to BaseClass 123"
(B() as A).method1(123)          --> "B 123 added to A 123 added to BaseClass 123"
(B() as BaseClass).method1(123)  --> "B 123 added to A 123 added to BaseClass 123"

2
Tôi đoán rằng nó sẽ hoạt động cho các phương thức của riêng tôi, nhưng không phải khi ghi đè các phương thức Khung hệ thống trong các lớp của tôi.
Christian Schnorr

Điều này dẫn tôi xuống con đường đúng cho một phần mở rộng giao thức có điều kiện của thuộc tính. Cảm ơn!
Hoàng tử Chris

1

Câu trả lời này không nhằm vào OP, ngoài thực tế là tôi cảm thấy được truyền cảm hứng để đáp lại câu nói của anh ấy, "Tôi có xu hướng chỉ đưa các nhu yếu phẩm (thuộc tính được lưu trữ, khởi tạo) vào định nghĩa lớp của tôi và chuyển mọi thứ khác vào phần mở rộng của riêng chúng. .. ". Tôi chủ yếu là một lập trình viên C # và trong C #, người ta có thể sử dụng các lớp một phần cho mục đích này. Ví dụ, Visual Studio đặt các nội dung liên quan đến giao diện người dùng vào một tệp nguồn riêng bằng cách sử dụng một phần lớp và để tệp nguồn chính của bạn không bị xáo trộn để bạn không bị phân tâm.

Nếu bạn tìm kiếm "lớp một phần nhanh chóng", bạn sẽ tìm thấy các liên kết khác nhau nơi các tín đồ của Swift nói rằng Swift không cần các lớp một phần vì bạn có thể sử dụng các tiện ích mở rộng. Thật thú vị, nếu bạn nhập "tiện ích mở rộng nhanh" vào trường tìm kiếm của Google, đề xuất tìm kiếm đầu tiên của nó là "ghi đè tiện ích mở rộng nhanh" và tại thời điểm này, câu hỏi Stack Overflow này là lần đầu tiên. Tôi hiểu điều đó có nghĩa là các vấn đề với (thiếu) khả năng ghi đè là chủ đề được tìm kiếm nhiều nhất liên quan đến tiện ích mở rộng Swift và nhấn mạnh thực tế là tiện ích mở rộng Swift không thể thay thế các lớp một phần, ít nhất là nếu bạn sử dụng các lớp dẫn xuất trong lập trình.

Dù sao, để cắt ngắn phần giới thiệu dài dòng, tôi đã gặp phải vấn đề này trong tình huống tôi muốn chuyển một số phương thức nồi hơi / hành lý ra khỏi các tệp nguồn chính cho các lớp Swift mà chương trình C # -to-Swift của tôi đang tạo. Sau khi gặp vấn đề không cho phép ghi đè cho các phương thức này sau khi chuyển chúng sang tiện ích mở rộng, tôi đã kết thúc việc thực hiện cách giải quyết đơn giản sau đây. Các tệp nguồn Swift chính vẫn chứa một số phương thức sơ khai nhỏ gọi các phương thức thực trong các tệp mở rộng và các phương thức mở rộng này được đặt tên duy nhất để tránh sự cố ghi đè.

public protocol PCopierSerializable {

   static func getFieldTable(mCopier : MCopier) -> FieldTable
   static func createObject(initTable : [Int : Any?]) -> Any
   func doSerialization(mCopier : MCopier)
}

.

public class SimpleClass : PCopierSerializable {

   public var aMember : Int32

   public init(
               aMember : Int32
              ) {
      self.aMember = aMember
   }

   public class func getFieldTable(mCopier : MCopier) -> FieldTable {
      return getFieldTable_SimpleClass(mCopier: mCopier)
   }

   public class func createObject(initTable : [Int : Any?]) -> Any {
      return createObject_SimpleClass(initTable: initTable)
   }

   public func doSerialization(mCopier : MCopier) {
      doSerialization_SimpleClass(mCopier: mCopier)
   }
}

.

extension SimpleClass {

   class func getFieldTable_SimpleClass(mCopier : MCopier) -> FieldTable {
      var fieldTable : FieldTable = [ : ]
      fieldTable[376442881] = { () in try mCopier.getInt32A() }  // aMember
      return fieldTable
   }

   class func createObject_SimpleClass(initTable : [Int : Any?]) -> Any {
      return SimpleClass(
                aMember: initTable[376442881] as! Int32
               )
   }

   func doSerialization_SimpleClass(mCopier : MCopier) {
      mCopier.writeBinaryObjectHeader(367620, 1)
      mCopier.serializeProperty(376442881, .eInt32, { () in mCopier.putInt32(aMember) } )
   }
}

.

public class DerivedClass : SimpleClass {

   public var aNewMember : Int32

   public init(
               aNewMember : Int32,
               aMember : Int32
              ) {
      self.aNewMember = aNewMember
      super.init(
                 aMember: aMember
                )
   }

   public class override func getFieldTable(mCopier : MCopier) -> FieldTable {
      return getFieldTable_DerivedClass(mCopier: mCopier)
   }

   public class override func createObject(initTable : [Int : Any?]) -> Any {
      return createObject_DerivedClass(initTable: initTable)
   }

   public override func doSerialization(mCopier : MCopier) {
      doSerialization_DerivedClass(mCopier: mCopier)
   }
}

.

extension DerivedClass {

   class func getFieldTable_DerivedClass(mCopier : MCopier) -> FieldTable {
      var fieldTable : FieldTable = [ : ]
      fieldTable[376443905] = { () in try mCopier.getInt32A() }  // aNewMember
      fieldTable[376442881] = { () in try mCopier.getInt32A() }  // aMember
      return fieldTable
   }

   class func createObject_DerivedClass(initTable : [Int : Any?]) -> Any {
      return DerivedClass(
                aNewMember: initTable[376443905] as! Int32,
                aMember: initTable[376442881] as! Int32
               )
   }

   func doSerialization_DerivedClass(mCopier : MCopier) {
      mCopier.writeBinaryObjectHeader(367621, 2)
      mCopier.serializeProperty(376443905, .eInt32, { () in mCopier.putInt32(aNewMember) } )
      mCopier.serializeProperty(376442881, .eInt32, { () in mCopier.putInt32(aMember) } )
   }
}

Như tôi đã nói trong phần giới thiệu của mình, điều này không thực sự trả lời câu hỏi của OP, nhưng tôi hy vọng cách giải quyết đơn giản này có thể hữu ích cho những người khác muốn chuyển phương thức từ tệp nguồn chính sang tệp mở rộng và chạy vào không vấn đề ghi đè.


1

Sử dụng POP (Lập trình hướng giao thức) để ghi đè các chức năng trong tiện ích mở rộng.

protocol AProtocol {
    func aFunction()
}

extension AProtocol {
    func aFunction() {
        print("empty")
    }
}

class AClass: AProtocol {

}

extension AClass {
    func aFunction() {
        print("not empty")
    }
}

let cls = AClass()
cls.aFunction()

1
Điều này giả định rằng lập trình viên kiểm soát định nghĩa ban đầu của AClass sao cho nó có thể dựa vào AProtocol. Trong trường hợp người ta muốn ghi đè chức năng trong AClass, đây thường không phải là trường hợp (ví dụ, AClass có thể sẽ là một lớp thư viện tiêu chuẩn do Apple cung cấp).
Jonathan Leonard

Lưu ý, trong một số trường hợp, bạn có thể áp dụng giao thức trong một phần mở rộng hoặc lớp con nếu bạn không muốn hoặc không thể sửa đổi định nghĩa ban đầu của lớp.
Shim
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.