2016-12-29 3 views
3

Swift поражает, но еще не зрело, поэтому существуют некоторые ограничения для компилятора, и среди них есть общие протоколы. Общие протоколы не могут использоваться как регулярные аннотации типов из-за соображений безопасности. Я нашел обходное решение на посту Гектора Матоса. Generic Protocols & Their ShortcomingsКак скомпилировать несколько стираемых модулей в swift?

Основная идея - стирание типа использования для преобразования общего протокола в общий класс, и это круто. Но я застрял при применении этой технологии к более сложным сценариям.

Предположим, что существует абстрактный источник, который производит данные, и абстрактную процедуру, которая обрабатывает эти данные и конвейер, который объединяет источник и процедуру, типы данных которой сопоставляются.

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 появляется при внедрении трубопровода

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

Ух, На самом деле это работает! Я что, шучу? №

Я не удовлетворен этим, потому что initializer из PipelineImpl довольно общий, поэтому я хочу, чтобы он был в протоколе (я не прав с этой навязчивой идеей?). И это приводит к двум конца:

  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(). Как следствие, это также означает, что (на данный момент, по крайней мере) вам не нужны стирания AnySource или AnyProcedure.

Простая реализация этой оболочки будет:

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'. Тем не менее, подход Хеймиша по-прежнему работает в этом случае, я думаю. Благодаря! –

+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)', он попадет в мертвый цикл. Я этого не осознавал. –

Смежные вопросы