Trying to extend IntegerType (and FloatingPointType); Why can't all Int types be converted to NSTimeInterval

孤人 提交于 2019-12-13 13:15:07

问题


(This probably needs a better title...)

I would like to have a set of accessors I can use in code to quickly express time durations. E.g:

42.seconds
3.14.minutes
0.5.hours
13.days

This post illustrates that you can't just do it with a simple new protocol, extension, and forcing IntegerType and FloatingPointType to adopt that. So I thought I'd just go the more redundant route and just extend IntegerType directly (and then repeat the code for FloatingPointType).

extension IntegerType {
    var seconds:NSTimeInterval {
        return NSTimeInterval(self)
    }
}

The error generated is confusing:

Playground execution failed: /var/folders/2k/6y8rslzn1m95gjpg534j7v8jzr03tz/T/./lldb/9325/playground131.swift:31:10: error: cannot invoke initializer for type 'NSTimeInterval' with an argument list of type '(Self)'
                return NSTimeInterval(self)
                       ^
/var/folders/2k/6y8rslzn1m95gjpg534j7v8jzr03tz/T/./lldb/9325/playground131.swift:31:10: note: overloads for 'NSTimeInterval' exist with these partially matching parameter lists: (Double), (UInt8), (Int8), (UInt16), (Int16), (UInt32), (Int32), (UInt64), (Int64), (UInt), (Int), (Float), (Float80), (String), (CGFloat), (NSNumber)
                return NSTimeInterval(self)

What confuses me is that it seems to say that I can't do an NSTimeInterval() initializer with (Self), but everything Self represents is listed in the next line where it shows all of the possible initializers of NSTimeInterval. What am I missing here?

Aside: I would love it if there were a well written tutorial on Swift's type system and doing these kinds of things. The intermediate/advanced stuff is just not well covered in Apple's sparse Swift documentation

Update/Clarification:

What I want is to be able to evaluate any of the above expressions:

42.seconds   --> 42
3.14.minutes --> 188.4
0.5.hours    --> 1800
13.days      --> 1123200

Furthermore, I want the return type of these to be NSTimeInterval (type alias for Double), such that:

42.seconds is NSTimeInterval   --> true
3.14.minutes is NSTimeInterval --> true
0.5.hours is NSTimeInterval    --> true
13.days is NSTimeInterval      --> true

I know that I can achieve this by simply extending Double and Int as such:

extension Int {
    var seconds:NSTimeInterval {
        return NSTimeInterval(self)
    }

    var minutes:NSTimeInterval {
        return NSTimeInterval(self * 60)
    }

    var hours:NSTimeInterval {
        return NSTimeInterval(self * 3600)
    }

    var days:NSTimeInterval {
        return NSTimeInterval(self * 3600 * 24)
    }
}

extension Double {
    var seconds:NSTimeInterval {
        return NSTimeInterval(self)
    }

    var minutes:NSTimeInterval {
        return NSTimeInterval(self * 60)
    }

    var hours:NSTimeInterval {
        return NSTimeInterval(self * 3600)
    }

    var days:NSTimeInterval {
        return NSTimeInterval(self * 3600 * 24)
    }
}

But I would also like the following expression to work:

let foo:Uint = 4242
foo.minutes                   --> 254520
foo.minutes is NSTimeInterval --> true

This won't work though because I have only extended Int, not UInt. I could redundantly extend Uint, and then UInt16, and then Int16, etc....

I wanted to generalize the extension of Int to IntegerType as shown in the original listing, so that I could just gain the conversions generally for all integer types. And then do the same for FloatingPointType rather than specifically Double. However, that produces the original error. I want to know why I can't extend IntegerType as generally shown. Are there other IntegerType adopters other than the ones shown in the list, that make it so the NSTimeInterval() initializer does not resolve?


回答1:


I could redundantly extend Uint, and then UInt16, and then Int16, etc....

Correct. That is how it's done today in Swift. You can't declare that a protocol conforms to another protocol in an extension. ("Why?" "Because the compiler doesn't allow it.")

But that doesn't mean you have to rewrite all the implementation. You currently have to implement it three times (four times if you want Float80, but that doesn't seem useful here). First, you declare your protocol.

import Foundation

// Declare the protocol
protocol TimeIntervalConvertible {
    func toTimeInterval() -> NSTimeInterval
}

// Add all the helpers you wanted
extension TimeIntervalConvertible {
    var seconds:NSTimeInterval {
        return NSTimeInterval(self.toTimeInterval())
    }

    var minutes:NSTimeInterval {
        return NSTimeInterval(self.toTimeInterval() * 60)
    }

    var hours:NSTimeInterval {
        return NSTimeInterval(self.toTimeInterval() * 3600)
    }

    var days:NSTimeInterval {
        return NSTimeInterval(self.toTimeInterval() * 3600 * 24)
    }
}

// Provide the implementations. FloatingPointType doesn't have an equivalent to
// toIntMax(). There's no toDouble() or toFloatMax(). Converting a Float to
// a Double injects data noise in a way that converting Int8 to IntMax does not.
extension Double {
    func toTimeInterval() -> NSTimeInterval { return NSTimeInterval(self) }
}

extension Float {
    func toTimeInterval() -> NSTimeInterval { return NSTimeInterval(self) }
}

