Я выполняю кучу вычислений, используя Decimal и Double. Без дженериков было бы много повторяющегося кода. Есть места, где мне в конечном итоге понадобится десятичная дробь в более родном формате, но я не могу понять, как это сделать в Swift.
Гипотетический пример: в Decimal нет log2(), но из-за приемлемой погрешности я хочу преобразовать его в Double и использовать таким образом.
Первоначально я думал, что просто создам общую функцию, которая будет работать как для Double, так и для Decimal.
// Dtype is either Decimal or Double
typealias Dtype = SignedNumeric
func mylog2<T: Dtype>(_ x: T) -> Double
{
let x_double: Double = Double(truncating: x as NSNumber) //'T' is not convertible to 'NSNumber'
return log2(x_double)
}
Но я получаю сообщение об ошибке, поэтому решил просто разделить их на отдельные функции. Подумав об этом, я пришел к выводу, что T не может раскрыть свой настоящий Тип. Затем я решил разделить функцию на отдельные типы и попытаться использовать ее таким образом:
func mylog2(_ x: Decimal) -> Double
{
let x_double: Double = Double(truncating: x as NSNumber)
return log2(x_double)
}
func mylog2(_ x: Double) -> Double
{
return log2(x)
}
Но тогда я не знаю, как заставить T использовать функцию mylog2() дискретными типами:
func myExample<T: Dtype>(_ a: T, _ b: T) -> Double
{
let c: T = (a + b) * (b - a)
//bunch of other maths funcs
return mylog2(c) //No exact matches in call to global function 'mylog2'
}
Я почти уверен, что в других языках есть способы справиться с подобными ситуациями, поэтому такой способ должен быть. Возможно, я подошел к проблеме с неправильным мышлением Swift, и есть совершенно другой способ, о котором я не знаю, как это сделать. В любом случае, если кто-то может помочь мне с быстрым способом решения этой проблемы, я буду признателен.
Я знаю, что это всего лишь пример, но когда вы обнаруживаете, что берете двоичный журнал десятичной дроби, создается ощущение, что вы взяли не те инструменты. Есть много способов сделать то, что вы описываете, в Swift, но все они требуют большого количества кода. Вы просто перемещаете код, потому что в конечном итоге он должен существовать. Большинство языков, которые поддерживают что-то вроде myExample
, делают это из-за сбоя во время выполнения, если вы передаете что-то неожиданное (что, если я передаю Float
, а у вас нет mylog2
для этого?). Swift предпочитает ошибки компилятора сбоям во время выполнения.
С точки зрения математики, большинство языков включают неявное приведение. C автоматически расширяет различные типы, чтобы математика «работала». Это может привести к тонким ошибкам, поэтому Swift намеренно решил не делать этого. 1 + 1.0
недействителен в Swift по своей природе. Другие языки, такие как JavaScript, предпочитают просто конвертировать все в Double. Это также приводит к появлению странных ошибок для больших чисел и оказывает серьезное влияние на производительность. Swift выбрал способ, который сложнее кодировать, но который легче обеспечить правильность и производительность. Это был компромисс. Свифт имеет тенденцию быть педантичным в отношении вещей; это хорошо и плохо в разное время.
Привет @Rob Napier, я ценю вопрос! Да, я имею в виду перейти от Log2() -> Double. Причина этого в том, что в этот момент я превращаю числа в визуальное представление (т. е. цветовой код), и все эти конечные данные больше не нужны. Вероятно, мне следовало бы начать с более убедительного примера «func / () -> Self» (функция оператора разделения), который бы возвращал результат, как вы и ожидали, сохраняя непрерывный тип данных. С этим нужно обращаться так же, как с моим myLog2(), потому что не существует общего протокола, который сочетал бы оператор деления с Decimal и Double.
Просто объявите свой собственный протокол, который требует реализации метода log2
. Соответствуйте Double
и Decimal
этому протоколу.
protocol Log2 {
func log2() -> Double
}
extension Decimal: Log2 {
func log2() -> Double {
Darwin.log2(Double(truncating: self as NSNumber))
}
}
extension Double: Log2 {
func log2() -> Double {
Darwin.log2(self)
}
}
Затем вы можете ограничить параметр универсального типа значением Log2 & SignedNumeric
.
typealias Dtype = SignedNumeric & Log2
func myExample<T: Dtype>(_ a: T, _ b: T) -> Double
{
let c: T = (a + b) * (b - a)
//bunch of other maths funcs
return c.log2()
}
Альтернативно создайте Dtype
собственный протокол, заменив Log2
.
protocol Dtype: SignedNumeric {
func log2() -> Double
// other functions you need on both Double and Decimal goes here...
}
И, вероятно, стоит сделать Dtype
собственный протокол, а не типалиас, поскольку я уверен, что их будет больше, чем этот.
Огромное спасибо @Sweeper. Я не знал, что такие вещи можно делать по протоколу. Это определенно открывает для меня новые двери. Идеальный!
На ваш вопрос «как думать в Swift» это действительно сердце Swift. Все дело в определении протоколов, которые соответствуют потребностям ваших алгоритмов, написании общих алгоритмов на основе этих протоколов, а затем согласовании типов с этими протоколами, часто задним числом (т. е. не как часть определения), чтобы указать, какие типы могут участвовать. Это совсем другой подход, чем во многих языках, использующих наследование. Вам не нужно заранее решать, какие типы вы можете поддерживать. Вы просто решаете, какое поведение типов вам нужно, а затем соединяете их вместе. Желаю удачи в этом.
Вы действительно хотите вернуть Double из
myLog2
? Это, вероятно, вызовет настоящую проблему. Я думаю, вам захочется вернуть Self, чтобы Decimal оставался Decimal (даже если вы выполняете вычисления в Double). Тем не менее, я бы очень хорошо подумал о поддержке как десятичной, так и двоичной плавающей запятой в одном и том же коде. Обычно они решают очень разные проблемы, поэтому я всегда с подозрением отношусь, когда люди пытаются их слишком сильно объединить; какова конечная цель? Swift плохо поддерживает Decimal (это пережиток ObjC, а не основной тип Swift).