Есть ли способ упростить эту "матрицу перегрузок", основанную на типах аргументов, которые в конечном итоге представляются определенным типом?

Мы пытаемся создать функцию addQueryItem который в конечном итоге использует строку и необязательную строку внутри.

Для большей гибкости в API, чем использовать String для типов аргументов мы вместо этого используем CustomStringConvertible (который реализует String), поэтому мы можем использовать все, что может быть представлено в виде строки.

Кроме того, чтобы мы могли передать его Stringна основе перечислений, мы также хотим принять RawRepresentable типы где RawValue это CustomStringConvertible сам.

Однако, поскольку теперь мы технически принимаем два разных вида значений для каждого параметра, нам приходится создавать "матрицу перегрузок" - всего четыре - для каждой комбинации двух типов.

Моей первой мыслью было использовать протоколно-ориентированное программирование путем расширения RawRepresentable так что придерживается CustomStringConvertible если это RawValue был также CustomStringConvertible, Тогда я мог бы просто передать это прямо в версию, которая занимает два CustomStringConvertible аргументы и устранить три других. Однако компилятору это не понравилось, потому что я пытаюсь расширить протокол, а не конкретный тип.

// This doesn't work
extension RawRepresentable : CustomStringConvertible
where RawValue:CustomStringConvertible {

    var description: String {
        return self.rawValue
    }
}

Из-за невозможности сделать вышеупомянутое, как уже упоминалось, мне нужно иметь все четыре из следующих:

func addQueryItem(name:CustomStringConvertible, value:CustomStringConvertible?){

    if let valueAsString = value.flatMap({ String(describing:$0) }) {
        queryItems.append(name: String(describing:name), value: valueAsString)
    }
}

func addQueryItem<TName:RawRepresentable>(name:TName, value:CustomStringConvertible?)
where TName.RawValue:CustomStringConvertible {
    addQueryItem(name: name.rawValue, value: value)
}

func addQueryItem<TValue:RawRepresentable>(name:CustomStringConvertible, value:TValue?)
where TValue.RawValue:CustomStringConvertible {

    addQueryItem(name: name, value: value?.rawValue)
}

func addQueryItem<TName:RawRepresentable, TValue:RawRepresentable>(name:TName, value:TValue?)
where TName.RawValue:CustomStringConvertible,
      TValue.RawValue:CustomStringConvertible
{
    addQueryItem(name: name.rawValue, value: value?.rawValue)
}

Итак, так как это не выглядит, как можно сделать RawRepresentable придерживаться CustomStringConvertibleЕсть ли другой способ решить эту проблему "матрицы перегрузок"?

2 ответа

Решение

Чтобы расширить мои комментарии, я считаю, что вы боретесь с системой типов Swift. В Swift вы, как правило, не должны пытаться автоматически конвертировать типы. Вызывающие должны явно соответствовать своим типам, когда им нужна особенность. Так что к вашему примеру Order enum, я считаю, что это должно быть реализовано следующим образом:

Во-первых, есть протокол для имен и значений:

protocol QueryName {
    var queryName: String { get }
}

protocol QueryValue {
    var queryValue: String { get }
}

Теперь для строково-конвертируемых перечислений приятно не реализовывать это самостоятельно.

extension QueryName where Self: RawRepresentable, Self.RawValue == String  {
    var queryName: String { return self.rawValue }
}

extension QueryValue where Self: RawRepresentable, Self.RawValue == String  {
    var queryValue: String { return self.rawValue }
}

Но для обеспечения безопасности типов вам необходимо явно соответствовать протоколу. Таким образом, вы не сталкиваетесь с вещами, которые не предназначены для использования таким образом.

enum Order: String, RawRepresentable, QueryName {
    case buy
}

enum Item: String, RawRepresentable, QueryValue {
    case widget
}

Теперь возможно QueryItems действительно должен взять строки. ХОРОШО.

class QueryItems {
    func append(name: String, value: String) {}
}

Но вещь, которая оборачивает это, может быть типобезопасной. Сюда Order.buy а также Purchase.buy не сталкивайтесь (потому что они не могут быть оба переданы):

class QueryBuilder<Name: QueryName, Value: QueryValue> {
    var queryItems = QueryItems()

    func addQueryItem(name: QueryName, value: QueryValue?) {
        if let value = value {
            queryItems.append(name: name.queryName, value: value.queryValue)
        }
    }
}

Вы можете использовать вышеупомянутое, чтобы сделать его менее типичным (используя такие вещи, как StringCustomConvertible и делая QueryBuilder не универсальный, который я не рекомендую, но вы можете сделать это). Но я все равно настоятельно рекомендую, чтобы вызывающие абоненты явно отмечали типы, которые они планируют использовать таким образом, явно помечая (и ничего больше), что они соответствуют протоколу.


Чтобы показать, как будет выглядеть менее безопасная версия:

protocol QueryName {
    var queryName: String { get }
}

protocol QueryValue {
    var queryValue: String { get }
}

extension QueryName where Self: RawRepresentable, Self.RawValue == String  {
    var queryName: String { return self.rawValue }
}

extension QueryValue where Self: RawRepresentable, Self.RawValue == String  {
    var queryValue: String { return self.rawValue }
}

extension QueryName where Self: CustomStringConvertible {
    var queryName: String { return self.description }
}

extension QueryValue where Self: CustomStringConvertible {
    var queryValue: String { return self.description }
}


class QueryItems {
    func append(name: String, value: String) {}
}

class QueryBuilder {
    var queryItems = QueryItems()

    func addQueryItem<Name: QueryName, Value: QueryValue>(name: Name, value: Value?) {
        if let value = value {
            queryItems.append(name: name.queryName, value: value.queryValue)
        }
    }
}

enum Order: String, RawRepresentable, QueryName {
    case buy
}

enum Item: String, RawRepresentable, QueryValue {
    case widget
}

Нет, вы не можете согласовать протокол с другим протоколом через расширение. Язык не поддерживает это.

Другие вопросы по тегам