User-Defined Implicit and Explicit Conversions

   


Most programs operate on data of a diverse set of types, ranging from the predefined simple types (int, double, and so on) over the derived predefined .NET types (in the Framework class library) to your own custom made types. If an operation adds two int values together and assigns the result to an int variable, the operation stays within the same type (if the result stays within the range of an int). Sometimes, however, we combine different types in the same operations with an accompanying need to convert data from one type to another. As discussed in Chapter 6, "Types, Part I: The Simple Types," typical circumstances that require conversions are as follows:

  • A value of one type is assigned to a variable of another type.

  • Values of different types are combined in the same expression.

  • The type of an argument in a method call is different from the type of its corresponding formal parameter in the method being called.

As long as the conversions only involve the simple types, the compiler feels at home. It follows predefined, exact rules and instructions, discussed at length in Chapter 6, that tell it

  • If an implicit conversion path exists (as specified in the implicit conversion path for simple types) and, if so, how it is performed

  • If an explicit conversion (using the cast operator (<Type>)) is possible and, if so, how it is performed

However, if we involve our own user-defined types in operations that either require conversions among user-defined types and simple types or among different unrelated user defined types, the compiler is clueless unless we explicitly specify, through user-defined (implicit or explicit) conversions, how these conversions are performed.

Inheritance and Assignments

graphics/common.gif

A programmer can, via inheritance discussed in Chapter 16, relate different classes to each other and form class hierarchies. Classes with these special relationships may or may not, depending on the context, be assigned to each other without requiring explicitly specified conversion paths. For that reason, I have emphasized that the classes involved in the conversions discussed in this section are unrelated.


Two Cases in Need of User-Defined Conversions

This section presents two typical examples that call for the implementation of user-defined conversions.

In the first scenario, we attempt to assign a variable called simpleTimeSeconds of type uint to an object of the user-defined TimeSpan class, presented in the previous section:

 class TimeSpan {     private uint totalSeconds;     ... } ... class TimeSpanTest {     public static void Main()     {         ...         uint simpleTimeSeconds = 30;         TimeSpan myTime;         myTime = simpleTimeSeconds; }     ... } 

However the line:

 myTime = simpleTimeSeconds; 

is invalid because the compiler does not know how to convert a value of type uint to a value of type TimeSpan.

To us humans an obvious conversion would be to assign the value of simpleTimeSeconds to myTime's totalSeconds, but the compiler does not (yet) show many signs of artificial intelligence, and cannot possibly know how to perform this conversion. Later you will see how the compiler can be instructed to perform the needed conversion by including a user defined implicit conversion in TimeSpan.

The compiler also faces a problem if we try to convert from one user-defined object to another user-defined object. The next example illustrates this.

We are writing a computer program for the space shuttle that will bring us to planet Blipos. Among other things, the program manipulates times on Earth along with times on Blipos. According to Chapter 6, a day on planet Blipos consists of 65535 minutes, a minute is 256 seconds, and a second is exactly the same duration as a second here on Earth. The differences between time measuring on Earth and Blipos prompts us to include two classes called BliposTimeSpan and EarthTimeSpan to represent time spans on Blipos and Earth, respectively. (The EarthTimeSpan is similar to the TimeSpan class previously presented in Listing 14.7). Both time span classes represent a time span internally with an instance variable called totalSeconds, which is of the same type uint, as illustrated in the next code snippet.


graphics/14infig03.gif

However, the last line cannot be performed without any rules for how to convert from one object type to another. In a moment, we will look at how this problem can be solved by specifying a user-defined conversion.

Using Non-User-Defined Conversion Techniques

The problems related to both examples described in the previous section can be solved by including conventional methods or properties to take care of the assignments. For example, instead of writing:

 myTime = simpleTimeSeconds; 

we could use the already existing Seconds property and assign the uint value to myTime by writing the following:

 myTime.Seconds = simpleTimeSeconds; 

In the second example, we could specify a method inside the EarthTimeSpan class called GetAsBliposTimeSpan that would create a new BliposTimeSpan object and return it to the caller. It might look like the following:

 public BliposTimeSpan GetAsBliposTimeSpan() {     BliposTimeSpan returnBlipos = new BliposTimeSpan();     returnBlipos.Seconds = totalSeconds;     return returnBlipos; } 

allowing us to write

 alienTime = myTime.GetAsBliposTimeSpan(); 

and validly achieve what we initially attempted with the following line:

 alienTime = myTime; 

However, just like operator overloading allows us to sprinkle syntactic sugar on our code and substitute method and property calls with arithmetic operators, user-defined conversions allow us to make our code look even sweeter by removing properties and conversion methods, such as those shown in this section, and support the following cleaner looking lines:

 myTime = simpleTimeSeconds; alienTime = myTime; 

