How to convert from NSNumber to .NET Object

202 Views Asked by At

When working with .NET iOS targets (ie. net6.0-ios, net6.0-maccatalyst, Xamarin, MAUI, etc.), there are methods on NSNumber for converting to a specific .NET type. For example, .BooleanValue, .Int32Value, .DoubleValue, etc.

But what should one do if you don't know the type in advance, and just want a .NET object (boxing the actual bool, int, double, or whatever)?

1

There are 1 best solutions below

1
Matt Johnson-Pint On

Here's an extension method that I came up with. It uses the ObjCType encoded values, and handles bool and decimal values properly.

It's not perfect though. For example, NSNumber.FromByte(255).ToObject() gives back an object containing an Int16, so it would appear that while "c" comes through for sbyte values, that byte value give "s" rather than "C". Not sure why.

/// <summary>
/// Converts an <see cref="NSNumber"/> to a .NET primitive data type and returns the result box in an <see cref="object"/>.
/// </summary>
/// <param name="n">The <see cref="NSNumber"/> to convert.</param>
/// <returns>An <see cref="object"/> that contains the number in its primitive type.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <see cref="NSNumber.ObjCType"/> was unrecognized.</exception>
/// <remarks>
/// This method always returns a result that is compatible with its value, but does not always give the expected result.
/// Specifically:
/// <list type="bullet">
///   <item><c>byte</c> returns <c>short</c></item>
///   <item><c>ushort</c> return <c>int</c></item>
///   <item><c>uint</c> returns <c>long</c></item>
///   <item><c>ulong</c> returns <c>long</c> unless it's > <c>long.MaxValue</c></item>
///   <item>n/nu types return more primitive types (ex. <c>nfloat</c> => <c>double</c>)</item>
/// </list>
/// Type encodings are listed here:
/// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
/// </remarks>
public static object ToObject(this NSNumber n)
{
    if (n is NSDecimalNumber d)
    {
        // handle NSDecimalNumber type directly
        return (decimal)d.NSDecimalValue;
    }

    return n.ObjCType switch
    {
        "c" when n.Class.Name == "__NSCFBoolean" => n.BoolValue, // ObjC bool
        "c" => n.SByteValue, // signed char
        "i" => n.Int32Value, // signed int
        "s" => n.Int16Value, // signed short
        "l" => n.Int32Value, // signed long (32 bit)
        "q" => n.Int64Value, // signed long long (64 bit)
        "C" => n.ByteValue, // unsigned char
        "I" => n.UInt32Value, // unsigned int
        "S" => n.UInt16Value, // unsigned short
        "L" => n.UInt32Value, // unsigned long (32 bit)
        "Q" => n.UInt64Value, // unsigned long long (64 bit)
        "f" => n.FloatValue, // float
        "d" => n.DoubleValue, // double
        "B" => n.BoolValue, // C++ bool or C99 _Bool
        _ => throw new ArgumentOutOfRangeException(nameof(n), n,
            $"NSNumber \"{n.StringValue}\" has an unknown ObjCType \"{n.ObjCType}\" (Class: \"{n.Class.Name}\")")
    };
}