Metode utama dalam ekstensi Swift

133

Saya cenderung hanya menempatkan kebutuhan (properti yang disimpan, inisialisasi) ke dalam definisi kelas saya dan memindahkan semua yang lain ke dalam mereka sendiri extension, semacam seperti extensionper blok logis yang akan saya kelompokkan // MARK:juga.

Untuk subkelas UIView misalnya, saya akan berakhir dengan ekstensi untuk hal-hal yang berhubungan dengan tata letak, satu untuk berlangganan dan menangani acara dan sebagainya. Dalam ekstensi ini, saya pasti harus mengganti beberapa metode UIKit, misalnya layoutSubviews. Saya tidak pernah melihat ada masalah dengan pendekatan ini - sampai hari ini.

Ambil hierarki kelas ini sebagai contoh:

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()

Outputnya adalah A B C. Itu tidak masuk akal bagi saya. Saya membaca tentang ekstensi protokol yang dikirim secara statis, tetapi ini bukan protokol. Ini adalah kelas reguler, dan saya berharap pemanggilan metode secara dinamis dikirim saat runtime. Jelas panggilan pada Csetidaknya harus secara dinamis dikirim dan diproduksi C?

Jika saya menghapus warisan dari NSObjectdan membuat Ckelas root, kompiler mengeluh mengatakan declarations in extensions cannot override yet, yang sudah saya baca. Tetapi bagaimana memiliki NSObjectsebagai kelas dasar mengubah banyak hal?

Pindah kedua menimpa ke deklarasi kelas mereka menghasilkan A A Aseperti yang diharapkan, bergerak hanya B's menghasilkan A B B, bergerak hanya A' s menghasilkan C B C, yang terakhir yang membuat benar-benar tidak masuk akal bagi saya: bahkan tidak satu statis diketik untuk Amenghasilkan A-Output lagi!

Menambahkan dynamickata kunci ke definisi atau override tampaknya memberi saya perilaku yang diinginkan 'dari titik itu dalam hierarki kelas ke bawah' ...

Mari kita ubah contoh kita menjadi sesuatu yang sedikit kurang dibangun, apa yang sebenarnya membuat saya memposting pertanyaan ini:

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()

Sekarang kita dapatkan A B A. Di sini saya tidak dapat membuat layoutSubviews UIView menjadi dinamis dengan cara apa pun.

Memindahkan kedua penimpaan ke dalam deklarasi kelas mereka membuat kita A A Alagi, hanya A atau hanya B yang masih membuat kita A B A. dynamiclagi memecahkan masalah saya.

Secara teori saya bisa menambahkan dynamicsemua yang overridepernah saya lakukan tetapi saya merasa seperti melakukan sesuatu yang salah di sini.

Apakah benar salah menggunakan extensionkode pengelompokan seperti yang saya lakukan?

Christian Schnorr
sumber
Menggunakan ekstensi dengan cara ini adalah kebiasaan untuk Swift. Bahkan Apple melakukannya di perpustakaan standar.
Alexander - Reinstate Monica
1
@AMomchilov Dokumen yang Anda tautkan berbicara tentang protokol, apakah saya melewatkan sesuatu?
Christian Schnorr
Saya menduga itu adalah mekanisme yang sama yang bekerja untuk keduanya
Alexander - Reinstate Monica
3
Muncul untuk menduplikasi pengiriman Swift ke metode yang ditimpa dalam ekstensi subclass . jawaban Matt ada bug (dan dia mengutip dokumen untuk mendukungnya).
jscs

Jawaban:

229

Ekstensi tidak bisa / tidak boleh diganti.

Tidak mungkin untuk menimpa fungsionalitas (seperti properti atau metode) dalam ekstensi seperti yang didokumentasikan dalam Panduan Swift Apple.

Ekstensi dapat menambahkan fungsionalitas baru ke suatu tipe, tetapi ekstensi tidak dapat menimpa fungsionalitas yang ada.

Panduan Pengembang Swift

Kompiler memungkinkan Anda mengganti ekstensi untuk kompatibilitas dengan Objective-C. Tapi itu sebenarnya melanggar arahan bahasa.

😊Itu mengingatkan saya pada " Tiga Hukum Robot " karya Isaac Asimov 🤖

Ekstensi ( gula sintaksis ) mendefinisikan metode independen yang menerima argumen mereka sendiri. Fungsi yang dipanggil yaitu layoutSubviewstergantung pada konteks yang diketahui kompiler ketika kode dikompilasi. UIView mewarisi dari UIResponder yang mewarisi dari NSObject sehingga penggantian dalam ekstensi diizinkan tetapi tidak boleh .

