การลบล้างวิธีการในส่วนขยาย Swift


133

ฉันมักจะใส่เฉพาะความจำเป็น (คุณสมบัติที่เก็บไว้, ตัวเริ่มต้น) ลงในคำจำกัดความของคลาสของฉันและย้ายทุกสิ่งทุกอย่างไปเป็นของตัวเองextensionซึ่งเหมือนกับextensionบล็อกตรรกะที่ฉันจะจัดกลุ่มด้วย// MARK:เช่นกัน

สำหรับคลาสย่อย UIView ฉันจะปิดท้ายด้วยส่วนขยายสำหรับสิ่งที่เกี่ยวข้องกับการจัดวางส่วนหนึ่งสำหรับการสมัครรับข้อมูลและจัดการเหตุการณ์และอื่น ๆ ในส่วนขยายเหล่านี้ผมอย่างหลีกเลี่ยงไม่ได้ที่จะแทนที่วิธี UIKit layoutSubviewsบางส่วนเช่น ฉันไม่เคยสังเกตเห็นปัญหาใด ๆ เกี่ยวกับแนวทางนี้ - จนถึงวันนี้

ใช้ลำดับชั้นของคลาสนี้เช่น:

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

ผลลัพธ์คือA B C. ที่ไม่ค่อยมีเหตุผลสำหรับฉัน ฉันอ่านเกี่ยวกับ Protocol Extensions ที่ถูกส่งแบบคงที่ แต่นี่ไม่ใช่โปรโตคอล นี่เป็นคลาสปกติและฉันคาดว่าการเรียกเมธอดจะถูกส่งแบบไดนามิกที่รันไทม์ เห็นได้ชัดว่าการโทรCอย่างน้อยควรมีการส่งและผลิตแบบไดนามิกCหรือไม่?

ถ้าฉันลบการสืบทอดNSObjectและสร้างCคลาสรูทคอมไพเลอร์จะบ่นว่าdeclarations in extensions cannot override yetซึ่งฉันได้อ่านไปแล้ว แต่การมีNSObjectคลาสรูทเปลี่ยนสิ่งต่าง ๆ อย่างไร?

การย้ายการลบล้างทั้งสองไปสู่การประกาศคลาสของพวกเขาก่อให้เกิดA A Aตามที่คาดไว้การย้ายเฉพาะการBผลิตการA B Bย้ายเพียงการAสร้างC B Cเท่านั้นซึ่งสุดท้ายก็ไม่สมเหตุสมผลสำหรับฉัน: แม้แต่การพิมพ์แบบคงที่เพื่อAสร้างAเอาต์พุตอีกต่อไป!

การเพิ่มdynamicคีย์เวิร์ดในนิยามหรือการแทนที่ดูเหมือนจะทำให้ฉันมีพฤติกรรมที่ต้องการ 'จากจุดนั้นในลำดับชั้นลงไป' ...

ลองเปลี่ยนตัวอย่างของเราเป็นสิ่งที่สร้างน้อยลงสิ่งที่ทำให้ฉันโพสต์คำถามนี้:

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

A B Aตอนนี้เราได้รับ ที่นี่ฉันไม่สามารถสร้าง layoutSubviews แบบไดนามิกของ UIView ได้ด้วยวิธีใด ๆ

ย้ายทั้งแทนที่เข้าสู่การประกาศคลาสของพวกเขาได้รับเราA A Aอีกครั้งเพียงเท่านั้นหรือ B A B Aยังคงได้รับเรา dynamicแก้ปัญหาของฉันได้อีกครั้ง

ตามทฤษฎีแล้วฉันสามารถเพิ่มdynamicทุกสิ่งoverrideที่ฉันเคยทำ แต่ฉันรู้สึกว่าฉันทำอะไรผิดพลาดที่นี่

การใช้extensions ในการจัดกลุ่มโค้ดอย่างที่ฉันทำผิดจริงๆหรือ?