The next section looks more closely at how we can specify our conversions in the source code of a C# program.

The Syntax of User-Defined Conversions

When we specify our user-defined conversions, we provide the instructions needed for the compiler to perform type conversions that involve user-defined types. These instructions are similar to those followed by the compiler when it converts from one simple type to another; they answer the following questions:

  • Is an implicit conversion possible from one type to the other and, if so, how is it performed?

  • Is an explicit conversion possible, using the cast operator (<Type>) and, if so, how is it performed? (Note: If an implicit conversion is possible, an explicit conversion is also possible).

The instructions (or statements) are held by a language construct that looks somewhat similar to an operator method, but with either the keyword implicit or the keyword explicit added in the header. Listing 14.8 demonstrates how we can the define an implicit conversion path from a value of type uint to a value of type TimeSpan and, thereby, allow us to write the following line:

 myTime = simpleTimeSeconds; 

as shown in line 34.

Listing 14.8 ImplicitConversionTester.cs
01: using System; 02: 03: class TimeSpan 04: { 05:     private uint totalSeconds; 06: 07:     public TimeSpan(uint initialTotalSeconds) 08:     { 09:         totalSeconds = initialTotalSeconds; 10:     } 11: 12:     public void PrintSeconds() 13:     { 14:         Console.WriteLine("Total seconds: { 0} ", totalSeconds); 15:     } 16: 17:     public static implicit operator TimeSpan(uint convertFrom) 18:     { 19:         TimeSpan newTimeSpan; 20: 21:         newTimeSpan = new TimeSpan(convertFrom); 22:         Console.WriteLine("Converting from uint to TimeSpan"); 23:         return newTimeSpan; 24:     } 25: } 26: 27: class ImplicitConversionTester 28: { 29:     public static void Main() 30:     { 31:         uint simpleTimeSeconds = 30; 32:         TimeSpan myTime; 33: 34:         myTime = simpleTimeSeconds; 35:         myTime.PrintSeconds(); 36:     } 37: } Converting from uint to TimeSpan Total seconds: 30 

This analysis should be read in conjunction with Syntax Box 14.4, shown after this analysis.

For space reasons, only a bare minimum version of TimeSpan is defined in lines 3 25.

A user-defined implicit conversion from uint to TimeSpan is contained in lines 17 24. The keywords implicit and operator in line 17 indicate a user-defined implicit conversion. (As shown in Syntax Box 14.4, we can substitute implicit with the keyword explicit to define a user-defined explicit conversion).

A user-defined conversion must always be declared public and static, as shown in line 17.

The type to which we are converting (in this case, TimeSpan) must follow the operator keyword and the following parentheses must enclose a parameter identifier (here called convertFrom) with the type from which we are converting.

During the assignment of simpleTimeSeconds to myTime in line 34, the implicit converter method is called and the parameter identifier convertFrom (line 17) is assigned the value held by simpleTimeSeconds. Line 17 promises that lines 19 23 create and return a TimeSpan object based on the value in convertFrom; this object is assigned to myTime in line 34. The statements for creating a suitable TimeSpan object in lines 19 23 are based on the following observation: convertFrom is equivalent to a TimeSpan object with its instance variable totalSeconds equal to convertFrom. We follow this recipe by passing convertFrom to the TimeSpan constructor in line 21. newTimeSpan's totalSeconds is thereby set to convertFrom.

Line 22 is merely inserted for demonstration purposes to ascertain for ourselves that this conversion method is, in fact, being called. This is confirmed by the sample output. The return keyword asks the newTimeSpan reference to be returned and, thereby, assigned to myTime in line 34. Line 35 finally causes totalSeconds of myTime to be printed. As expected, the sample output confirms that its value is equal to the value held by simpleTimeSeconds.

Syntax Box 14.4 contains the formal syntax for specifying a user-defined implicit conversion and a user-defined explicit conversion.

Syntax Box 14.4 User Defined Implicit Conversion

 User_defined_implicit_conversion::=  public static implicit operator <Type_convert_to> ( <Type_convert_from> graphics/ccc.gif <Parameter_identifier> ) {       <Statements> } User_defined_explicit_conversion::= public static explicit operator <Type_convert_to> ( <Type_convert_from> graphics/ccc.gif <Parameter_identifier> ) {       <Statements> } 

