Проблема с associtedtype при ссылке на метатип протокола

Вот моя проблема:

Допустим, у меня есть протокол, в котором associatedtype указывает на его метатип:

protocol TestMeta {
    associatedtype T
    
    var x : T.Type { get }
    var y : T { get }
}

Если я создам структуру с конкретным типом, нет проблем:

struct AMeta : TestMeta {
    var x : Int.Type
    var y : Int
}

Но если associatedtype относится к протоколу, у меня возникает ошибка Тип 'BMeta' не соответствует протоколу 'TestMeta':

protocol P { }

struct BMeta : TestMeta {
    var x : P.Type
    var y : P
}

(даже если я добавлю определение typealias, чтобы помочь механизму вывода)

Конечно, если я не ссылаюсь на метатип, то все работает с конкретными типами и протоколами, даже если у меня есть другие переменные метатипа, не определенные в протоколе:

protocol TestNoMeta {
    associatedtype T
    
    var z : T { get }
}

struct ANoMeta : TestNoMeta {
    var z : Int
    var t : Int.Type
}

struct BNoMeta : TestNoMeta {
    var z : P
    var t : P.Type
}

Итак, если кто-нибудь может объяснить, что я делаю неправильно? И как я могу достичь своей цели? Заранее спасибо.

EDIT: Но, как указал @New Dev, я не объяснил, что искал. Я хочу иметь возможность сделать что-то вроде этого:

struct S : P { }

let b = BMeta(x: S.self, y: S())

Зная, что все еще компилируется с протоколом NoMeta:

let bb = BNoMeta(z: S(), t: S.Type) 

РЕДАКТИРОВАТЬ 2: В конце концов я хочу сделать что-то вроде этого:

protocol P { 
    init() 
}

extension TestMeta {
    func build() -> P {
        return x.init()
    }
}

struct BMeta : TestMeta {
    var x : P.Type
    var y : P
}

struct S : P { }

let b = BMeta(x: S.self, y: S()) 
let c = b.build()

РЕДАКТИРОВАТЬ 3: Хорошо, хорошо, вот мой реальный вариант использования, я думал, что было бы лучше упростить вещи, но, похоже, нет...

protocol Initializable {
    init()
}

protocol OptionListFactory {
    associatedtype Option : Initializable
    static var availableOptions: [Option.Type] { get }
}

extension OptionListFactory {
    static func build(code: Int) -> Option? {
        if code >= 0 && code < availableOptions.count {
            return availableOptions[code].init()
        }
        else {
            return nil
        }
    }
}

protocol Contract : Initializable {
    ...
}

struct Contract01 : Contract { ... }
struct Contract02 : Contract { ... }
...
struct Contract40 : Contract { ... }

struct ContractFactory : OptionListFactory {
    static let availableOptions: [Contract.Type] = [
        Contract01.self,
        Contract02.self,
        ...        
        Contract40.self,
    ]
}

protocol Element : Initializable {
    ...
}

struct Element01 : Element { ... }
struct Element02 : Element { ... }
    ...
struct Element20 : Element { ... }

struct ElementFactory : OptionListFactory {
    static let availableOptions: [Element.Type] = [
        Element01.self,
        Element02.self,
        ...
        Element20.self,
    ]
}

Надеюсь, вы лучше поймете мою цель...


