Дженерики не компилируются
Я пытаюсь реализовать следующую структуру, используя дженерики. Получение ошибки компилятора, не могу понять, почему.
class Translator<T:Hashable> {...}
class FooTranslator<String>:Translator<String> {...}
Идея состоит в том, что Translator использует T в качестве типа ключа в словаре. Это может быть, например, строка или перечисление. Подкласс предоставляет конкретный словарь.
Но происходит сбой, потому что: "Тип" String "не соответствует протоколу" Hashable ""
Но String соответствует Hashable. Он также не работает с Int, который также соответствует Hashable.
Если я удаляю ограничение типа, просто для тестирования (где я также должен отключить словарь, так как я не могу использовать ничего, что не может быть хешируемым в качестве ключа) - он компилируется
class Translator<T> {...}
class FooTranslator<String>:Translator<String> {...}
Что я делаю неправильно?
Ответы
Ответ 1
Я не разработчик Swift, но, увидев подобные проблемы в Java, я подозреваю, что проблема заключается в том, что на данный момент вы объявляете параметр типа String
, потому что вы объявляете class FooTranslator<String>
- поэтому type в Translator<String>
- это просто параметр типа, который не имеет ограничений. Вы не хотите, чтобы параметр типа вообще был, я подозреваю (т.е. Вы не хотите, чтобы ваш FooTranslator
был общим классом.)
Как отмечено в комментариях, в подклассах Swift родового класса также должны быть общие. Вы могли бы объявить параметр типа выброса, например:
class FooTranslator<T>:Translator<String>
который все еще избегает объявления нового параметра типа String
, что и вызывало проблему. Это означает, что вы вводите новый параметр типа, когда вам не нужны какие-либо параметры типа, но возможно лучше, чем ничего...
Это все, исходя из предположения, что вам действительно нужен подкласс, например. для добавления или переопределения членов. С другой стороны, если вы просто хотите, чтобы тип был точно таким же, как Translator<String>
, вместо него следует использовать псевдоним типа:
typealias FooTranslator = Translator<String>
Или даже смешайте их ужасно, если вы действительно хотите подкласс, но не хотите ссылаться на него общим способом:
class GenericFooTranslator<T>:Translator<String>
typealias FooTranslator = GenericFooTranslator<Int>
(Обратите внимание, что Int
здесь намеренно не String
, чтобы показать, что T
in Translator
не совпадает с T
в FooTranslator
.)
Ответ 2
Для начала объясните ошибку:
Дано:
class Foo<T:Hashable> { }
class SubFoo<String> : Foo<String> { }
Сложная часть здесь заключается в том, что мы ожидаем, что "String" означает строгую структуру Swift, которая содержит набор символов. Но это не так.
Здесь "String" - это имя общего типа, которым мы присвоили наш новый подкласс SubFoo
. Это становится очень очевидным, если мы вносим некоторые изменения:
class SubFoo<String> : Foo<T> { }
Эта строка генерирует ошибку для T
как использование необъявленного типа.
Тогда, если мы изменим строку на это:
class SubFoo<T> : Foo<T> { }
Мы вернулись к той же ошибке, которую вы изначально имели, "T" не соответствует "Hashable". Это очевидно здесь, потому что T не смущает имя существующего типа Swift, который соответствует "Hashable". Очевидно, что "T" является общим.
Когда мы пишем "String" , это также просто имя заполнителя для общего типа, а не тип String
, который существует в Swift.
Если нам нужно другое имя для определенного типа общего класса, подходящий подход почти наверняка будет typealias
:
class Foo<T:Hashable> {
}
typealias StringFoo = Foo<String>
Это абсолютно корректный Swift, и он просто компилируется.
Если вместо этого мы хотим использовать подкласс и добавлять методы или свойства в общий класс, тогда нам нужен класс или протокол, которые сделают наш общий характер более конкретным для того, что нам нужно.
Возвращаясь к исходной проблеме, сначала избавьтесь от ошибки:
class Foo<T: Hashable>
class SubFoo<T: Hashable> : Foo<T> { }
Это совершенно верно Swift. Но это может быть не особенно полезно для того, что мы делаем.
Единственная причина, по которой мы не можем сделать следующее:
class SubFoo<T: String> : Foo<T> { }
просто потому, что String
не является классом Swift - это структура. И это не допускается для какой-либо структуры.
Если мы напишем новый протокол, который наследует от Hashable
, мы можем использовать это:
protocol MyProtocol : Hashable { }
class Foo<T: Hashable> { }
class SubFoo<T: MyProtocol> : Foo<T> { }
Это совершенно верно.
Также обратите внимание, что нам действительно не нужно наследовать от Hashable
:
protocol MyProtocol { }
class Foo<T: Hashable> { }
class SubFoo<T: Hashable, MyProtocol> { }
Это также отлично.
Однако обратите внимание, что по какой-либо причине Swift не позволит вам использовать класс здесь. Например:
class MyClass : Hashable { }
class Foo<T: Hashable> { }
class SubFoo<T: MyClass> : Foo<T> { }
Swift загадочно жалуется, что "T" не соответствует "Hashable" (даже когда мы добавляем необходимый код, чтобы сделать это так.
В конце концов, правильный подход и самый подход, подходящий для Swift, будет писать новый протокол, который наследуется от "Hashable" и добавляет любую функциональность, которая вам нужна.
Не должно быть строго важно, чтобы наш подкласс принял String
. Важно, что независимо от того, что наш подкласс занимает, он имеет необходимые методы и свойства, которые нам нужны во всем, что мы делаем.