การใช้ส่วนขยายด้วยวิธีนี้เป็นแบบแผนสำหรับ Swift แม้แต่ Apple ก็ทำในไลบรารีมาตรฐาน
Alexander - คืนสถานะ Monica


1
@AMomchilov เอกสารที่คุณเชื่อมโยงพูดถึงโปรโตคอลฉันขาดอะไรไปหรือเปล่า?
Christian Schnorr

ฉันสงสัยว่ามันเป็นกลไกเดียวกันที่ใช้ได้กับทั้งสอง
Alexander - Reinstate Monica

3
ดูเหมือนจะซ้ำSwift จัดส่งวิธีการแทนที่ในส่วนขยาย คำตอบของ Matt ก็คือมันเป็นจุดบกพร่อง (และเขาอ้างถึงเอกสารเพื่อสนับสนุนสิ่งนั้น)
jscs

คำตอบ:


229

ส่วนขยายไม่สามารถ / ไม่ควรแทนที่

ไม่สามารถแทนที่ฟังก์ชัน (เช่นคุณสมบัติหรือวิธีการ) ในส่วนขยายตามที่ระบุไว้ใน Swift Guide ของ Apple

ส่วนขยายสามารถเพิ่มฟังก์ชันการทำงานใหม่ให้กับประเภทได้ แต่ไม่สามารถแทนที่ฟังก์ชันที่มีอยู่ได้

คู่มือสำหรับนักพัฒนา Swift

คอมไพเลอร์ช่วยให้คุณสามารถแทนที่ส่วนขยายเพื่อให้เข้ากันได้กับ Objective-C แต่มันละเมิดคำสั่งภาษาจริงๆ

😊นั่นทำให้ฉันนึกถึง " Three Laws of Robotics " ของ Isaac Asimov 🤖

ส่วนขยาย ( น้ำตาลวากยสัมพันธ์ ) กำหนดวิธีการอิสระที่รับอาร์กิวเมนต์ของตนเอง ฟังก์ชันที่ถูกเรียกใช้เช่นlayoutSubviewsขึ้นอยู่กับบริบทที่คอมไพเลอร์ทราบเมื่อคอมไพล์โค้ด สืบทอดจาก UIView UIResponder ซึ่งสืบทอดจาก NSObject เพื่อแทนที่ในส่วนขยายจะได้รับอนุญาต แต่ไม่ควรจะเป็น

ดังนั้นจึงไม่มีอะไรผิดปกติกับการจัดกลุ่ม แต่คุณควรแทนที่ในชั้นเรียนที่ไม่ได้อยู่ในส่วนขยาย

หมายเหตุคำสั่ง

คุณสามารถทำได้เฉพาะoverrideเมธอด superclass เช่นload() initialize()ในส่วนขยายของคลาสย่อยถ้าเมธอดนั้นเข้ากันได้กับ Objective-C

ดังนั้นเราสามารถดูว่าทำไมจึงอนุญาตให้คุณรวบรวมโดยใช้layoutSubviewsไฟล์.

แอพ Swift ทั้งหมดทำงานภายในรันไทม์ Objective-C ยกเว้นเมื่อใช้เฟรมเวิร์ก Swift เท่านั้นที่อนุญาตให้ใช้รันไทม์ Swift เท่านั้น

ดังที่เราพบว่ารันไทม์ Objective-C โดยทั่วไปเรียกเมธอดหลักสองคลาสload()และinitialize()โดยอัตโนมัติเมื่อเริ่มต้นคลาสในกระบวนการของแอพของคุณ

เกี่ยวกับdynamicตัวปรับแต่ง

จากApple Developer Library (archive.org)

คุณสามารถใช้โมดิdynamicฟายเออร์เพื่อกำหนดให้การเข้าถึงสมาชิกถูกส่งแบบไดนามิกผ่านรันไทม์ Objective-C

