LWChris
LWChris

Reputation: 4211

Handling null in custom cast operator

I am trying to add a cast from a class to a struct.

In my concrete case, the to-be-casted source type would be class MacAddress, a class used for string-input validation of the various typical 6-byte-notations such as 0a:3f… or 0A-3F…, conversion of such strings to a byte-array, offering formatted ToString, etc.

The target type would be readonly struct BluetoothAddress, a wrapper for the rather unhandy ulong that the Windows.Devices.Bluetooth APIs return or require, providing DebuggerDisplay, etc.

In principle, every non-null instance of MacAddress will always represent a sequence of any 6 bytes, which can always unambiguously seed a BluetoothAddress struct. Therefore, if I wanted to allow passing a "user-input wrapping MacAddress" into functions that require an "ulong-wrapping BluetoothAddress", I would need an implicit cast like this:

public static implicit operator BluetoothAddress(MacAddress macAddress) {
    var bytes = macAddress.ToByteArray();
    ulong Shift(int i) => ((ulong) bytes[i]) << ((5 - i) * 8);
    ulong value = Shift(0) | Shift(1) | Shift(2) | Shift(3) | Shift(4) | Shift(5);
    return new(value);
}

There's just one problem: accessing .ToByteArray() would throw NRE if macAddress was null.

Guidelines say "DO NOT throw from implicit casts." and "DO throw System.InvalidCastException if a call to a cast operator results in a lossy conversion and the contract of the operator does not allow lossy conversions."

So, how should the cast operator in BluetoothAddress be designed?

  1. Throw an InvalidCastException if macAddress is null?
  2. Throw an ArgumentNullException(nameof(macAddress))?
  3. Let the framework throw the NullReferenceException?
  4. try-catch the NRE and pass it on as inner exception of my own InvalidCastException?
  5. …?

And should it be explicit (because it can throw), or can it still be implicit, because my cast operator will never throw unless you're trying to cast null to this value type (so a throw shouldn't be too unexpected)?

Upvotes: 0

Views: 154

Answers (1)

Jeroen Mostert
Jeroen Mostert

Reputation: 28789

There's no rule that says you can't throw on null when casting between classes, but you probably shouldn't, aside from the general discussion of whether using casts for this kind of operation is a good idea if "interesting" logic is involved (it probably isn't).

null is a valid value for any reference type (nullable reference types notwithstanding, as these are just annotations), regardless of how it's constructed. Throwing an InvalidCastException when converting a null introduces inconsistencies, as no built-in conversion operator between reference types does that. In particular, consider the fact that (TargetClass) (object) default(SourceClass) compiles and runs successfully, whereas (TargetClass) default(SourceClass) would not, under the proposed semantics.

Conversion between value types is a different matter: because null is not a valid value for any value type (except Nullable, but more on that later) you'll never run into this problem casting from value types. If casting to a value type from a reference type, then either InvalidCastException or NullReferenceException would be appropriate -- the former because we're in a cast, the latter because it's what the runtime itself produces when attempting this with no user-defined conversion. ArgumentNullException is technically also correct, though confusing to the caller, as it doesn't appear as an argument at the call site.

Finally, for nullable value types, consider that (TargetClass) default(SourceStruct?) is valid and bypasses your operator altogether, producing a null value. This, too, argues against throwing an exception for the null case from your own operator.

Upvotes: 1

Related Questions