Jadi tidak ada yang salah dengan pengelompokan tetapi Anda harus menimpa di kelas bukan di ekstensi.

Catatan Petunjuk

Anda hanya dapat overridemenggunakan metode superclass yaitu load() initialize()dalam ekstensi subclass jika metode ini kompatibel dengan Objective-C.

Oleh karena itu kita dapat melihat mengapa ini memungkinkan Anda untuk mengkompilasi menggunakan layoutSubviews.

Semua aplikasi Swift mengeksekusi di dalam runtime Objective-C kecuali untuk saat menggunakan kerangka kerja Swift-only murni yang memungkinkan untuk runtime hanya Swift.

Seperti yang kami temukan runtime Objective-C umumnya memanggil dua metode utama kelas load()dan initialize()secara otomatis ketika menginisialisasi kelas dalam proses aplikasi Anda.

Mengenai dynamicpengubah

Dari Perpustakaan Pengembang Apple (archive.org)

Anda dapat menggunakan dynamicpengubah untuk meminta agar akses ke anggota dikirimkan secara dinamis melalui runtime Objective-C.

Ketika Swift API diimpor oleh runtime Objective-C, tidak ada jaminan pengiriman dinamis untuk properti, metode, subskrip, atau inisialisasi. Kompilator Swift mungkin masih dapat devirtualize atau akses anggota sebaris untuk mengoptimalkan kinerja kode Anda, melewati runtime Objective-C. 😳

Jadi dynamicdapat diterapkan ke layoutSubviews-> Anda UIView Classkarena diwakili oleh Objective-C dan akses ke anggota tersebut selalu digunakan menggunakan runtime Objective-C.

Itu sebabnya kompiler memungkinkan Anda untuk menggunakan overridedan dynamic.

Edison
sumber
6
ekstensi tidak bisa menimpa hanya metode yang didefinisikan di kelas. Itu dapat menggantikan metode yang didefinisikan dalam kelas induk.
RJE
-Swift3-Yah, ini aneh, karena Anda juga dapat mengganti (dan menimpanya di sini, maksud saya seperti Swizzling) metode dari kerangka yang Anda sertakan. bahkan jika kerangka itu ditulis dengan sangat cepat .... mungkin kerangka kerja juga terikat ke objc dan itulah mengapa why
farzadshbfn
@tymac saya tidak mengerti. Jika runtime Objective-C membutuhkan sesuatu demi kompatibilitas Objective-C, mengapa kompiler Swift masih mengizinkan override dalam ekstensi? Bagaimana menandai penggantian pada ekstensi Swift sebagai kesalahan sintaksis dapat membahayakan runtime Objective-C?
Alexander Vasenin
1
Sangat menyebalkan, jadi pada dasarnya ketika Anda ingin membuat kerangka kerja dengan kode yang sudah ada dalam sebuah proyek, Anda harus men-subclass dan mengganti nama semuanya ...
thibaut noah
3
@AuRis Apakah Anda punya referensi?
ricardopereira
18

Salah satu tujuan Swift adalah pengiriman statis, atau lebih tepatnya pengurangan pengiriman dinamis. Namun Obj-C adalah bahasa yang sangat dinamis. Situasi yang Anda lihat berasal dari hubungan antara 2 bahasa dan cara mereka bekerja sama. Seharusnya tidak benar-benar kompilasi.

Salah satu poin utama tentang ekstensi adalah ekstensi, bukan untuk penggantian / penggantian. Jelas dari nama dan dokumentasi bahwa ini adalah niatnya. Memang jika Anda mengambil tautan ke Obj-C dari kode Anda (hapus NSObjectsebagai superclass) itu tidak akan dikompilasi.

Jadi, kompiler sedang mencoba untuk memutuskan apa yang dapat dikirim secara statis dan apa yang harus dikirim secara dinamis, dan itu jatuh melalui celah karena tautan Obj-C dalam kode Anda. Alasan dynamic'berhasil' adalah karena memaksa Obj-C menghubungkan semua hal sehingga semuanya selalu dinamis.

Jadi, tidak salah menggunakan ekstensi untuk pengelompokan, itu bagus, tetapi salah untuk menimpa ekstensi. Timpa apa pun harus di kelas utama itu sendiri, dan memanggil poin ekstensi.

Gerbong
sumber
Ini juga berlaku untuk variabel? Misalnya, jika Anda ingin menimpa supportedInterfaceOrientationsdi UINavigationController(untuk tujuan menunjukkan pandangan yang berbeda dalam orientasi yang berbeda), Anda harus menggunakan kelas kustom bukan perpanjangan? Banyak jawaban menyarankan menggunakan ekstensi untuk mengganti supportedInterfaceOrientationstetapi akan senang klarifikasi. Terima kasih!
Crashalot
10

