2016-12-29 44 views
3

Swift是驚人的,但還不成熟,所以有一些編譯器限制,其中包括通用協議。由於類型安全考慮,通用協議不能用作常規類型註釋。我在Hector Matos的帖子中發現了一個解決方法。 Generic Protocols & Their Shortcomings如何在swift中編寫多個刪除類型的模塊?

主要思想是使用類型擦除將泛型協議轉換爲泛型類,它很酷。但是在將這種技術應用於更復雜的場景時,我陷入了困境。

假設有一個抽象的Source產生數據,一個抽象的Procedure處理這些數據,一個流水線組合了一個數據類型匹配的源和過程。

protocol Source { 
    associatedtype DataType 
    func newData() -> DataType 
} 
protocol Procedure { 
    associatedtype DataType 
    func process(data: DataType) 
} 
protocol Pipeline { 
    func exec() // The execution may differ 
} 

而且我想在客戶端的代碼是簡單的:

class Client { 
    private let pipeline: Pipeline 
    init(pipeline: Pipeline) { 
     self.pipeline = pipeline 
    } 
    func work() { 
     pipeline.exec() 
    } 
} 

// Assume there are two implementation of Source and Procedure, 
// SourceImpl and ProcedureImpl, whose DataType are identical. 
// And also an implementation of Pipeline -- PipelineImpl 

Client(pipeline: PipelineImpl(source: SourceImpl(), procedure: ProcedureImpl())).work() 

實現源和程序很簡單,因爲他們在的關係是不底:

class SourceImpl: Source { 
    func newData() -> Int { return 1 } 
} 

class ProcedureImpl: Procedure { 
    func process(data: Int) { print(data) } 
} 

PITA在實施Pipeline時出現

// A concrete Pipeline need to store the Source and Procedure, and they're generic protocols, so a type erasure is needed 
class AnySource<T>: Source { 
    private let _newData:() -> T 
    required init<S: Source>(_ source: S) where S.DataType == T { 
     _newData = source.newData 
    } 
    func newData() -> T { return _newData() } 
} 
class AnyProcedure<T>: Procedure { 
    // Similar to above. 
} 

class PipelineImpl<T>: Pipeline { 
    private let source: AnySource<T> 
    private let procedure: AnySource<T> 
    required init<S: Source, P: Procedure>(source: S, procedure: P) where S.DataType == T, P.DataType == T { 
     self.source = AnySource(source) 
     self.procedure = AnyProcedure(procedure) 
    } 
    func exec() { 
     procedure.process(data: source.newData()) 
    } 
} 

呃,其實這個工程! 我在開玩笑嗎?我不滿意這一個,因爲PipelineImplinitializer是相當通用的,所以我希望它在協議(我是這個癡迷錯誤?)。這導致兩端:

  1. 該協議Pipeline將是通用的。 initializer包含一個where子句,它指的是placeholder T,所以我需要將placeholder T作爲associated type移動到協議中。然後該協議變成一個通用的,這意味着我不能直接在我的客戶端代碼中使用它 - 可能需要另一種類型的擦除。

    雖然我所能承受的麻煩寫另一類型擦除爲Pipeline協議的,我不知道該如何應對initializer function因爲AnyPipeline<T>類必須實現關於該協議的初始化,但它只是一個thunk class實際上,它不應該實現任何初始化器本身。

  2. 保持協議Pipeline非通用。隨着寫initializer

    init<S: Source, P: Procedure>(source: S, procedure: P) 
    where S.DataType == P.DataType 
    

    我可以防止協議是通用的。這意味着協議只聲明「源和過程必須具有相同的數據類型,我不在乎它是什麼」。這使得更多的意義,但我沒能實現一個具體的類,確認該協議

    class PipelineImpl<T>: Protocol { 
        private let source: AnySource<T> 
        private let procedure: AnyProcedure<T> 
        init<S: Source, P: Procedure>(source: S, procedure: P) 
        where S.DataType == P.DataType { 
         self.source = AnySource(source) // doesn't compile, because S is nothing to do with T 
         self.procedure = AnyProcedure(procedure) // doesn't compile as well 
        } 
        // If I add S.DataType == T, P.DataType == T condition to where clasue, 
        // the initializer won't confirm to the protocol and the compiler will complain as well 
    } 
    

所以,我怎麼能解決這個問題?

感謝您閱讀allll這個。

回答

1

我覺得你過於複雜有所這個(除非我失去了一些東西) - 你的PipelineImpl不會出現比對功能的包裝,從Source取數據,並將其傳遞給更多的東西Procedure

因此,它不需要是通用的,因爲外界不需要知道傳遞的數據類型 - 它只需要知道它可以調用exec()。因此,這也意味着(現在至少)您不需要AnySourceAnyProcedure類型的刪除。

一個簡單的實現這個包裝的是:

struct PipelineImpl : Pipeline { 

    private let _exec :() -> Void 

    init<S : Source, P : Procedure>(source: S, procedure: P) where S.DataType == P.DataType { 
     _exec = { procedure.process(data: source.newData()) } 
    } 

    func exec() { 
     // do pre-work here (if any) 
     _exec() 
     // do post-work here (if any) 
    } 
} 

這讓你可以自由地初始化劑添加到您的Pipeline協議,因爲它不需要什麼實際DataType擔心自己 - 只有源和程序必須相同DataType

protocol Pipeline { 
    init<S : Source, P : Procedure>(source: S, procedure: P) where S.DataType == P.DataType 
    func exec() // The execution may differ 
} 
+0

好一點,但也有可能存在着一些不同的前和後的工作在不同的'exec'函數來完成。不過,我想,@Hhish的方法仍然適用於這種情況。謝謝! –

+0

@NandiinBao啊,我知道了 - 我編輯了我的答案來解釋這個。很高興能有所幫助:) – Hamish

0

@Hamish指出了一個很好的解決方案。

張貼了這個問題之後,我做了一些測試,並發現了一個解決辦法

class PipelineImpl<T>: Pipeline { 
    required init<S: Source, P: Procedure>(source: S, procedure: P) where S.DataType == T, P.DataType == T { 
     // This initializer does the real jobs. 
     self.source = AnySource(source) 
     self.procedure = AnyProcedure(procedure) 
    } 
    required convenience init<S: Source, P: Procedure>(source: S, procedure: P) where S.DataType == P.DataType { 
     // This initializer confirms to the protocol and forwards the work to the initializer above 
     self.init(source: source, procedure: procedure) 
    } 
} 
+0

你的第二個初始化程序實際上會自動調用它自己,因爲編譯器不能保證'DataType == T' - 因此'self.init'只能引用自身。 – Hamish

+0

我在Xcode操作系統中對它進行了測試,並且調用init(source:procedure :)實際上會調用第一個初始化程序,而第二個從未調用。第二個只是爲了確認協議。但是,通過這樣做,我可能會失去在協議中包含初始化程序簽名的要點... –

+0

此外,您是對的。如果我將第一個初始化程序設爲私有,並且調用類似於「PipelineImpl (source:source,procedure:procedure)」,則它將陷入死循環。我沒有意識到。 –