NSCalendar in Swift - init can return nil, but isn't optional

对着背影说爱祢 提交于 2019-12-10 13:56:37

问题


NSCalendar(calendarIdentifier: calendarName) can return nil if calendarName is not valid - this is the original Objective-C behaviour, and is also true in Swift. However, it appears that the compiler believes the initializer returns an NSCalendar rather than an NSCalendar?, as follows:

let c1 = NSCalendar(calendarIdentifier: "gregorian")// _NSCopyOnWriteCalendarWrapper
let c2 = NSCalendar(calendarIdentifier: "buddhist")// _NSCopyOnWriteCalendarWrapper

//let c3:NSCalendar = NSCalendar(calendarIdentifier: "rubbish") // run-time error
let c3:NSCalendar? = NSCalendar(calendarIdentifier: "rubbish") // nil

So if the initializer can return nil, my understanding is that I should be able to do

if let c4 = NSCalendar(calendarIdentifier: "rubbish") as? NSCalendar { 
    //error: conditional downcast from 'NSCalendar' to 'NSCalendar' always succeeds
}

However, this is a compile-time error as shown.

What am I misunderstanding here, and how can I safely test that a named calendar actually exists?


回答1:


Note: The following only applies to Swift 1.0. In Swift 1.1, there are failable initializers.


This works:

if let c4 = NSCalendar(calendarIdentifier: "rubbish") as NSCalendar? { 

}

In Swift 1.0, there is a known issue in the Xcode release notes about Swift not supporting Objective-C initializers that return nil. Basically, what is happening is that according to the Swift language, the expression NSCalendar(...) has type NSCalendar, which is a non-optional type (cannot be nil). However, this is really an imported initializer from Objective-C, where it can return nil.

So what currently happens is that when you call this and it returns nil, you have a value that at runtime is nil, but Swift thinks is a non-optional NSCalendar (which cannot be nil). This is a really bad situation in which you have a value that is not possible for the type. The Xcode release notes mention a "workaround" where you convert the result of the initializer to an optional type before using it. The reason that this works is that at runtime, both optional and non-optional object pointers are represented as simple pointers, where nil object pointer is the null pointer, and non-optional ones are assumed to not be null pointers. The operation of converting from a non-optional to optional object pointer is a simple assignment (in both cases it's a non-null pointer). But in the case where the value is nil, a simple assignment turns it into a (valid) nil value of optional type. So everything is happy.

But if you don't convert it to optional first, all hell breaks loose, as you have a value that is not supposed to be possible for that type.

Your attempt to downcast using as? from NSCalendar to NSCalendar isn't allowed, because a cast from NSCalendar to NSCalendar cannot fail (theoretically).




回答2:


Even though it looks like the initialiser should return a full NSCalendar object, it appears as though it behaves as an implicit optional (NSCalandar!). If you look at the object in the debugger it comes up as an NSCalendar:

(NSCalendar) $R1 = 0x0000000000000000 { ObjectiveC.NSObject = parent is NULL }

Even more strange is that the following code does not produce any runtime errors, when accessing a nil NSCalendar - at least for me:

    let x = NSCalendar(calendarIdentifier: "asdasda")
    let y = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)
    if (x.isDateInToday(NSDate())) {
        println("x works")
    }

    if (y.isDateInToday(NSDate())) {
        println("y works")
    }
    if (x == nil) {
        println("x not calendar")
    }

    if (y == nil) {
        println("y not calendar")
    }

For me this outputs 'y works' and 'x is not a calendar'. This would in pure Swift have a runtime error if using a nil implicit optional but pqnet points out that calling a method on a nil pointer in objective c doesn't produce an error, it is just ignored - which is what seems to be doing. So strange, but it does appear in this case safe to do this:

    let x = NSCalendar(calendarIdentifier: "asdasda")
    if (x != nil) {
        //do what you need with the calendar
    }

However a more Swift way would be to use an explicit optional - i.e. do this:

    let x : NSCalendar? = NSCalendar(calendarIdentifier: "asdasda")
    if let cal = x {
        //do what you need with the calendar as cal
    }

You could also use an implicit optional let x : NSCalendar =... and check with if x != nil as above - your choice, it seems sometimes different contexts cause implicit or explicit optionals...

Edit:

From the Xcode release notes:

Swift does not support object initializers that fail by returning null. (16480364)! Workaround: If there is a factory method, use it instead. Otherwise, capture the result in an optional. For example:

let url: NSURL? = NSURL(string: "not a url")


来源:https://stackoverflow.com/questions/25284182/nscalendar-in-swift-init-can-return-nil-but-isnt-optional

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