เมื่อ Swift API ถูกนำเข้าโดยรันไทม์ Objective-C จะไม่มีการรับประกันการจัดส่งแบบไดนามิกสำหรับคุณสมบัติวิธีการตัวห้อยหรือตัวเริ่มต้น คอมไพเลอร์ Swift อาจยังคงเบี่ยงเบนหรือเข้าถึงสมาชิกแบบอินไลน์เพื่อเพิ่มประสิทธิภาพการทำงานของโค้ดของคุณโดยข้ามรันไทม์ Objective-C 😳

ดังนั้นdynamicสามารถนำไปใช้กับlayoutSubviews-> ของคุณได้UIView Classเนื่องจากมันแสดงโดย Objective-C และการเข้าถึงสมาชิกนั้นจะถูกใช้โดยใช้รันไทม์ Objective-C เสมอ

นั่นเป็นเหตุผลที่คอมไพเลอร์อนุญาตให้คุณใช้overrideและdynamic.


6
ส่วนขยายไม่สามารถแทนที่เฉพาะเมธอดที่กำหนดไว้ในคลาส มันสามารถแทนที่เมธอดที่กำหนดไว้ในคลาสพาเรนต์
RJE

-Swift3- มันแปลกเพราะคุณสามารถลบล้าง (และโดยการแทนที่ที่นี่ฉันหมายถึงวิธีการ Swizzling) จากกรอบงานที่คุณรวมไว้ แม้ว่าเฟรมเวิร์กเหล่านั้นจะถูกเขียนด้วยความรวดเร็วอย่างแท้จริง .... เฟรมเวิร์กอาจถูก จำกัด ไว้ที่ objc ด้วยและนั่นคือเหตุผลว่าทำไม🤔
farzadshbfn

@tymac ฉันไม่เข้าใจ หากรันไทม์ Objective-Cต้องการบางอย่างเพื่อประโยชน์ในการใช้งานร่วมกันได้ของ Objective-C เหตุใดคอมไพเลอร์ Swiftจึงยังคงอนุญาตให้แทนที่ในส่วนขยายได้ การทำเครื่องหมายการแทนที่ในส่วนขยาย Swift เป็นข้อผิดพลาดทางไวยากรณ์อาจเป็นอันตรายต่อรันไทม์ Objective-C ได้อย่างไร
Alexander Vasenin

1
น่าผิดหวังมากดังนั้นโดยพื้นฐานแล้วเมื่อคุณต้องการสร้างเฟรมเวิร์กด้วยโค้ดที่มีอยู่แล้วในโปรเจ็กต์คุณจะต้องซับคลาสและเปลี่ยนชื่อทุกอย่าง ...
thibaut noah

3
@AuRis คุณมีข้อมูลอ้างอิงหรือไม่?
ricardopereira

18

หนึ่งในเป้าหมายของ Swift คือการจัดส่งแบบคงที่หรือการลดการจัดส่งแบบไดนามิก Obj-C เป็นภาษาที่มีพลวัตมาก สถานการณ์ที่คุณเห็นนั้นเกิดจากความเชื่อมโยงระหว่าง 2 ภาษาและวิธีการทำงานร่วมกัน มันไม่ควรรวบรวมจริงๆ

ประเด็นหลักอย่างหนึ่งเกี่ยวกับส่วนขยายคือส่วนขยายไม่ใช่เพื่อการแทนที่ / การลบล้าง ชัดเจนจากทั้งชื่อและเอกสารว่านี่คือเจตนา แน่นอนว่าถ้าคุณนำลิงค์ไปยัง Obj-C ออกจากโค้ดของคุณ (ลบออกNSObjectเป็น superclass) มันจะไม่คอมไพล์

ดังนั้นคอมไพเลอร์จึงพยายามตัดสินใจว่าจะจัดส่งแบบคงที่และสิ่งที่ต้องจัดส่งแบบไดนามิกและมันตกลงไปในช่องว่างเนื่องจากลิงก์ Obj-C ในโค้ดของคุณ เหตุผลที่dynamic'ใช้งานได้' เป็นเพราะมันบังคับให้เชื่อมโยงกับ Obj-C กับทุกสิ่งดังนั้นจึงเป็นแบบไดนามิกเสมอ