Notes:

  • The keywords public, static, and operator are always required.

  • Exactly one parameter identifier must be included.

  • The class in which the user-defined conversion resides must be involved in the conversion, because we are either specifying how to convert from a value of this class type or to a value of this class type. As a result either <Type_convert_to> or <Type_convert_from> must have the name of the class in which the conversion resides.

  • Any pre-defined conversions, such as the implicit conversion path for simple numeric types, cannot be redefined.

  • <Statements> must include a return statement that returns a value with the same type as that specified by <Type_convert_to>.

  • The <Type_convert_to> and <Type_convert_from> form the signature of the user-defined conversion. Because the implicit and explicit keywords are not part of the signature, you cannot define an implicit and an explicit conversion with identical signatures within the same class.

Note

graphics/common.gif

Any user-defined implicit conversion automatically allows you to apply the cast operator in an explicit type conversion. For example, we could have written the following instead of line 34 in Listing 14.8:

 34:         myTime = (TimeSpan) simpleTimeSeconds; 

However, it is not possible to perform an implicit conversion with a user-defined explicit conversion path.


Only Specify User-Defined Implicit Conversions for Safe Conversions

graphics/bulb.gif

Recall that the simple types only specify implicit conversion paths when there is no risk of data loss and no risk of executing operations that would cause exceptions to be generated. When these risks are present, C# forces you to perform explicit conversions instead by using the cast operator, which acts as a warning sign in the source code. You should follow this same approach when implementing your own conversions. In Listing 14.8, there was no danger of data loss because we were essentially assigning a value of type uint (simpleTimeSeconds) to a variable also of type uint (totalSeconds from inside TimeSpan). On the other hand, if we specify a conversion path from TimeSpan to a variable of, say, type ushort (as in Listing 14.9), there is a clear risk of data loss caused by ushort's narrower range. Consequently, an explicit conversion is suitable here as demonstrated next.


Listing 14.9 contains the recommended user-defined explicit conversion from TimeSpan to ushort in lines 20 24. As a result, we can apply the type cast in line 34, where myTime is assigned to simpleTimeSeconds, but any implicit conversions are impossible.

Listing 14.9 ExplicitConversionTester.cs
01: using System; 02: 03: class TimeSpan 04: { 05:     private uint totalSeconds; 06: 07:     public TimeSpan(uint initialTotalSeconds) 08:     { 09:         totalSeconds = initialTotalSeconds; 10:     } 11: 12:     public uint TotalSeconds 13:     { 14:         get 15:         { 16:             return totalSeconds; 17:         } 18:     } 19: 20:     public static explicit operator ushort(TimeSpan convertFrom) 21:     { 22:         Console.WriteLine("Converting from TimeSpan to ushort"); 23:         return (ushort)convertFrom.TotalSeconds; 24:     } 25: } 26: 27: class ExplicitConversionTester 28: { 29:     public static void Main() 30:     { 31:         ushort simpleTimeSeconds; 32:         TimeSpan myTime = new TimeSpan(130); 33: 34:         simpleTimeSeconds = (ushort) myTime; 35:         Console.WriteLine("Value of simpleTimeSeconds: { 0} ", 36:             simpleTimeSeconds); 37:     } 38: } Converting from TimeSpan to ushort Value of simpleTimeSeconds: 130 

Instead of being the <Type_convert_to>, as in line 17 of Listing 14.8, TimeSpan (in line 20) is the <Type_convert_from>, ushort is the <Type_convert_to>, and the implicit keyword has been substituted with the explicit keyword in line 20.

Listing 14.10 demonstrates how we can specify a user-defined implicit conversion between the two user-defined types, EarthTimeSpan and BliposTimeSpan and allow the last of the following three lines to be executed:

 EarthTimeSpan myTime = new EarthTimeSpan(130); BliposTimeSpan alienTime; alienTime = myTime; 
Listing 14.10 ConvertingUserDefinedTypes.cs
01: using System; 02: 03: class EarthTimeSpan 04: { 05:     private uint totalSeconds; 06: 07:     public EarthTimeSpan(uint initialTotalSeconds) 08:     { 09:         totalSeconds = initialTotalSeconds; 10:     } 11: 12:     public uint TotalSeconds 13:     { 14:         get 15:         { 16:             return totalSeconds; 17:         } 18:     } 19: 20:     public static implicit operator BliposTimeSpan (EarthTimeSpan convertFrom) 21:     { 22:         BliposTimeSpan newBliposTimeSpan = new BliposTimeSpan  graphics/ccc.gif(convertFrom.TotalSeconds); 23:         Console.WriteLine("Converting from EarthTimeSpan to BliposTimeSpan"); 24:         return newBliposTimeSpan; 25:     } 26: } 27: 28: class BliposTimeSpan 29: { 30:     private uint totalSeconds; 31: 32:     public BliposTimeSpan(uint initialTotalSeconds) 33:     { 34:         totalSeconds = initialTotalSeconds; 35:     } 36: 37:     public void PrintTimeSpan() 38:     { 39:         Console.WriteLine("Blipos time span: { 0}  seconds", totalSeconds); 40:     } 41: } 42:  43: class ExplicitConversionTester 44: { 45:     public static void Main() 46:     { 47:         EarthTimeSpan myTimeSpan = new EarthTimeSpan(200); 48:         BliposTimeSpan alienTimeSpan; 49: 50:         alienTimeSpan = myTimeSpan; 51:         alienTimeSpan.PrintTimeSpan(); 52:     } 53: } Converting from EarthTimeSpan to BliposTimeSpan Blipos time span: 200 seconds 

