Бинарный оператор Swift для протоколов, который возвращает тип получателя (который соответствует протоколу)

Для Friday Fun я хотел смоделировать углы во взаимозаменяемых форматах. Я не уверен, что сделал это самым идиоматичным способом Swift, но я учусь. Итак, у меня есть протокол Angle, а затем 3 разных типа структур (радианы, градусы и повороты), которые соответствуют протоколу Angle. Я хотел бы иметь возможность добавлять/вычитать их, но хитрость в том, что я хочу, чтобы аргумент lhs определял тип возвращаемого значения. Например:

Degrees(180) + Rotations(0.25) --> Degrees(270)

or

Rotations(0.25) + Radians(M_PI) -> Rotations(0.75)

Я надеялся, что смогу сделать что-то вроде

func + (lhs:Angle, rhs:Angle) -> Angle {
    return lhs.dynamicType(rawRadians: lhs.rawRadians + rhs.rawRadians)
}

Протокол Angle требует var rawRadians:CGFloat { get }, а также init(rawRadians:CGFloat)

Я мог бы сделать это с помощью подхода двойной отправки в стиле Smalltalk, но я думаю, что наиболее подходящий подход для Swift (особенно тот, который требует меньше кода, двойная отправка требует много стандартного кода).


person Travis Griggs    schedule 25.09.2015    source источник


Ответы (1)


Вам просто нужно общее дополнение:

func +<A: Angle>(lhs: A, rhs: Angle) -> A {
    return A(rawRadians: lhs.rawRadians + rhs.rawRadians)
}

Таким образом, добавление вернет любой тип, указанный слева.

Вообще говоря, если вы используете dynamicType, вы, вероятно, боретесь со Swift. Swift больше полагается на дженерики и протоколы (т. е. информацию о статическом типе во время компиляции), а не на динамическую диспетчеризацию (т. е. информацию о динамическом типе во время выполнения).

В этом коде, как вы говорите, A является заполнителем для «некоторого типа, соответствующего Angle, который должен быть определен во время компиляции». Итак, в вашем первом примере:

Degrees(180) + Rotations(0.25) --> Degrees(270)

На самом деле это вызывает специализированную функцию +<Degrees>. И это:

Rotations(0.25) + Radians(M_PI) -> Rotations(0.75)

вызывает (логически) другую функцию с именем +<Rotations>. Компилятор может оптимизировать эти функции в одну функцию, но логически они являются независимыми функциями, созданными во время компиляции. По сути, это ярлык для написания addDegrees(Degrees, Angle) и addRotations(Rotations, Angle) вручную.

Теперь, что касается вашего вопроса о функции, которая принимает два угла и возвращает... ну, что? Если вы хотите вернуть Angle в этом случае, это легко и точно соответствует вашей исходной подписи:

func +(lhs: Angle, rhs: Angle) -> Angle {
    return Radians(rawRadians: lhs.rawRadians + rhs.rawRadians)
}

«Но…» вы говорите, «это возвращает Radians». Нет, это не так. Он возвращает Angle. Вы можете делать с ним все, что захотите. Детали реализации непрозрачны, как и должно быть. Если вас волнует, что базовая структура данных является Radians, вы почти наверняка делаете что-то не так.

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

func +(lhs: Angle, rhs: Angle) -> Angle {
    return lhs.dynamicType.init(rawRadians: lhs.rawRadians + rhs.rawRadians)
}

Но важно понимать, что это не соответствует вашему запросу на наличие «аргумента lhs для указания типа возвращаемого значения». Это приводит к тому, что аргумент lhs определяет реализацию возврата. Возвращаемый тип всегда Angle. Если вы хотите изменить возвращаемый тип, вам нужно использовать дженерики.

person Rob Napier    schedule 25.09.2015
comment
Потрясающий! Это работает. Если у вас есть возможность, я был бы признателен, если бы вы добавили небольшое объяснение того, как компилятор интерпретирует общую часть синтаксиса. Я смутно разбираюсь в дженериках, но недостаточно. Является ли ‹A› по существу заполнителем для любого типа, который может там быть, и вы ограничиваете допустимые карты через часть :Angle? - person Travis Griggs; 25.09.2015
comment
Я говорил слишком рано. Я только что обнаружил проблему с этим. Если у меня есть две переменные с объявленным типом Angle, которые не уточняются до одной из трех моих соответствующих структур, то компилятор говорит, что Binary operator '+' cannot be applied between two 'Angle' operands - person Travis Griggs; 25.09.2015