Чому протоколи не відповідають самим собі?
Дозволити протоколам відповідати самим собі в загальному випадку - це невідомо. Проблема полягає в вимогах статичного протоколу.
До них належать:
static
методи та властивості
- Ініціалізатори
- Пов'язані типи (хоча вони в даний час перешкоджають використанню протоколу як фактичного типу)
Ми можемо отримати доступ до цих вимог на загальному заповнювачі місця, T
де T : P
- однак ми не можемо отримати доступ до них у самому типі протоколу, оскільки немає конкретного відповідного типу, на який слід переходити. Тому ми не можемо дозволити T
бути P
.
Розглянемо, що буде в наступному прикладі, якби ми дозволили Array
розширення застосувати до [P]
:
protocol P {
init()
}
struct S : P {}
struct S1 : P {}
extension Array where Element : P {
mutating func appendNew() {
// If Element is P, we cannot possibly construct a new instance of it, as you cannot
// construct an instance of a protocol.
append(Element())
}
}
var arr: [P] = [S(), S1()]
// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
arr.appendNew()
Ми не можемо закликати appendNew()
a [P]
, тому що P
(the Element
) не є конкретним типом і тому не може бути ініційованим. Його потрібно викликати на масиві з елементами, що мають тип бетону, де цей тип відповідає P
.
Це схожа історія зі статичним методом та вимогами до властивостей:
protocol P {
static func foo()
static var bar: Int { get }
}
struct SomeGeneric<T : P> {
func baz() {
// If T is P, what's the value of bar? There isn't one – because there's no
// implementation of bar's getter defined on P itself.
print(T.bar)
T.foo() // If T is P, what method are we calling here?
}
}
// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
SomeGeneric<P>().baz()
Ми не можемо говорити з точки зору SomeGeneric<P>
. Нам потрібні конкретні реалізації вимог статичного протоколу (зверніть увагу на те, як у вищенаведеному прикладі немає реалізацій foo()
або bar
визначено). Хоча ми можемо визначити реалізацію цих вимог у P
розширенні, вони визначені лише для конкретних типів, яким вони відповідають P
- ви все одно не можете їх викликати P
.
Через це Swift просто повністю забороняє нас використовувати протокол як тип, який відповідає самому собі, - оскільки коли до цього протоколу є статичні вимоги, він цього не робить.
Вимоги до протоколу екземпляра не є проблематичними, тому що ви повинні викликати їх у фактичному екземплярі, який відповідає протоколу (і, отже, повинні бути реалізовані вимоги). Отже, викликаючи вимогу для екземпляра, набраного як P
, ми можемо просто переадресувати цей виклик на реалізацію цієї вимоги, що лежить в основі конкретного типу.
Однак в цьому випадку внесення особливих винятків з правила може призвести до несподіваної невідповідності способів обробки протоколів за допомогою загального коду. Незважаючи на це, ситуація не надто відрізняється від associatedtype
вимог - які (на даний момент) заважають вам використовувати протокол як тип. Наявність обмеження, яке заважає використовувати протокол як тип, який відповідає самому собі, коли він має статичні вимоги, може стати варіантом для майбутньої версії мови
Редагувати: І як це було досліджено нижче, це схоже на те, до чого прагне команда Свіфт.
@objc
протоколи
А насправді саме так мова поводиться з @objc
протоколами. Коли у них немає статичних вимог, вони відповідають самим собі.
Наступні компіляції просто чудово:
import Foundation
@objc protocol P {
func foo()
}
class C : P {
func foo() {
print("C's foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c)
baz
вимагає, щоб T
відповідати P
; але ми можемо замінити в P
протягом T
бо P
не мають статичні вимоги. Якщо ми додамо статичну вимогу P
, приклад більше не компілюється:
import Foundation
@objc protocol P {
static func bar()
func foo()
}
class C : P {
static func bar() {
print("C's bar called")
}
func foo() {
print("C's foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c) // error: Cannot invoke 'baz' with an argument list of type '(P)'
Тож одне вирішення цієї проблеми - скласти свій протокол @objc
. Звичайно, це не є ідеальним рішенням у багатьох випадках, оскільки воно змушує ваші типи відповідності бути класами, а також вимагати виконання Obj-C, тому не робить його життєздатним на інших платформах Apple, таких як Linux.
Але я підозрюю, що це обмеження є (однією з) першочергових причин того, що мова вже реалізує "протокол без статичних вимог". @objc
протоколів. Компілятор може значно спростити загальний код, написаний навколо них.
Чому? Оскільки значення типу @objc
протоколу фактично є лише посиланнями класів, вимоги яких надсилаються за допомогою objc_msgSend
. Зі зворотного боку, не-@objc
значення, які протоколом, є складнішими, оскільки вони несуть навколо себе таблицю значень та свідчень, щоб одночасно керувати пам’яттю свого (потенційно опосередковано збереженого) обгорнутого значення та визначати, які реалізації потрібно викликати для різних вимоги відповідно.
Через таке спрощене представлення для @objc
протоколів, значення такого типу протоколу P
може поділяти те саме представлення пам'яті, що і «загальне значення» типу якогось загального заповнювача заповнення T : P
, імовірно, полегшуючи команду Swift дозволити самовідповідність. Те саме не стосується непротоколів @objc
, однак такі загальні значення в даний час не несуть таблиць значень або протоколів свідчень протоколу.
Однак ця функція є навмисною, і, сподіваємось, вона буде передана на непротоколи @objc
, що підтвердив член команди Swift Слава Пестов у коментарях SR-55 у відповідь на ваш запит про це (на це запит ):
Matt Neuburg додав коментар - 7 вересня 2017 13:33
Це компілює:
@objc protocol P {}
class C: P {}
func process<T: P>(item: T) -> T { return item }
func f(image: P) { let processed: P = process(item:image) }
Додавання @objc
робить його компільованим; вилучивши його, він не збирається знову Деякі з нас на Stack Overflow вважають це дивовижним і хотіли б дізнатися, чи це навмисно чи невдалий край.
Слава Пестов додав коментар - 7 вересня 2017 13:53
Це навмисно - скасування цього обмеження полягає в тому, про що йдеться у цьому помилку. Як я сказав, це складне, і у нас поки немає конкретних планів.
Тож сподіваємось, що це буде одна з днів, коли мова також підтримує непротоколи @objc
.
Але які поточні рішення існують для непротоколів @objc
?
Реалізація розширень із обмеженнями протоколу
У Swift 3.1, якщо ви хочете розширення з обмеженням, що певний загальний заповнювач місця заповнення або асоційований тип повинен бути заданим типом протоколу (а не лише конкретного типу, який відповідає цьому протоколу) - ви можете просто визначити це з ==
обмеженням.
Наприклад, ми можемо записати ваше розширення масиву як:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
Звичайно, це тепер заважає нам називати його на масиві з елементами конкретного типу, які відповідають P
. Ми можемо вирішити це, просто визначивши додаткове розширення для того, коли Element : P
і просто переслати на == P
розширення:
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
Однак варто відзначити, що це здійснить перетворення O (n) масиву в a [P]
, оскільки кожен елемент повинен бути в ящику в екзистенційному контейнері. Якщо продуктивність є проблемою, ви можете просто вирішити це, повторно застосувавши метод розширення. Це не цілком задовільне рішення - сподіваємось, що майбутня версія мови буде містити спосіб виразити обмеження "тип протоколу чи відповідність типу протоколу".
До Swift 3.1, як показує Роб у своїй відповіді , найзагальніший спосіб досягти цього - це просто побудувати тип обгортки для a [P]
, який потім можна визначити із способами (-ми) розширення.
Передача екземпляра типу протоколу обмеженому загальному заповнювачу місця
Розглянемо таку (надуману, але не рідкість) ситуацію:
protocol P {
var bar: Int { get set }
func foo(str: String)
}
struct S : P {
var bar: Int
func foo(str: String) {/* ... */}
}
func takesConcreteP<T : P>(_ t: T) {/* ... */}
let p: P = S(bar: 5)
// error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)'
takesConcreteP(p)
Ми не можемо пройти p
до takesConcreteP(_:)
, як ми не можемо в даний час замінити P
для загального заповнювач T : P
. Давайте розглянемо кілька способів вирішити цю проблему.
1. Відкриття екзистенціалів
Замість того , щоб намагатися замінити P
на T : P
, що якби ми могли копатися в базовий тип бетону , що P
введене значення було обгортання і замінити , що замість цього? На жаль, для цього потрібна мовна функція під назвою екзистенціалів відкриття , яка наразі безпосередньо не доступна користувачам.
Однак, Swift робить неявно відкриті екзістенціали (значення протоколу типізований) при доступі користувачів на них (тобто викопує тип виконання і робить його доступним у вигляді родового заповнювач). Ми можемо використовувати цей факт у розширенні протоколу на P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Зверніть увагу на неявну загальну Self
заповнювач заповнення, яку приймає метод розширення, яка використовується для введення неявного self
параметра - це відбувається поза кадром з усіма членами розширення протоколу. Викликаючи такий метод за значенням P
, набраним протоколом , Swift викопує базовий конкретний тип і використовує це для задоволення Self
загального заповнювача. Ось чому ми можемо назвати takesConcreteP(_:)
з self
- ми задовольняє T
з Self
.
Це означає, що зараз ми можемо сказати:
p.callTakesConcreteP()
І takesConcreteP(_:)
називається, коли його загальний заповнювач місць T
задовольняється базовим конкретним типом (в даному випадку S
). Зауважте, що це не "відповідні їм самі протоколи", оскільки ми замінюємо конкретний тип, а не P
- спробуйте додати в протокол статичну вимогу і побачити, що відбувається, коли ви викликаєте його зсередини takesConcreteP(_:)
.
Якщо Swift продовжує відмовляти протоколам від відповідності собі, наступною найкращою альтернативою буде неявне відкриття екзистенціалів при спробі передачі їх як аргументів параметрам загального типу - ефективно роблячи саме те, що робив наш батут розширення протоколу, просто без котла.
Однак зауважте, що відкриття екзистенціалів не є загальним рішенням проблеми невідповідності самих протоколів. Він не стосується гетерогенних колекцій типових протокольних значень, які можуть мати різні конкретні типи. Наприклад, врахуйте:
struct Q : P {
var bar: Int
func foo(str: String) {}
}
// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}
// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]
// So there's no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array)
З тих самих причин функція з декількома T
параметрами також буде проблематичною, оскільки параметри повинні приймати аргументи одного типу - однак, якщо у нас є два P
значення, ми не можемо гарантувати під час компіляції, що вони мають однаковий базовий конкретний тип.
Для вирішення цієї проблеми ми можемо використовувати гумку типу.
2. Побудуйте гумку типу
Як каже Роб , ластик типу - це найбільш загальне рішення проблеми невідповідності самих протоколів. Вони дозволяють нам зафіксувати тип протоколу в конкретний тип, який відповідає цьому протоколу, пересилаючи вимоги до цього примірника.
Отже, давайте побудуємо поле стирання типу, яке пересилає P
вимоги до екземпляра на базовий довільний екземпляр, який відповідає P
:
struct AnyP : P {
private var base: P
init(_ base: P) {
self.base = base
}
var bar: Int {
get { return base.bar }
set { base.bar = newValue }
}
func foo(str: String) { base.foo(str: str) }
}
Тепер ми можемо просто поговорити AnyP
замість P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
А тепер подумайте на хвилинку лише, чому нам довелося будувати цей ящик. Як ми обговорювали на початку, Свіфт потребує конкретного типу для випадків, коли протокол має статичні вимоги. Поміркуйте, якби P
була статична вимога - нам би знадобилося це здійснити в AnyP
. Але як це слід було реалізувати? Ми маємо справу з довільними випадками, які відповідають P
тут - ми не знаємо про те, як їх основні конкретні типи реалізують статичні вимоги, тому ми не можемо змістовно це висловити AnyP
.
Тому рішення в цьому випадку дійсно корисне лише у випадку вимог протоколу екземпляра . У загальному випадку ми все ще не можемо трактувати P
як конкретний тип, який відповідає P
.
let arr
рядку, компілятор визначає тип[S]
і компілює код. Схоже, тип протоколу не може використовуватися так само, як відносини клас - надклас.