Both the BliposTimeSpan and the EarthTimeSpan classes are stripped-down versions designed for demonstration purposes.

Lines 20 25 specifies the user-defined implicit conversion. BliposTimeSpan is the <Type_convert_to>, and EarthTimeSpan is the <Type_convert_from>.

Note that any one of the two classes can be equipped with the user-defined conversion definition. Removing lines 20 25 from inside EarthTimeSpan and positioning them inside BliposTimeSpan instead would still allow exactly the same the implicit conversion in line 50 to take place.

Combining User-Defined and Implicit Conversions

In Chapter 6, I mentioned the compiler's ability to move through several simple numeric types on the pre-defined implicit conversion path to perform a requested conversion. For example, the compiler will convert a byte value to a ulong value by moving along the following path, which is an extract from Figure 6.17 in Chapter 6.


graphics/14infig04.gif

This is old news, but it forms the basis for understanding an interesting fact relevant to user-defined conversions: The compiler will combine any implicit conversion path (whether predefined or user-defined) with your user-defined conversions to convert from one type to another. Consequently, if you attempt to convert between two types and a user-defined conversion only specifies part of this path, the compiler will look for an implicit path that completes the journey between the two types. The TimeSpan class and its user-defined explicit conversion in lines 20 24 of Listing 14.9 can be used to illustrate this.

 20:     public static explicit operator ushort(TimeSpan convertFrom) 21:     { 22:         Console.WriteLine("Converting from TimeSpan to ushort"); 23:         return (ushort)convertFrom.TotalSeconds; 24:     } 

This user-defined conversion not only allows us to cast the TimeSpan class into the ushort type, but we can also convert TimeSpan to a uint and a ulong, as specified in Figure 14.5 (or any other type along this implicit conversion path that can be seen in its entirety in Figure 6.17 in Chapter 6), because the compiler will complete the journey along the implicit conversion path from ushort to ulong.

Figure 14.5. Converting from TimeSpan to ulong.
graphics/14fig05.gif

This allows us to write the following

 TimeSpan myTime = new TimeSpan(120); ulong simpleTime; simpleTime = (ushort)myTime; 

which converts myTime of type TimeSpan to simpleTime of type ulong in the third line.

Had we defined another class called GalaxyTimeSpan containing a user-defined implicit conversion from ulong to GalaxyTimeSpan, the implicit conversion path could be depicted as shown in Figure 14.6. We could then convert from TimeSpan to GalaxyTimeSpan, as demonstrated in line three of the following lines:

 TimeSpan myTime = new TimeSpan(500); GalaxyTimeSpan milkyWayGalaxyTime; milkyWayGalaxyTime = (ushort)myTime; 
Figure 14.6. Converting from TimeSpan to GalaxyTimeSpan.
graphics/14fig06.gif

Even though both TimeSpan and GalaxyTimeSpan contains totalSeconds instance variables of the types uint and ulong, with the ability to represent values of considerable magnitude, the conversion always moves through the ushort type where numbers exceeding ushort's range (0 65535) will cause an overflow or underflow. For example, if you initialize totalSeconds of the TimeSpan value myTime to 70000 (as in the next three lines of code), a conversion to milkyWayTime of type GalaxyTimeSpan, shown in line three, results in milkyWayTime with a totalSeconds instance variable holding the value 4464 (70000-65536).

 TimeSpan myTime = new TimeSpan(70000); GalaxyTimeSpan milkyWayTime; milkyWayTime = (ushort)myTime; 

Do Not Overuse User-Defined Conversions

graphics/common.gif

It is sometimes tempting to include user-defined conversions that support and promote obscure and unintuitive code. For example, if we specified a user-defined implicit conversion from our program's Dog class to its Cat class, we would allow the following mysterious assignment statement:

 myCat = myDog; 

Are we assigning the name of the dog to the cat, or perhaps its age, maybe its color, or perhaps a combination of the three? If we happen to transfer the age, it would be much clearer to do it through properties and write the following:

 Cat myCat = new Cat(); myCat.Age = myDog.Age; 



   


C# Primer Plus
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2000
Pages: 286
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net