2017-02-01 18 views
5

У меня есть код, который следует за общий дизайн:Wrong специализированная Обобщенная функция вызывается в Swift 3 от косвенного вызова

protocol DispatchType {} 
class DispatchType1: DispatchType {} 
class DispatchType2: DispatchType {} 

func doBar<D:DispatchType>(value:D) { 
    print("general function called") 
} 

func doBar(value:DispatchType1) { 
    print("DispatchType1 called") 
} 

func doBar(value:DispatchType2) { 
    print("DispatchType2 called") 
} 

, где в действительности DispatchType на самом деле является хранение бэкенд. Функции doBar - это оптимизированные методы, которые зависят от правильного типа хранилища. Все работает отлично, если я:

let d1 = DispatchType1() 
let d2 = DispatchType2() 

doBar(value: d1) // "DispatchType1 called" 
doBar(value: d2) // "DispatchType2 called" 

Однако, если я функцию, которая вызывает doBar:

func test<D:DispatchType>(value:D) { 
    doBar(value: value) 
} 

и я стараюсь подобный образец вызова, я получаю:

test(value: d1)  // "general function called" 
test(value: d2)  // "general function called" 

Это похоже на то, с чем Swift должен справиться, поскольку он должен во время компиляции определять ограничения типа. Так же, как быстрый тест, я также пытался писать doBar как:

func doBar<D:DispatchType>(value:D) where D:DispatchType1 { 
    print("DispatchType1 called") 
} 

func doBar<D:DispatchType>(value:D) where D:DispatchType2 { 
    print("DispatchType2 called") 
} 

но получить те же результаты.

Любые идеи, если это правильно Быстрое поведение, и если да, то хороший способ обойти это поведение?

Редактировать 1: Пример того, почему я пытался избежать использования протоколов. Предположим, у меня есть код (значительно упрощено от моего фактического кода):

protocol Storage { 
    // ... 
} 

class Tensor<S:Storage> { 
    // ... 
} 

Для Tensor класса У меня есть базовый набор операций, которые могут быть выполнены на Tensor с. Однако сами операции будут изменять свое поведение на основе хранилища. В настоящее время я сделать это с:

func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... } 

В то время как я могу положить их в Tensor класса и использование расширений:

extension Tensor where S:CBlasStorage { 
    func dot(_ tensor:Tensor<S>) -> Tensor<S> { 
     // ... 
    } 
} 

это имеет несколько побочных эффектов, которые я не люблю:

  1. Я думаю, dot(lhs, rhs) предпочтительнее lhs.dot(rhs). Функции удобства можно написать, чтобы обойти это, но это создаст огромный взрыв кода.

  2. Это приведет к тому, что класс Tensor станет монолитным. Я действительно предпочитаю, чтобы он содержал минимальный объем необходимого кода и расширял его функциональность вспомогательными функциями.

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

Edit 2: Один из вариантов является то, что все работает как ожидается, если вы используете ограничения на все:

func test<D:DispatchType>(value:D) where D:DispatchType1 { 
    doBar(value: value) 
} 

func test<D:DispatchType>(value:D) where D:DispatchType2 { 
    doBar(value: value) 
} 

заставит правильно doBar называться. Это также не идеально, так как это вызовет много дополнительного кода для написания, но, по крайней мере, позволяет мне сохранить мой текущий дизайн.

Редактировать 3: Я столкнулся с документацией, показывающей использование ключевого слова static с дженериками. Это помогает по крайней мере, с точки (1):

class Tensor<S:Storage> { 
    // ... 
    static func cos(_ tensor:Tensor<S>) -> Tensor<S> { 
     // ... 
    } 
} 

позволяет писать:

let result = Tensor.cos(value) 

и поддерживает перегрузку операторов:

let result = value1 + value2 

это иметь дополнительное многословие требуется Tensor. Это может сделал немного лучше:

typealias T<S:Storage> = Tensor<S> 
+0

Это происходит из-за того, что Swift реализует методы отправки. Пожалуйста, взгляните на https://www.raizlabs.com/dev/2016/12/swift-method-dispatch/ - особенно в разделе Reference Type Matters. – courteouselk

+0

Связанный: [Расширение коллекции с рекурсивным свойством/методом, зависящим от типа элемента] (http://stackoverflow.com/q/41640321/2976878) – Hamish

+0

В некотором роде: [Как назвать более конкретный метод перегрузки] (http://stackoverflow.com/questions/41531569/how-to-call-the-more-specific-method-of-overloading) – dfri

ответ

7

Это действительно правильное поведение, как разрешение перегрузки происходит во время компиляции (это было бы довольно дорогая операция пройдет во время выполнения). Поэтому в пределах test(value:), единственное, что компилятор знает о value, состоит в том, что он имеет какой-то тип, который соответствует DispatchType - таким образом только перегрузка может отправиться в func doBar<D : DispatchType>(value: D).

Все было бы иначе, если бы общие функции всегда были специализированы компилятором, потому что тогда специализированная реализация test(value:) будет знать конкретный тип value и, следовательно, сможет выбрать соответствующую перегрузку. Однако специализация общих функций в настоящее время является лишь оптимизацией (так как без инкрустации, она может добавить значительную раздутость в ваш код), поэтому это не изменяет наблюдаемое поведение.

Одним из решений для обеспечения полиморфизма является использование таблицы свидетелей протокола (см. this great WWDC talk), добавив doBar() в качестве требования протокола и реализуя специализированные реализации в соответствующих классах, соответствующих протоколу, причем общая реализация является частью расширения протокола.

Это позволит динамически отправлять doBar(), что позволяет ему вызывать от test(value:) и иметь правильную реализацию.

protocol DispatchType { 
    func doBar() 
} 

extension DispatchType { 
    func doBar() { 
     print("general function called") 
    } 
} 

class DispatchType1: DispatchType { 
    func doBar() { 
     print("DispatchType1 called") 
    } 
} 

class DispatchType2: DispatchType { 
    func doBar() { 
     print("DispatchType2 called") 
    } 
} 

func test<D : DispatchType>(value: D) { 
    value.doBar() 
} 

let d1 = DispatchType1() 
let d2 = DispatchType2() 

test(value: d1) // "DispatchType1 called" 
test(value: d2) // "DispatchType2 called" 
+0

Это может быть предполагаемое поведение, но я определенно не ожидал этого! – Andreas

+0

Я согласен, что не ожидается поведения, по крайней мере, из шаблонов C++.Есть ли способ заставить компилятор создавать отдельные версии и использовать их для разрешения (я пробовал '@ _specialize', но это не сработало)? Я знаю, что если я делаю неоригинальные версии 'test', это работает, поэтому в теории Свифт способен вести себя корректно. К сожалению, использование протоколов делает дизайн очень уродливым, поэтому я предпочитаю не делать этого ... –

+0

@AbeSchneider Я не считаю, что в настоящее время существует способ заставить специализацию данной общей функции - это просто делается как оптимизация с помощью компилятор. Хотя, возможно, стоит [подать заявку на улучшение] (https://bugs.swift.org), чтобы узнать, рассмотрит ли команда Swift. Учитывая, что ваш исходный код использует протокол, я не уверен, что вы считаете настолько уродливым в отношении подхода добавления doBar() как требование - хотя может быть лучшее решение для вашего конкретного случая использования (возможно, приведение типов) , Хотя трудно сказать, не видя конкретного примера. – Hamish