Ada cara untuk mencapai pemisahan yang bersih dari tanda tangan kelas dan implementasi (dalam ekstensi) sambil mempertahankan kemampuan untuk menimpa subclass. Caranya adalah dengan menggunakan variabel di tempat fungsi

Jika Anda memastikan untuk menentukan setiap subclass dalam file sumber swift yang terpisah, Anda dapat menggunakan variabel yang dikomputasi untuk penggantian sambil menjaga implementasi terkait terorganisir rapi dalam ekstensi. Ini akan menghindari "aturan" Swift dan akan membuat API / tanda tangan kelas Anda diatur dengan rapi di satu tempat:

// ---------- 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))" 
   }
}

Setiap ekstensi kelas dapat menggunakan nama metode yang sama untuk implementasi karena mereka bersifat pribadi dan tidak terlihat satu sama lain (selama mereka berada di file yang terpisah).

Seperti yang Anda lihat pewarisan (menggunakan nama variabel) berfungsi dengan baik menggunakan 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"
Alain T.
sumber
2
Saya kira itu akan bekerja untuk metode saya sendiri, tetapi tidak ketika mengganti metode Kerangka Sistem di kelas saya.
Christian Schnorr
Ini membawa saya ke jalan yang benar untuk ekstensi protokol kondisional wrapper properti. Terima kasih!
Chris Prince
1

Jawaban ini tidak ditujukan pada OP, selain dari fakta bahwa saya merasa terinspirasi untuk menanggapi dengan pernyataannya, "Saya cenderung hanya menempatkan kebutuhan (properti yang disimpan, inisialisasi) ke dalam definisi kelas saya dan memindahkan semua yang lain ke ekstensi mereka sendiri. .. " Saya terutama seorang programmer C #, dan di C # satu dapat menggunakan kelas parsial untuk tujuan ini. Misalnya, Visual Studio menempatkan hal-hal terkait UI dalam file sumber terpisah menggunakan kelas parsial, dan membiarkan file sumber utama Anda tidak berantakan sehingga Anda tidak memiliki gangguan itu.

Jika Anda mencari "kelas parsial swift" Anda akan menemukan berbagai tautan di mana pengikut Swift mengatakan bahwa Swift tidak memerlukan kelas parsial karena Anda dapat menggunakan ekstensi. Menariknya, jika Anda mengetik "ekstensi cepat" ke dalam bidang pencarian Google, saran pencarian pertamanya adalah "penggantian ekstensi cepat", dan saat ini pertanyaan Stack Overflow ini adalah hit pertama. Saya menganggap itu berarti bahwa masalah dengan (kurangnya) kemampuan menimpa adalah topik yang paling dicari terkait dengan ekstensi Swift, dan menyoroti fakta bahwa ekstensi Swift tidak dapat mengganti kelas parsial, setidaknya jika Anda menggunakan kelas turunan dalam pemrograman.

Lagi pula, untuk mempersingkat pengantar yang bertele-tele, saya mengalami masalah ini dalam situasi di mana saya ingin memindahkan beberapa metode boilerplate / bagasi dari file sumber utama untuk kelas Swift yang dihasilkan oleh program C # -to-Swift saya. Setelah mengalami masalah tidak ada penggantian yang diizinkan untuk metode ini setelah memindahkannya ke ekstensi, saya akhirnya menerapkan solusi sederhana berikut ini. File sumber Swift utama masih mengandung beberapa metode rintisan kecil yang memanggil metode nyata dalam file ekstensi, dan metode ekstensi ini diberi nama unik untuk menghindari masalah penggantian.

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) } )
   }
}

Seperti yang saya katakan dalam pengantar saya, ini tidak benar-benar menjawab pertanyaan OP, tapi saya berharap solusi sederhana ini dapat membantu orang lain yang ingin memindahkan metode dari file sumber utama ke file ekstensi dan menjalankan no. -mengatasi masalah.

RenniePet
sumber
1

Gunakan POP (Pemrograman Berorientasi Protokol) untuk mengabaikan fungsi dalam ekstensi.

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()
Musim dingin
sumber
1
Ini mengasumsikan bahwa programmer mengendalikan definisi asli AClass sehingga dapat mengandalkan AProtocol. Dalam situasi di mana seseorang ingin menimpa fungsionalitas dalam AClass, ini biasanya tidak terjadi (yaitu, AClass kemungkinan akan menjadi kelas perpustakaan standar yang disediakan oleh Apple).
Jonathan Leonard
Catatan, Anda dapat (dalam beberapa kasus) menerapkan protokol dalam ekstensi atau subkelas jika Anda tidak ingin atau tidak dapat mengubah definisi asli kelas.
shim