extension IntegerType {
    func toTimeInterval() -> NSTimeInterval { return NSTimeInterval(self.toIntMax()) }
}

// And then we tell it that all the int types can get his implementation
extension Int: TimeIntervalConvertible {}
extension Int8: TimeIntervalConvertible {}
extension Int16: TimeIntervalConvertible {}
extension Int32: TimeIntervalConvertible {}
extension Int64: TimeIntervalConvertible {}
extension UInt: TimeIntervalConvertible {}
extension UInt8: TimeIntervalConvertible {}
extension UInt16: TimeIntervalConvertible {}
extension UInt32: TimeIntervalConvertible {}
extension UInt64: TimeIntervalConvertible {}

This is how number types are currently done in Swift. Look through stdlib. You'll see lots of stuff like:

extension Double {
    public init(_ v: UInt8)
    public init(_ v: Int8)
    public init(_ v: UInt16)
    public init(_ v: Int16)
    public init(_ v: UInt32)
    public init(_ v: Int32)
    public init(_ v: UInt64)
    public init(_ v: Int64)
    public init(_ v: UInt)
    public init(_ v: Int)
}

Would it be nice in some cases to talk about "number-like things?" Sure. You can't in Swift today.

"Why?"

Because the compiler doesn't implement it. Some day it may. Until then, create the extension for every type you want it on. A year ago this would have taken even more code.

Note that while some of this is "Swift doesn't have that feature yet," some is also on purpose. Swift intentionally requires explicit conversion between number types. Converting between number types can often lead to losing information or injecting noise, and has historically been a source of tricky bugs. You should be thinking about that every time you convert a number. For example, there's the obvious case that going from Int64 to Int8 or from UInt8 to Int8 can lose information. But going from Int64 to Double can also lose information. Not all 64-bit integers can be expressed as a Double. This is a subtle fact that burns people quite often when dealing with very large numbers, and Swift encourages you to deal with it. Even converting a Float to a Double injects noise into your data. 1/10 expressed as a Float is a different value than 1/10 expressed as a Double. When you convert the Float to a Double, did you mean to extend the repeating digits or not? You'll introduce different kinds of error depending on which you pick, so you need to pick.

Note also that your .days can introduce subtle bugs depending on the exact problem domain. A day is not always 24 hours. It can be 23 hours or 25 hours depending on DST changes. Sometimes that matters. Sometimes it doesn't. But it's a reason to be very careful about treating "days" as though it were a specific number of seconds. Usually if you want to work in days, you should be using NSDate, not NSTimeInterval. I'd be very suspicious of that particular one.

BTW, you may be interested in my old implementation of this idea. Rather than use the syntax:

1.seconds

I used the syntax:

1 * Second

And then overloaded multiplication to return a struct rather than a Double. Returning a struct this way gives much better type safety. For example, I could type-check "time * frequency == cycles" and "cycles / time == frequency", which is something you can't do with Double. Unfortunately NSTimeInterval isn't a separate type; it's just another name for Double. So any method you put on NSTimeInterval is applied to every Double (which is sometimes weird).

Personally I'd probably solve this whole problem this way:

let Second: NSTimeInterval = 1
let Seconds = Second
let Minute = 60 * Seconds
let Minutes = Minute
let Hour = 60 * Minutes
let Hours = Hour

let x = 100*Seconds

You don't even need to overload the operators. It's already done for you.




回答2:


NSTimeInterval is just an alias for Double. You can easily achieve what you want by defining your own struct, like this:

struct MyTimeInterval {
    var totalSecs: NSTimeInterval

    var totalHours: NSTimeInterval {
        get {
            return self.totalSecs / 3600.0
        }
        set {
            self.totalSecs = newValue * 3600.0
        }
    }

    init(_ secs: NSTimeInterval) {
        self.totalSecs = secs
    }

    func totalHourString() -> String {
        return String(format: "%.2f hours", arguments: [self.totalHours])
    }
}

var t = MyTimeInterval(5400)
print(t.totalHourString())



回答3:


extension Int {
    var seconds: Int {
            return self
        }
    var minutes: Int {
        get {
            return self * 60
        }
    }
    var hours: Int {
        get {
            return minutes * 60
        }
    }
    var timeString: String {
        get {
            let seconds = abs(self) % 60
            let minutes = ((abs(self) - seconds) / 60) % 60
            let hours = ((abs(self) - seconds) / 3660)
            let sign = self < 0 ? "-" :" "
            let str = String(format: "\(sign)%2d:%02d:%02d", arguments: [Int(hours),Int(minutes),Int(seconds)])
            return str
        }
    }
}

6.minutes == 360.seconds                    // true

let t1 = 1.hours + 22.minutes + 12.seconds
let t2 = 12.hours - 15.minutes
let time = t1 - t2

print("t1    =",t1.timeString,t1)             // t1    =   1:22:12 4932
print("t2    =",t2.timeString,t2)             // t2    =  11:45:00 42300
print("t1-t2 =",time.timeString,time)         // t1-t2 = -10:22:48 -37368

(12.hours / 30.minutes) == 24                 // true


来源:https://stackoverflow.com/questions/33620967/trying-to-extend-integertype-and-floatingpointtype-why-cant-all-int-types-be

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!