person Zaphod    schedule 15.11.2020    source источник
comment
Не уверен, чего вы на самом деле пытаетесь достичь, но чтобы это скомпилировалось, измените на var x : P.Protocol   -  person New Dev    schedule 16.11.2020
comment
К сожалению, это не означает то же самое. Например, если я создаю структуру, соответствующую P: struct S : P { } с вашим решением, я не могу сделать: let b = BMeta(x: S.self, y: S()), потому что компилятор не может преобразовать S.Type в P.Protocol   -  person Zaphod    schedule 16.11.2020
comment
Странно ссылаться на протокол как на мета для типа, мета должна быть чем-то конкретным, а не абстрактным. Зачем вам нужен этот дизайн, какова ваша конечная цель?   -  person Cristik    schedule 16.11.2020
comment
@Cristic IRL, P будет содержать init() для расширения для создания экземпляра.   -  person Zaphod    schedule 16.11.2020
comment
Я отредактировал свой ответ, упростив мой окончательный вариант использования.   -  person Zaphod    schedule 16.11.2020
comment
Почему бы не использовать дженерик здесь?   -  person New Dev    schedule 16.11.2020
comment
Очень неясно, чего вы ожидаете от Edit 2. Когда вы говорите, что я хочу сделать что-то подобное, чего вы на самом деле пытаетесь достичь? Как отмечает New Dev, это выглядит как очень простая (почти тривиальная) проблема дженериков, которую вы пытаетесь навязать протоколу. Протоколы и дженерики решают разные проблемы. Что представляет собой y? Вы, кажется, не используете его вообще. Я предполагаю, что P является более сложным протоколом, который включает init как одно требование из нескольких? В противном случае протокол P не имеет большого смысла (вы ничего не можете сделать с c в вашем примере).   -  person Rob Napier    schedule 16.11.2020
comment
Я добавил третье редактирование, чтобы точно показать мою цель   -  person Zaphod    schedule 16.11.2020
comment
Да, это редактирование действительно говорит нам о чем-то. Мне нужно подумать об этом на мгновение, но это выглядит очень просто решить.   -  person Rob Napier    schedule 16.11.2020


Ответы (1)


Это стандартные протоколы не соответствуют вопросам протоколов. Контракт — это протокол, который требует, чтобы соответствующие типы также соответствовали Initializable. Он сам по себе не может быть инициализирован. По сути, это канонический пример того, почему протоколы не могут соответствовать сами себе в самом общем случае. Если бы это было так, я мог бы написать Contract.init() и получить в ответ... что? Невозможно выделить память для абстрактного контракта.

Поскольку Contract не соответствует Initializable, он не может быть напрямую Option. Сегодня в Swift также отсутствует какой-либо механизм, позволяющий говорить о наследовании протоколов. Вы не можете сказать, что Option должен быть протоколом, требующим Initializable. Это просто за пределами системы типов Swift.

Тем не менее, это довольно негибкий подход. Это требует, чтобы каждый тип принимал тривиальное init, что очень ограничивает типы, которые могут соответствовать. Есть гораздо лучшие способы IMO реализовать этот вид фабрики.

Избавьтесь от Initializable, избавьтесь от требования static (позже вы поймете, почему) и замените тип функцией-строителем.

protocol OptionListFactory {
    associatedtype Option
    var availableOptions: [() -> Option] { get }
}

Это приводит к тому, что расширение настраивается следующим образом:

extension OptionListFactory {
    // Remove `static`
    func build(code: Int) -> Option? {
        if code >= 0 && code < availableOptions.count {
            return availableOptions[code]()  // Call the builder function
        }
        else {
            return nil
        }
    }
}

И простая фабрика:

protocol Contract {}

struct Contract01 : Contract {}
struct Contract02 : Contract {}

struct ContractFactory : OptionListFactory {
    let availableOptions: [() -> Contract] = [
        Contract01.init,   // init rather than self
        Contract02.init,
    ]
}

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

// Elements need a name
protocol Element {
    var name: String { get }
}

struct Element01 : Element { let name: String }
struct Element02 : Element { let name: String }
struct Element20 : Element { let name: String }

struct ElementFactory : OptionListFactory {
    let availableOptions: [() -> Element]

    // And now we can assign that name
    init(prefix: String) {
        availableOptions = [
            { Element01(name: prefix + "01") },
            { Element02(name: prefix + "02") },
            { Element20(name: prefix + "20") },
        ]
    }
}

let ef = ElementFactory(prefix: "local")
let e20 = ef.build(code: 2)
// e20?.name == "local20"

Делая экземпляры фабрик вместо использования static, они становятся более гибкими и могут быть настроены. А используя функции, вы можете создавать композиции способами, о которых вы, возможно, и не подозревали. Например, вы можете добавлять оператор отладочной печати каждый раз, когда создается параметр:

struct DebugFactory<Base: OptionListFactory>: OptionListFactory {
    let availableOptions: [() -> Base.Option]

    init(base: Base) {
        availableOptions = base.availableOptions.map { f in
            return {
                print("Creating object")
                return f()
            }
        }
    }
}

// Works exactly the same as `ef`, just prints.
let debugFactory = DebugFactory(base: ef)
debugFactory.build(code: 2)
person Rob Napier    schedule 16.11.2020
comment
Черт, ты хорош! Тысяча благодарностей Вам!!! Это очень ясно для меня! - person Zaphod; 16.11.2020