The last section looked at overriding Equals() and provided the guideline that the class should also implement == and !=. The term for implementing any operator is operator overloading, and this section describes how to do this, 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 any type. Virtually all operators are supported, except for assignment operators; there is no way to change the behavior of the = operator.
Comparison Operators (==, !=, <, >, <=, >=)
Once Equals() is overridden, there is a possible inconsistency. Two objects could return true for Equals() but false for the == operator because == performs a reference equality check by default as well. To correct this it is important to overload the equals (==) and not equals (!=) operators as well.
For the most part, the implementation for these operators can delegate the logic to Equals(), or vice versa. However, some initial null checks are required first (see Listing 9.6).
Listing 9.6. Implementing the == and != Operators
Note that to perform the null checks, you cannot use an equality check for null (leftHandSide == null). Doing so would recursively call back into the method, resulting in a loop until overflowing the stack. To avoid this you call ReferenceEquals() to check for null.
Binary Operators (+, -, *, /, %, &,|, ^, <<,>>)
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 9.7 shows.
Listing 9.7. Adding an Operator
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 prefixed by the word operator as a keyword. As shown in Listing 9.8, given the definition of the - and + binary operators, you can add and subtract an Arc to and from the coordinate.
Listing 9.8. Calling the and + Binary Operators
The results of Listing 9.8 appear in Output 9.3.
The and + operators on Coordinate return a third coordinate after subtracting Arc. This allows you to string multiple operators and operands together, as in result = coordinate1 + coordinate2 + coordinate3 coordinate4;.
This works because the result of the first operand (coordinate1 + coordinate2) is another Coordinate, which you can then add to the next operand.
In contrast, consider 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 and, therefore, you could not string operators and operands. Caution is in order when defining operators that behave this way, because doing so is counterintuitive.
Combining Assignment with Binary Operators (+=, -=,*=, /=, %=, &=...)
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 9.7, therefore, you can have code such as:
coordinate += arc;
which is equivalent to the following:
coordinate = coordinate + arc;
Conditional Logical Operators (&&, ||)
Like assignment operators, conditional logical operators cannot be overloaded explicitly. However, since 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 falsein an if statement, for exampleit is necessary to override the TRue/false unary operators.
Unary Operators (+, -, !, ~, ++, --, true, false)
Overloading unary operators is very similar to overloading binary operators, except that they take only one parameter, also of the containing type. Listing 9.9 overloads the + and operators for Longitude and Latitude and then uses these operators when overloading the same operators in Arc.
Listing 9.9. Overloading the and + Unary Operators
Just as with numeric types, the + operator in this listing doesn't have any effect and is provided for symmetry.
Overloading true and false has the additional requirement that they both be overloaded. The signatures are the same as other operator overloads; however, the return must be a bool, as demonstrated in Listing 9.10.
Listing 9.10. Overloading the true and false Operators
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 alternate 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 to handle the casting of one type to another. Furthermore, the method declaration allows for specifying whether the cast is implicit or explicit.
Defining a conversion operator is similar in style to defining any other operator, except that the "operator" is the data type of the conversion. Additionally, the operator keyword follows a keyword that indicates whether the conversion is implicit or explicit (see Listing 9.11).
Listing 9.11. Providing an Implicit Conversion between Latitude and double
With these conversion operators, you now can cast 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 (for example, coordinate = new Coordinate(43, 172);).
Guidelines for Conversion Operators
The difference between defining an implicit and an explicit conversion operator centers on preventing an unintentional implicit cast 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 appropriate 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 that you ensure that the format is correct, or 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.
Either the return or the parameter must be of the enclosing typein support of encapsulation. C# does not allow you to specify conversions outside the scope of the converted type.
A second consideration is the fact 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 not successfully convert back to the original type should be defined as explicit.
Referencing Other Assemblies
Instead of placing all code into one monolithic binary file, C# and the underlying CLI platform allow you to spread code across multiple assemblies. This enables you to reuse assemblies across multiple executables.
To reuse the code within a different assembly, it is necessary to reference the assembly when running the C# compiler. Generally, the referenced assembly is a class library, and creating a class library requires a different assembly target from the default console executable targets you created thus far.
Changing the Assembly Target
The compiler allows you to create four different assembly types via the /target option.
Assemblies to be shared across multiple applications are generally compiled as class libraries. Consider, for example, a library dedicated to functionality around longitude and latitude coordinates. To compile the Coordinate, Longitude, and Latitude classes into their own library, you use the command line shown in Output 9.4.
Assuming you use .NET and the C# compiler is in the path, this builds an assembly library called Coordinates.dll.
Encapsulation of Types
Just as classes serve as an encapsulation boundary for behavior and data, assemblies provide a similar boundary among groups of types. Developers can break a system into assemblies and then share those assemblies with multiple applications or integrate them with assemblies provided by third parties.
By default, a class without any access modifier is defined as internal. The result is that the class is inaccessible from outside the assembly. Even though another assembly references the assembly containing the class, all internal classes within the referenced assemblies will be inaccessible.
Just as private and protected provide levels of encapsulation to members within a class, C# supports the use of access modifiers at the class level for control over the encapsulation of the classes within an assembly. The access modifiers available are public and internal, and in order to expose a class outside of the assembly, the class must be marked as public. Therefore, before compiling the Coordinates.dll assembly, it is necessary to modify the type declarations as public (see Listing 9.12).
Listing 9.12. Making Classes Available Outside an Assembly
Similarly, declarations such as class and enum can also be either public or internal.
The internal access modifier is not limited to type declarations. It is also available on type members. Therefore, you can designate a type as public but mark specific methods within the type as internal so that the members are available only from within the assembly. It is not possible for the members to have a greater accessibility than the type. If the class is declared as internal, then public members on the type will be accessible only from within the assembly.
protected internalis another type member access modifier. Members with an accessibility modifier of protected internal will be accessible from all locations within the containing assembly and from classes that derive from the type, even if the derived class is not in the same assembly.
Referencing an Assembly
To access code within a different assembly, the C# compiler allows the developer to reference the assembly on the command line. The option is / reference (/r is the abbreviation), followed by the list of references. The Program class listing from Listing 9.8 uses the Coordinate class, and if you place this into a separate executable, you reference Coordinates.dll using the .NET command line shown in Output 9.5.
The Mono command line appears in Output 9.6.