Невозможно соответствовать протоколу, создав расширение с пунктами Where

protocol Typographable {
    func setTypography(_ typography: Typography)
}

extension UILabel: Typographable {}

extension Typographable where Self == UILabel {

    func setTypography(_ typography: Typography) {

        self.font = typography.font
        self.textColor = typography.textColor
        self.textAlignment = typography.textAlignment
        self.numberOfLines = typography.numberOfLines
    }
}

Я создал протокол Typographable, UILabel реализует этот протокол, а реализация находится в extension Typographable where Self == UILabel.

он отлично работает в Swift 4.0, но больше не работает в Swift 4.1, сообщение об ошибке Type 'UILabel' does not conform to protocol 'Typographable'

Я внимательно прочитал CHANGELOG swift 4.1, но Я не могу найти ничего полезного.

Это нормально, я что-то пропустил?


person JIE WANG    schedule 12.04.2018    source источник
comment
Почему бы просто не extension UILabel: Typographable { ... }   -  person Martin R    schedule 12.04.2018
comment
@MartinR, потому что у меня есть еще один XXLabel, у которого другая реализация setTypography. если я использую extension UILabel: Typographable { ... }, мне нужно написать что-то вроде if self is XXLabel, что не элегантно.   -  person JIE WANG    schedule 12.04.2018
comment
На данный момент у меня нет объяснения (и я не могу проверить, отличалось ли поведение в Swift 4.0), но extension Typographable where Self: UILabel { ... } заставил бы его скомпилировать   -  person Martin R    schedule 12.04.2018
comment
Я могу подтвердить, что ваш подход работает со Swift 4 (Xcode 9.2), но не со Swift 4.1.   -  person Martin R    schedule 12.04.2018
comment
@JIEWANG XXLabel наследует от UILabel? В этом случае обратите внимание, что при согласовании UILabel с Typographable динамические отправки на setTypography (при вызове значения, типизированного протоколом) будут отправляться на расширение UILabel, а не на расширение Typographable. Таким образом, ваш подход не будет полностью работать и в Swift 4.0.3.   -  person Hamish    schedule 13.04.2018
comment
@MartinR Кстати, вы можете напрямую загрузить набор инструментов для выпуска Swift 4.0.3 (от swift.org/download/#releases, прямая ссылка: swift.org/builds/swift-4.0.3-release/xcode/swift-4.0.3-RELEASE/) и используйте его прямо из Xcode 9.3. Избавляет от необходимости жонглировать двумя версиями Xcode :)   -  person Hamish    schedule 13.04.2018


Ответы (1)


Это довольно интересно. Короче говоря (хорошо, может быть, не это коротко) — это преднамеренная сторона effect of #12174, что позволяет использовать методы расширения протокола, которые возвращают Self, чтобы удовлетворить требования протокола для нефинальных классов, а это означает, что теперь вы можете сказать это в 4.1:

protocol P {
  init()
  static func f() -> Self
}

extension P {
  static func f() -> Self {
    return self.init()
  }
}

class C : P {
  required init() {}
}

В Swift 4.0.3 вы получите запутанную ошибку в реализации расширения f(), говоря:

Метод «f()» в неконечном классе «C» должен возвращать Self, чтобы соответствовать протоколу «P»

Как это относится к вашему примеру? Что ж, рассмотрим этот несколько похожий пример:

class C {}
class D : C {}

protocol P {
  func copy() -> Self
}

extension P where Self == C {
  func copy() -> C {
    return C()
  }
}

extension C : P {}

let d: P = D()
print(d.copy()) // C (assuming we could actually compile it)

Если бы Swift позволил реализации расширения протокола copy() удовлетворить требование, мы создали бы экземпляры C даже при вызове экземпляра D, нарушив контракт протокола. Поэтому Swift 4.1 делает соответствие незаконным (чтобы сделать соответствие в первом примере допустимым), и делает это независимо от того, есть ли в игре Self возвратов.

Что мы на самом деле хотим выразить с помощью расширения, так это то, что Self должен быть или наследоваться от C, что заставляет нас рассматривать случай, когда подкласс использует соответствие.

В вашем примере это будет выглядеть так:

protocol Typographable {
    func setTypography(_ typography: Typography)
}

extension UILabel: Typographable {}

extension Typographable where Self : UILabel {

    func setTypography(_ typography: Typography) {

        self.font = typography.font
        self.textColor = typography.textColor
        self.textAlignment = typography.textAlignment
        self.numberOfLines = typography.numberOfLines
    }
}

который, как говорит Мартин, отлично компилируется в Swift 4.1. Хотя, как говорит Мартин, это можно переписать гораздо проще:

protocol Typographable {
    func setTypography(_ typography: Typography)
}

extension UILabel : Typographable {

    func setTypography(_ typography: Typography) {

        self.font = typography.font
        self.textColor = typography.textColor
        self.textAlignment = typography.textAlignment
        self.numberOfLines = typography.numberOfLines
    }
}

Если говорить более подробно, то #12174 позволяет распространять неявный Self параметр через следящие (соответствующие реализации) переходники. Это достигается путем добавления универсального заполнителя к этому преобразователь, ограниченному соответствующим классом.

Итак, для такого соответствия:

class C {}

protocol P {
  func foo()
}

extension P {
  func foo() {}
}

extension C : P {}

В Swift 4.0.3 таблица-свидетель протокола C (у меня есть небольшой бред о PWT здесь, который может быть полезен для их понимания ) содержит запись в преобразователь с подписью:

(C) -> Void

(обратите внимание, что в отрывке, на который я ссылаюсь, я пропускаю детали наличия переходников и просто говорю, что PWT содержит запись о реализации, используемой для удовлетворения требования. Семантика, по большей части, одна и та же хотя)

Однако в Swift 4.1 подпись преобразователь теперь выглядит так:

<Self : C>(Self) -> Void

Почему? Потому что это позволяет нам распространять информацию о типе для Self, что позволяет нам сохранить динамический тип экземпляра для создания в первом примере (и, таким образом, сделать его допустимым).

Теперь для расширения, которое выглядит так:

extension P where Self == C {
  func foo() {}
}

есть несоответствие между сигнатурой реализации расширения, (C) -> Void, и сигнатурой преобразователя, <Self : C>(Self) -> Void. Таким образом, компилятор отклоняет соответствие (возможно, это слишком строго, поскольку Self является подтипом C, и мы могли бы применить здесь контравариантность, но это текущее поведение).

Однако, если у нас есть расширение:

extension P where Self : C {
  func foo() {}
}

все снова в порядке, так как обе подписи теперь <Self : C>(Self) -> Void.

Одна интересная вещь, которую следует отметить в отношении #12174, заключается в том, что при требования содержат связанные типы. Итак, это работает:

class C {}

protocol P {
  associatedtype T
  func foo() -> T
}

extension P where Self == C {
  func foo() {} // T is inferred to be Void for C.
}

extension C : P {}

Но вам, вероятно, не следует прибегать к таким ужасным обходным путям. Просто измените ограничение расширения протокола на where Self : C.

person Hamish    schedule 12.04.2018