ดังนั้นจึงไม่ใช่เรื่องผิดที่จะใช้ส่วนขยายในการจัดกลุ่มซึ่งเป็นเรื่องที่ดี แต่การแทนที่ส่วนขยายนั้นผิด การลบล้างใด ๆ ควรอยู่ในคลาสหลักและเรียกไปยังจุดต่อขยาย


นอกจากนี้ยังใช้กับตัวแปร? ตัวอย่างเช่นถ้าคุณต้องการที่จะแทนที่supportedInterfaceOrientationsในUINavigationController(สำหรับวัตถุประสงค์ในการแสดงมุมมองที่แตกต่างกันในทิศทางที่แตกต่างกัน) คุณควรใช้ชั้นเองไม่ได้เป็นส่วนขยาย? หลายคำตอบแนะนำให้ใช้ส่วนขยายเพื่อลบล้างsupportedInterfaceOrientationsแต่ชอบคำชี้แจง ขอบคุณ!
Crashalot

10

มีวิธีในการแยกลายเซ็นคลาสและการนำไปใช้อย่างชัดเจน (ในส่วนขยาย) ในขณะที่รักษาความสามารถในการลบล้างในคลาสย่อย เคล็ดลับคือการใช้ตัวแปรแทนฟังก์ชัน

หากคุณแน่ใจว่าได้กำหนดคลาสย่อยแต่ละคลาสในไฟล์ซอร์สที่รวดเร็วแยกกันคุณสามารถใช้ตัวแปรที่คำนวณสำหรับการลบล้างในขณะที่ทำให้การใช้งานที่เกี่ยวข้องจัดระเบียบในส่วนขยายได้อย่างหมดจด สิ่งนี้จะหลีกเลี่ยง "กฎ" ของ Swift และจะทำให้ API / ลายเซ็นของชั้นเรียนของคุณถูกจัดระเบียบอย่างเรียบร้อยในที่เดียว:

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

ส่วนขยายของแต่ละคลาสสามารถใช้ชื่อเมธอดเดียวกันสำหรับการนำไปใช้งานได้เนื่องจากเป็นแบบส่วนตัวและมองไม่เห็นซึ่งกันและกัน (ตราบใดที่อยู่ในไฟล์แยกกัน)

ดังที่คุณเห็นการสืบทอด (โดยใช้ชื่อตัวแปร) ทำงานได้อย่างถูกต้องโดยใช้ 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
ฉันเดาว่าจะใช้ได้กับวิธีการของฉันเอง แต่ไม่ใช่เมื่อแทนที่เมธอด System Framework ในคลาสของฉัน
Christian Schnorr

สิ่งนี้ทำให้ฉันไปสู่เส้นทางที่ถูกต้องสำหรับส่วนขยายโปรโตคอลตามเงื่อนไขของ property wrapper ขอบคุณ!
Chris Prince

1

คำตอบนี้ไม่ได้มุ่งเป้าไปที่ OP นอกเหนือจากความจริงที่ว่าฉันรู้สึกได้รับแรงบันดาลใจในการตอบสนองจากคำพูดของเขา "ฉันมักจะใส่เฉพาะความจำเป็น (คุณสมบัติที่เก็บไว้, ตัวเริ่มต้น) ลงในคำจำกัดความของคลาสของฉันและย้ายทุกสิ่งทุกอย่างไปเป็นส่วนขยายของตัวเอง .. " ฉันเป็นโปรแกรมเมอร์ C # เป็นหลักและใน C # หนึ่งสามารถใช้คลาสบางส่วนเพื่อจุดประสงค์นี้ได้ ตัวอย่างเช่น Visual Studio จะวางสิ่งที่เกี่ยวข้องกับ UI ไว้ในซอร์สไฟล์แยกต่างหากโดยใช้คลาสบางส่วนและปล่อยให้ไฟล์ต้นฉบับหลักของคุณไม่กระจายเพื่อให้คุณไม่มีสิ่งที่ทำให้ไขว้เขว

