Массивы дженериков в Swift
Я играл с массивами общих классов с разными типами. Проще всего объяснить мою проблему с помощью некоторого примера кода:
// Obviously a very pointless protocol...
protocol MyProtocol {
var value: Self { get }
}
extension Int : MyProtocol { var value: Int { return self } }
extension Double: MyProtocol { var value: Double { return self } }
class Container<T: MyProtocol> {
var values: [T]
init(_ values: T...) {
self.values = values
}
func myMethod() -> [T] {
return values
}
}
Теперь, если я попытаюсь создать массив контейнеров, например:
var containers: [Container<MyProtocol>] = []
Я получаю сообщение об ошибке:
Протокол "MyProtocol" может использоваться только в качестве общего ограничения, поскольку он имеет требования к себе или связанному типу.
Чтобы исправить это, я могу использовать [AnyObject]
:
let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]
// Explicitly stating the types just for clarity.
Но теперь возникает другая "проблема" при перечислении через containers
:
for container in containers {
if let c = container as? Container<Int> {
println(c.myMethod())
} else if let c = container as? Container<Double> {
println(c.myMethod())
}
}
Как вы можете видеть в приведенном выше коде, после определения типа container
в обоих случаях вызывается тот же метод. Мой вопрос:
Есть ли лучший способ получить container
с правильным типом, чем приведение к любому возможному типу container
? Или есть что-то еще, что я забыл?
Ответы
Ответ 1
Существует способ - вроде - делать то, что вы хотите - своего рода. Существует способ, с протоколами, устранить ограничение типа и по-прежнему получить результат, который вы хотите, вроде, но это не всегда красиво. Вот что я придумал в качестве протокола в вашей ситуации:
protocol MyProtocol {
func getValue() -> Self
}
extension Int: MyProtocol {
func getValue() -> Int {
return self
}
}
extension Double: MyProtocol {
func getValue() -> Double {
return self
}
}
Обратите внимание, что свойство value
, которое вы первоначально вложили в объявление протокола, было изменено на метод, возвращающий объект.
Это не очень интересно.
Но теперь, поскольку вы избавились от свойства value
в протоколе, MyProtocol
может использоваться как тип, а не только как ограничение типа. Ваш класс Container
больше не нужен вообще. Вы можете объявить это следующим образом:
class Container {
var values: [MyProtocol]
init(_ values: MyProtocol...) {
self.values = values
}
func myMethod() -> [MyProtocol] {
return values
}
}
И поскольку Container
больше не является общим, вы можете создать Array
из Container
и выполнить итерацию по ним, распечатав результаты метода myMethod()
:
var containers = [Container]()
containers.append(Container(1, 4, 6, 2, 6))
containers.append(Container(1.2, 3.5))
for container in containers {
println(container.myMethod())
}
// Output: [1, 4, 6, 2, 6]
// [1.2, 3.5]
Трюк заключается в создании протокола, который включает только общие функции и не устанавливает никаких других требований к соответствующему типу. Если вам это удастся, вы можете использовать протокол как тип, а не только как ограничение типа.
И в качестве бонуса (если вы хотите это назвать), ваш массив значений MyProtocol
может даже смешивать разные типы, соответствующие MyProtocol
. Поэтому, если вы дадите расширение String
a MyProtocol
следующим образом:
extension String: MyProtocol {
func getValue() -> String {
return self
}
}
Фактически вы можете инициализировать Container
со смешанными типами:
let container = Container(1, 4.2, "no kidding, this works")
[Предупреждение. Я тестирую это на одной из игровых площадок онлайн. Я еще не смог проверить его в Xcode...]
Edit:
Если вы все еще хотите, чтобы Container
был общим и содержал только один тип объекта, вы можете добиться этого, выполнив его собственный протокол:
protocol ContainerProtocol {
func myMethod() -> [MyProtocol]
}
class Container<T: MyProtocol>: ContainerProtocol {
var values: [T] = []
init(_ values: T...) {
self.values = values
}
func myMethod() -> [MyProtocol] {
return values.map { $0 as MyProtocol }
}
}
Теперь у вас все еще может быть массив объектов [ContainerProtocol]
и итерации через них, вызывающие myMethod()
:
let containers: [ContainerProtocol] = [Container(5, 3, 7), Container(1.2, 4,5)]
for container in containers {
println(container.myMethod())
}
Возможно, это все еще не работает для вас, но теперь Container
ограничивается одним типом, и все же вы все равно можете перебирать массив из объектов ContainterProtocol
.
Ответ 2
Это хороший пример того, "что вы хотели?" И на самом деле демонстрирует сложность, которая взрывается, если у Swift действительно были первоклассные типы.
protocol MyProtocol {
var value: Self { get }
}
Великий. MyProtocol.value
возвращает любой тип, реализующий его, помня, что это должно быть определено во время компиляции, а не во время выполнения.
var containers: [Container<MyProtocol>] = []
Итак, определенный во время компиляции, какой тип это? Забудьте компилятор, просто сделайте это на бумаге. Да, не знаю, какого типа это будет. Я имею в виду конкретный тип. Нет метатипов.
let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0)]
Вы знаете, что спуститесь по неправильной дороге, когда AnyObject
пробрался в ваши подписи. Ничто об этом никогда не будет работать. После AnyObject
будет только вретище.
Или есть что-то еще, что я забыл?
Да. Вам нужен тип, и вы его не предоставили. Вы предоставляете правило для ограничения типа, но не фактического типа. Вернитесь к своей реальной проблеме и подумайте об этом глубже. (Анализ метатипов почти никогда не является вашей "реальной" проблемой, если вы не работаете над CS PhD, и в этом случае вы будете делать это в Идрисе, а не в Swift.) Какую актуальную проблему вы решаете?
Ответ 3
Это может быть лучше объяснено с помощью таких протоколов, как Equatable
. Вы не можете объявить массив [Equatable]
, потому что в то время как два экземпляра Int
можно сравнивать друг с другом, а два экземпляра Double
можно сравнивать друг с другом, вы не можете сравнить Int
с Double
, хотя они оба реализовать Equatable
.
MyProtocol
- это протокол, который означает, что он предоставляет общий интерфейс. К сожалению, вы также использовали Self
в определении. Это означает, что каждый тип, соответствующий MyProtocol
, будет реализовывать его по-разному.
Вы сами написали - Int
будет иметь value
как var value: Int
, а MyObject
будет иметь value
как var value: MyObject
.
Это означает, что struct/class, который соответствует MyProtocol
, не может использоваться вместо другого struct/class, который соответствует MyProtocol
. Это также означает, что вы не можете использовать MyProtocol
таким образом, не указывая конкретный тип.
Если вы замените этот Self
на конкретный тип, например. AnyObject
, он будет работать. Однако в настоящее время (Xcode 6.3.1) он вызывает ошибку сегментации при компиляции).
Ответ 4
Если вы попробуете этот измененный пример на игровой площадке, он будет систематически аварийно:
// Obviously a very pointless protocol...
protocol MyProtocol {
var value: Int { get }
}
extension Int : MyProtocol { var value: Int { return self } }
//extension Double: MyProtocol { var value: Double { return self } }
class Container<T: MyProtocol> {
var values: [T]
init(_ values: T...) {
self.values = values
}
}
var containers: [Container<MyProtocol>] = []
Возможно, они все еще работают над этим, и в будущем все может измениться.
В любом случае, теперь я объясняю это тем, что протокол не является конкретным типом. Таким образом, вы уже не видите, сколько места в барах будет занимать что-то, соответствующее протоколу (например, Int
может не занимать одинаковое количество бара как Double
). Таким образом, может быть довольно сложной проблемой выделение массива в ram.
Используя NSArray
, вы выделяете массив указателей (указатели на NSObjects
), и все они занимают одинаковое количество бара. Вы можете придумать NSArray
как массив конкретного типа "указатель на NSObject
". Таким образом, проблема не вычисляется при распределении ram.
Считайте, что Array
, а также Dictionary
в Swift являются Generic Struct, а не объектами, содержащими указатели на объекты, как в Obj-C.
Надеюсь, что это поможет.
Ответ 5
Я изменил объявление массива как массив AnyObject, чтобы можно было использовать фильтр, карту и уменьшить (а также добавить еще несколько объектов для проверки).
let containers: [AnyObject] = [Container<Int>(1, 2, 3), Container<Double>(1.0, 2.0, 3.0), "Hello", "World", 42]
Это позволит вам проверить тип в массиве и фильтр перед тем, как вы пройдете цикл через массив
let strings = containers.filter({ return ($0 is String) })
println(strings) // [Hello, World]
for ints in containers.filter({ return ($0 is Int) }) {
println("Int is \(foo)") // Int is 42
}
let ints = containers.filter({ return ($0 is Container<Int>) })
// as this array is known to only contain Container<Int> instances, downcast and unwrap directly
for i in ints as [Container<Int>] {
// do stuff
println(i.values) // [1, 2, 3]
}