Well-Formed Types
The previous chapters covered most of the constructs for defining records and value types (structs and enums). However, several details remain to round out the type definition with fit-and-finish-type functionality. This chapter explains how to put the final touches on a type declaration, which includes such concepts as operator overloading, referencing other libraries, additional access modifiers, namespaces, inline documentation with XML comments, and resource management.
The preceding chapter looked at implementing value equality behavior and provided the guideline that the class should also implement == and !=. Implementing any operator is called operator overloading. This section describes how to perform such overloading not only for == and !=, but also for other supported operators.
For example, string provides a + operator that concatenates two strings. This is perhaps not surprising, because string is a predefined type, so it could possibly have special compiler support. However, C# provides for adding + operator support to a class or struct. In fact, all operators are supported except x.y, f(x), new, typeof, default, checked, unchecked, delegate, is, as, =, and =>. One particularly noteworthy operator that cannot be implemented is the assignment operator; there is no way to change the behavior of the = operator.
Before going through the exercise of implementing an operator overload, consider the fact that such operators are not discoverable through IntelliSense. Unless the intent is for a type to act like a primitive type (e.g., a numeric type), you should avoid overloading an operator as the only means of accessing the behavior.
In Chapter 9 we reviewed the code generated for a record’s implementation of the == and != operators. In addition, you can override the greater and less than operators, including when combined with equality checks (see Listing 10.1).
Note that the compiler enforces implementing these comparison operators in pairs (==, !=), (<, >), (<=, >=). The implementation in Listing 10.1 relies on one pair element calling the other. Also, for reference types, be sure to handle null appropriately—before invoking any of the members. You should never throw an exception from within an operator overloading implementation.
You can add an Arc to a Coordinate. However, the code so far provides no support for the addition operator. Instead, you need to define such a method, as Listing 10.2 demonstrates.
The +, -, *, /, %, &, |, ^, <<, and >> operators are implemented as binary static methods, where at least one parameter is of the containing type. The method name is the operator symbol prefixed by the keyword operator. As shown in Listing 10.3 with Output 10.1, given the definition of the - and + binary operators, you can add and subtract an Arc to and from the coordinate. Note that Longitude and Latitude will also require implementations of the + operator because they are called by source.Longitude + arc.LongitudeDifference and source.Latitude + arc.LatitudeDifference.
For Coordinate, you implement the – and + operators to return coordinate locations after adding/subtracting Arc. This allows you to string multiple operators and operands together, as in result = ((coordinate1 + arc1) + arc2) + arc3. Moreover, by supporting the same operators (+/-) on Arc (see Listing 10.4 later in this chapter), you could eliminate the parentheses. This approach works because the result of the first operand (arc1 + arc2) is another Arc, which you can then add to the next operand of type Arc or Coordinate.
In contrast, consider what would happen if you provided a – operator that had two Coordinates as parameters and returned a double corresponding to the distance between the two coordinates. Adding a double to a Coordinate is undefined, so you could not string together operators and operands. Caution is in order when defining operators that return a different type, because doing so is counterintuitive.
As previously mentioned, there is no support for overloading the assignment operator. However, assignment operators in combination with binary operators (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, and >>=) are effectively overloaded when overloading the binary operator. Given the definition of a binary operator without the assignment, C# automatically allows for assignment in combination with the operator. Using the definition of Coordinate in Listing 10.2, therefore, you can have code such as
coordinate += arc;
which is equivalent to the following:
coordinate = coordinate + arc;
Like assignment operators, conditional logical operators cannot be overloaded explicitly. However, because the logical operators & and | can be overloaded, and the conditional operators comprise the logical operators, effectively it is possible to overload conditional operators. x && y is processed as x & y, where y must evaluate to true. Similarly, x || y is processed as x | y only if x is false. To enable support for evaluating a type to true or false—in an if statement, for example—it is necessary to override the true/false unary operators.
Overloading unary operators is very similar to overloading binary operators, except that they take only one parameter, also of the containing type. Listing 10.4 overloads the + and – operators for Longitude and Latitude and then uses these operators when overloading the same operators in Arc.
Just as with numeric types, the + operator in this listing doesn’t have any effect and is provided for symmetry.
Overloading the true and false operators is subject to the additional requirement that both must be overloaded—not just one of the two. The signatures are the same as with other operator overloads; however, the return must be a bool, as demonstrated in Listing 10.5.
You can use types with overloaded true and false operators in if, do, while, and for controlling expressions.
Currently, there is no support in Longitude, Latitude, and Coordinate for casting to an alternative type. For example, there is no way to cast a double into a Longitude or Latitude instance. Similarly, there is no support for assigning a Coordinate using a string. Fortunately, C# provides for the definition of methods specifically intended to handle the converting of one type to another. Furthermore, the method declaration allows for specifying whether the conversion is implicit or explicit.
Implementing the explicit and implicit conversion operators is not technically overloading the cast operator (()). However, this action is effectively what takes place, so defining a cast operator is common terminology for implementing explicit or implicit conversion.
Defining a conversion operator is similar in style to defining any other operator, except that the “operator” is the resultant type of the conversion. Additionally, the operator keyword follows a keyword that indicates whether the conversion is implicit or explicit (see Listing 10.6).
With these conversion operators, you now can convert doubles implicitly to and from Latitude objects. Assuming similar conversions exist for Longitude, you can simplify the creation of a Coordinate object by specifying the decimal degrees portion of each coordinate portion (e.g., coordinate = new Coordinate(43, 172);).
The difference between defining an implicit and an explicit conversion operator centers on preventing an unintentional implicit conversion that results in undesirable behavior. You should be aware of two possible consequences of using the explicit conversion operator. First, conversion operators that throw exceptions should always be explicit. For example, it is highly likely that a string will not conform to the format that a conversion from string to Coordinate requires. Given the chance of a failed conversion, you should define the particular conversion operator as explicit, thereby requiring that you be intentional about the conversion and ensure that the format is correct or, alternatively, that you provide code to handle the possible exception. Frequently, the pattern for conversion is that one direction (string to Coordinate) is explicit and the reverse (Coordinate to string) is implicit.
A second consideration is that some conversions will be lossy. Converting from a float (4.2) to an int is entirely valid, assuming an awareness of the fact that the decimal portion of the float will be lost. Any conversions that will lose data and will not successfully convert back to the original type should be defined as explicit. If an explicit cast is unexpectedly lossy or invalid, consider throwing a System.InvalidCastException.