หากคุณค้นหา "swift partial class" คุณจะพบลิงก์ต่างๆที่ผู้เข้าร่วม Swift กล่าวว่า Swift ไม่ต้องการคลาสบางส่วนเนื่องจากคุณสามารถใช้ส่วนขยายได้ ที่น่าสนใจคือหากคุณพิมพ์ "ส่วนขยายที่รวดเร็ว" ลงในช่องค้นหาของ Google คำแนะนำการค้นหาแรกคือ "แทนที่ส่วนขยายที่รวดเร็ว" และในขณะนี้คำถาม Stack Overflow นี้เป็นคำถามแรก ฉันคิดว่านั่นหมายความว่าปัญหาเกี่ยวกับความสามารถในการลบล้าง (ขาด) เป็นหัวข้อที่มีการค้นหามากที่สุดที่เกี่ยวข้องกับส่วนขยายของ Swift และเน้นย้ำความจริงที่ว่าส่วนขยาย Swift ไม่สามารถแทนที่คลาสบางส่วนได้อย่างน้อยถ้าคุณใช้คลาสที่ได้รับมาใน การเขียนโปรแกรม

อย่างไรก็ตามเพื่อตัดการแนะนำสั้น ๆ ที่ยืดยาวฉันพบปัญหานี้ในสถานการณ์ที่ฉันต้องการย้ายวิธีการสำเร็จรูป / สัมภาระออกจากไฟล์ต้นทางหลักสำหรับคลาส Swift ที่โปรแกรม C #-to-Swift ของฉันสร้างขึ้น หลังจากพบปัญหาที่ไม่อนุญาตให้มีการแทนที่สำหรับวิธีการเหล่านี้หลังจากย้ายไปเป็นส่วนขยายแล้วฉันก็ใช้วิธีแก้ปัญหาง่ายๆดังต่อไปนี้ ไฟล์ต้นฉบับ Swift หลักยังคงมีวิธีการต้นขั้วเล็ก ๆ ที่เรียกวิธีการจริงในไฟล์ส่วนขยายและวิธีการขยายเหล่านี้จะได้รับชื่อที่ไม่ซ้ำกันเพื่อหลีกเลี่ยงปัญหาการแทนที่

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

อย่างที่ฉันพูดไว้ในบทนำสิ่งนี้ไม่ได้ตอบคำถามของ OP แต่ฉันหวังว่าวิธีแก้ปัญหาที่เข้าใจง่ายนี้อาจเป็นประโยชน์สำหรับผู้อื่นที่ต้องการย้ายวิธีการจากไฟล์ต้นฉบับไปยังไฟล์นามสกุลและเข้าสู่ no - แก้ไขปัญหา


1

ใช้ POP (Protocol-Oriented Programming) เพื่อแทนที่ฟังก์ชันในส่วนขยาย

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
สิ่งนี้ถือว่าโปรแกรมเมอร์อยู่ในการควบคุมคำจำกัดความดั้งเดิมของ AClass ซึ่งสามารถพึ่งพา AProtocol ได้ ในสถานการณ์ที่ต้องการลบล้างฟังก์ชันการทำงานใน AClass โดยทั่วไปจะไม่เป็นเช่นนั้น (กล่าวคือ AClass น่าจะเป็นคลาสไลบรารีมาตรฐานที่ Apple จัดหาให้)
Jonathan Leonard

หมายเหตุคุณสามารถ (ในบางกรณี) ใช้โปรโตคอลในส่วนขยายหรือคลาสย่อยหากคุณไม่ต้องการหรือไม่สามารถแก้ไขนิยามดั้งเดิมของคลาสได้
ชิม
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.