Reputation: 4211
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?
InvalidCastException
if macAddress
is null
?ArgumentNullException(nameof(macAddress))
?NullReferenceException
?InvalidCastException
?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
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