NSDecimalNumber bridging inconsistency?

I'm current working in an old Obj-C codebase and needing to bridge much more than I ever had before. One issue I've run into is that NSDecimalNumber bridging seems inconsistent. Take a Swift type:

@objc
final class Info: NSObject {
    let money: Decimal

    init(money: Decimal) {
        self.money = money
    }
}

Now, from Obj-C, info.money is NSDecimalNumber. But if I try to call [[Info alloc] initWithMoney:someNSDecimalNumber] I get the error: Sending 'NSDecimalNumber *__strong' to parameter of incompatible type 'NSDecimal'. Calling .decimalValue fixes that issue. Is this intentional? It seems undesirable for the different directions of bridging to return different types.

That does seem wrong to me; info.money should definitely be an NSDecimal. I wonder if we can fix this without breaking source compat, though. Can you file a bug, at least?

I'll double check.

Looks like my initial report was in error as I was trying to switch from NSNumber to Decimal on the Swift side.

This may actually be a documentation issue. The Swift version of the NSDecimalNumber documentation says, at the very top:

An object for representing and performing arithmetic on base-10 numbers that bridges to Decimal; use NSDecimalNumber when you need reference semantics or other Foundation-specific behavior.

Also, lower down on the page, it says:

The Swift overlay to the Foundation framework provides the Decimal structure, which bridges to the NSDecimalNumber class. For more information about value types, see Working with Cocoa Frameworks in Using Swift with Cocoa and Objective-C (Swift 4.1).

In the Obj-C version of the docs:

The Swift overlay to the Foundation framework provides the NSDecimal structure, which bridges to the NSDecimalNumber class. For more information about value types, see Working with Cocoa Frameworks in Using Swift with Cocoa and Objective-C (Swift 4.1).

However, in a test project, this Obj-C type:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ObjCInfo : NSObject

@property (nonatomic) NSDecimalNumber *value;

@end

NS_ASSUME_NONNULL_END

has this Swift 4.2 interface, as shown in Xcode 10.1:

import Foundation

open class ObjCInfo : NSObject {
    open var value: NSDecimalNumber
}

which seems to have no bridging at all!

However, this Swift type:

@objc
class Info: NSObject {
    @objc
    var value: Decimal
    
    @objc
    init(value: Decimal) {
        self.value = value
    }
}

generates this in the -Swift.h:

SWIFT_CLASS("_TtC13DecimalTester4Info")
@interface Info : NSObject
@property (nonatomic) NSDecimal value;
- (nonnull instancetype)initWithValue:(NSDecimal)value OBJC_DESIGNATED_INITIALIZER;
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_DEPRECATED_MSG("-init is unavailable");
@end

Attempting to run this code:

let objCInfo = ObjCInfo()
objCInfo.value = 10
        
let swiftInfo = Info(value: 10)
swiftInfo.value = objCInfo.value

results in the error: 'NSDecimalNumber' is not implicitly convertible to 'Decimal'; did you mean to use 'as' to explicitly convert?

Try to do the opposite:

objCInfo.value = swiftInfo.value

results in the error: Cannot assign value of type 'Decimal' to type 'NSDecimalNumber', suggesting to initialize the NSDecimalNumber with swiftInfo.value.

The errors are expected, if no bridging is occurring between NSDecimalNumber and Decimal. At this point I'm confused, as it appears there's no bridging at all for NSDecimalNumber.

Also, the Working with Foundation Types document specifically maps Decimal to NSDecimalNumber. So it seems this bridge should exist but doesn't?

They have bridging in the "you can convert between these types" sense but not the "one type is automatically imported as the other" sense. That's the same thing that NSInteger/Int and NSNumber have, for example.

That doesn’t seem to be what the documentation is saying, especially the working guide. It says Swift’s numeric types bridge to NSNumber and Decimal bridges to NSDecimalNumber. As far as I can see, everything in that list is bridged directly.

That's a good point. @krilnon, do you think it'd be worth distinguishing the two kinds of bridging? We certainly don't want to give the impression that NSNumbers will come in as 'Int' or 'Double'.

I mean, it’s clear to me the document is talking about going from Swift to Obj-C, but indicating the inverse relationship would be good too. It does seem like there should be some other word for non-symmetric bridging.

Ah, it's still symmetric. There are (well, should be) no cases where there's an Objective-C class 'Foo' that's imported as Swift type 'Bar', but then shows up in the generated header as 'Bar'*, and there are no cases where the Objective-C class 'Foo' shows up as 'Foo' in Swift but then shows up in the generated header as another type 'Bar'.

The rule is pretty much "if the struct type exists in C, it isn't automatically mapped to the bridged type". That handles all the NSNumber and NSValue cases as well as NSDecimalNumber.

* There is the curious case of BOOL, bool, and Boolean, which Swift tries to map to Bool whenever it knows the mapping is reversible, but isn't great at going the other way. But that doesn't involve a class type.

So this is solely a documentation issue? From what you'e said:

  1. NSDecimalNumber doesn't bridge into Swift at all. You either use it directly or have to convert between types.
  2. NSDecimal bridges to Decimal and vice versa.

If those conclusions are correct, it seems every bit of documentation around NSDecimalNumber is wrong.

No, you're using the word "bridging" differently than I am (and differently than the documentation is). We're using "bridging" to refer to the behavior of as coercions and as? dynamic casts to convert between otherwise unrelated types, which is especially important for dictionaries that may contain value types when they're converted to and from NSDictionaries.

I usually go with "importing" or "mapping" for the other behavior. That's something that covers things like "ptrdiff_t is imported as Int" as well as "NSString is imported as String".

To put it another way, "being bridged" is a capability, just like it is for CF/NS "toll-free bridging".

EDIT: I'll admit to additional confusion from us not separating these two concepts so much in the implementation. The Clang attribute that says "please map this type to a Swift type" is (currently) spelled swift_bridge, even though that doesn't guarantee anything about the as behavior.

It seems to me like all of the documentation needs to be updated to make that distinction then, as every Foundation reference type that has a value equivalent has the same disclaimer about the the type it "bridges to". In every other case I can find, this relation is both for bridging as you've defined it, and value type mapping, except in the case of NSDecimalNumber. In that case, only the bridging through as is there, not the value type mapping, which seems to be what most of the documentation is actually talking about. Or in the short term, just making the distinction clear for NSDecimalNumber?

I listed several bridges where that is not the case: NSNumber/Double, NSNumber/Int, and NSValue/NSRange.

The documentation for NSNumber, NSValue, and NSRange don't call out any particular bridging or mapping to Swift (which is actually a bit awkward, and there are a lot of questions on SO about those types). The "Working with Foundation Types" document does call out Swift numeric types -> NSNumber, so that may something that needs to be updated as well.

Unless you don't think this is a documentation issue either?

I think the fact that we're having this conversation is an indication that the documentation has room for improvement. :-) I just wanted to make it clear that NSDecimalNumber isn't being treated specially; we already had to work out this model for NSNumber.

I think it makes sense to call the user-passive case something other than "bridging".

1 Like