Реализация функции в ограниченном расширении протокола не вызывается - PullRequest
0 голосов
/ 29 июня 2018

Краткое описание проблемы

У меня есть подкласс общего вида TintableView<T>: UIView, который реализует протокол TintStateComputing с идентичным связанным типом T. Реализация ограниченного расширения TintStateComputing не вызывается; вместо этого вызывается его неограниченная реализация расширения.

Протокол TintStateComputing имеет функцию computeTintState() -> T, которая делает то, на что это похоже: проверяет свойства локального состояния и возвращает соответствующий экземпляр T.

Я хочу написать реализации расширений func computeTintState() -> T для TintStateComputing, ограниченные типом T. Например, с перечислением ControlState:

extension TintStateComputing where T == ControlState {
    func computeTintState() -> T {
        return self.isEnabled ? .enabled : T.default
    }
}

Однако, чтобы завершить согласование протокола, я думаю, что мне нужно учитывать другие значения T. Итак, я также указал неограниченное расширение TintStateComputing. Эта реализация расширения без ограничений всегда вызывается вместо ограниченной реализации.

extension TintStateComputing {
    func computeTintState() -> T {
        return _tintState ?? T.default
    }
}

Игровая площадка

import UIKit

// MARK: - Types

public enum ControlState: Int {
    case normal, enabled, highlighted, selected, disabled
}

public protocol Defaultable {
    static var `default`: Self { get }
}

extension ControlState: Defaultable {
    public static let `default`: ControlState = .normal
}

// MARK: - TintStateComputing declaration

public protocol TintStateComputing {
    associatedtype TintState: Hashable & Defaultable
    func computeTintState() -> TintState
    var _tintState: TintState? { get }
    var isEnabled: Bool { get }
}

// MARK: - TintableView declaration

class TintableView<T: Hashable & Defaultable>: UIView, TintStateComputing {
    // `typealias TintState = T` is implictly supplied by compiler
    var _tintState: T?
    var isEnabled: Bool = true { didSet { _tintState = nil }}

    var tintState: T  {
        get {
            guard _tintState == nil else {
                return _tintState!
            }
            return computeTintState()
        }
        set {
            _tintState = newValue
        }
    }
}

// MARK: - Unconstrained TintStateComputing extension

extension TintStateComputing {
    func computeTintState() -> TintState {
        return _tintState ?? TintState.default
    }
}

// MARK: - Constrained TintStateComputing extension

extension TintStateComputing where TintState == ControlState {
    func computeTintState() -> TintState {
        return self.isEnabled ? .enabled : TintState.default
    }
}

// MARK: - Test Case

let a = TintableView<ControlState>()
a.isEnabled = true
print("Computed tint state: \(a.tintState);  should be .enabled") // .normal
print("finished")

Обход

Сегодня утром я понял, что, поскольку (по крайней мере, на данный момент) я действительно пытаюсь выполнить обработку флага isEnabled: Bool на представлении, я мог бы следовать той же схеме, что и для Defaultable, чтобы определить значение по умолчанию «включен» случай.

public protocol Enableable {
    static var defaultEnabled: Self { get }
}

extension ControlState: Defaultable, Enableable {
    public static let `default`: ControlState = .normal
    public static let defaultEnabled: ControlState = .enabled
}

В этот момент я действительно могу исключить протокол TintStateComputing и обновить реализацию tintState: T моего представления, чтобы напрямую учесть флаг.

var tintState: T  {
    get {
        guard _tintState == nil else { return _tintState! }
        return self.isEnabled ? T.defaultEnabled : T.default
    }
    set {
        _tintState = newValue
    }
}

Это не так обобщенно, как помещение реализации в ограниченное расширение, но пока она будет работать. Я думаю, что если у меня появятся будущие подклассы с многомерными типами состояний тинта (например, 'enabled' + 'in-range'), я смогу обратиться через override.

struct CalendarState: Equatable, Hashable, Defaultable, Enableable  {
    let x: Int

    static let `default`: CalendarState = CalendarState(x: 0)
    static let defaultEnabled: CalendarState = CalendarState(x: 1)
}

class ControlTintableView: TintableView<ControlState> {}
class CalendarView: TintableView<CalendarState> {}

let a = ControlTintableView()
a.isEnabled = true
print("ControlTintableView computed tint state: \(a.tintState);  should be: .enabled") // .enabled

let b = CalendarView()
b.isEnabled = true
print("CalendarView computed tint state: \(b.tintState);  should be: CalendarState(x: 1)") // CalendarState(x: 1)

1 Ответ

0 голосов
/ 29 июня 2018

Проблема в том, что существует только одна специализация TintableView, и она основана на том, что он знает из своего собственного определения. При компиляции класса он считает computeTintState(), видит, что TintState не обещано в этот момент точно ControlState, и поэтому компилирует в более общую версию.

Чтобы делать то, что вы хотите, когда он встречает TintableView<ControlState>, ему необходимо полностью пересмотреть и перекомпилировать класс TintableView. Свифт в настоящее время не делает этого. В этом случае я не считаю это ошибкой. Я думаю, что этот код пытается быть слишком волшебным и злоупотребляет расширениями, но это только мое мнение по этому поводу. Если вы считаете, что Swift должен справиться с подобным случаем, я рекомендую открыть дефект на bugs.swift.org.

Имейте в виду, что произойдет, если TintableView будет в одном модуле, а расширение TintState == ControlState - в другом (скажем, в модуле с let a =). В этом случае было бы невозможно получить запрашиваемое вами поведение, потому что один модуль не может перепрофилировать другой модуль (возможно, у него нет доступного исходного кода). Считаете ли вы этот код хорошим, если бы он вел себя по-одному, когда он был в одном модуле, но имел разные видимые поведения, если бы он был в разных модулях? Вот почему я считаю это слишком сложным и подверженным ошибкам. Эта проблема специализации сплит-модуля возникает постоянно, но в основном она просто влияет на производительность (и stdlib использует частные директивы компилятора для ее улучшения, потому что это особый случай).

...