Objective C # Principle 28: Avoid Conversion
Item 28: avoid conversion Operators
A conversion operation is a conversion operation between substitutability. A class can replace another class. This may be a good thing: An object of a derived class can be replaced by an object of its base class. A classic example is shape inheritance. First there is a shape class, and then derived from many other types: rectangular, elliptical, circular and other. You can replace the circle with a graph shape anywhere. This is the generational type of polymorphism. This is correct, because the circle is a special shape. When you create a class, explicit type conversion can be completed automatically. Just like class inheritance in. net, because system. object is the base class of all types, any type can be replaced by system. obejct. In the same case, you can replace any type you create with the interface it implements or its base class interface, or replace it with a base class. In addition, C # supports many other conversions.
When you add a conversion operation for a type, it tells the compiler that your type can be replaced by the target class. This may cause some potential errors, because your type may not be replaced by the target type, instead, the C # language permits another type of conversion. Please refer to the following article ). Its side effect is that the status of the target type may be invalid for the original type after being modified. Even worse, if your conversion produces a temporary object, the side effect is that you modify the temporary object directly and it will be permanently lost in the garbage collector. In short, the conversion operation should be based on the Type object at compile time, rather than the type object at runtime. Users may need to perform various forced conversion operations on the types. Such actual operations may produce non-maintenanceCode.
You can use a conversion operation to convert an unknown type to your type. This makes it clearer that you want to create an object ). The conversion operation will generate difficult issues in the code. Assume that you have created a class library structure such as 3.1. The elliptic and circle are inherited from the shape class. Although you believe that the elliptic and circle are related, you still decide to keep such an inheritance relationship. This is because you do not want to use a non-Abstract leaf class in the inheritance relationship. This will cause some difficult implementation problems when inheriting the circular class from the elliptical class. However, you realize that every circle should be an elliptic, and some also may be circular.
(Fig. 3.1)
The example given by the author is not very appropriate in this principle, and the author assumes the cause above. Therefore, please do not give this example a rough guess, just understand the ideas expressed by the author. I believe that similar conversion problems may occur in your C # development, but it is unlikely that the circle will be switched to an elliptic .)
This causes you to add two conversion operations. Because every circle is an elliptic, you need to add an implicit conversion from a circular to a new elliptic. Implicit conversion is called when one class is required to be converted to another class. Corresponding, display conversion isProgramThe mandatory conversion operator is used in the code.
Public class circle: Shape
{
Private pointf _ center;
Private float _ radius;
Public circle ():
This (pointf. Empty, 0)
{
}
Public circle (pointf C, float R)
{
_ Center = C;
_ Radius = R;
}
Public override void draw ()
{
//...
}
Static public implicit operator ellipse (Circle C)
{
Return new ellipse (C. _ center, C. _ center,
C. _ radius, C. _ radius );
}
}
Now you have implemented an implicit conversion operation. You can use a circle in any place that requires an ellipse. And the conversion is completed automatically:
Public double computearea (ellipse E)
{
// Return the area of the ellipse.
}
// Call it:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
Computearea (C );
I just want to use this example to express alternative types: A circle can replace an elliptic. The computearea function can work on alternative types. You are lucky, but let's look at the example below:
Public void flatten (ellipse E)
{
E. R1/= 2;
E. R2 * = 2;
}
// Call it using a circle:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
Flatten (C );
This is invalid. The flatten () method requires an elliptic as a parameter, and the compiler must convert the circle to an elliptic in some way. Indeed, an implicit conversion has been implemented. You have also called the conversion. The flatten () method obtains the parameter of the new elliptical object created from your conversion operation. This temporary object is modified by the flatten () function and quickly becomes a spam object. It is precisely because of this temporary object that the flatten () function has side effects. The final result is the circular object C, which does not change at all. From implicit conversion to display conversion, it only forces the user to call forced conversion:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
Flatten (ellipse) C );
The original problem still exists. You want the user to forcibly convert the call to solve this problem, but in fact a temporary object is still generated, and the temporary object is lost after being flatten. The original circle, C, is still not modified. Instead, if you create a constructor to convert the circle to an elliptic, the operation is clear:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
Flatten (New ellipse (c ));
I believe many programmers can see at a glance that the Oval transmitted to flatten () in the previous two lines of code is lost after modification. They may solve this problem by tracking objects:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
// Work with the circle.
//...
// Convert to an ellipse.
Ellipse E = new ellipse (C );
Flatten (E );
You can use a variable to save the modified (flattened) ellipse and use the constructor to replace the conversion operation. You will not lose any function: You just make the operation of creating new objects clearer. (Experienced C ++ programs may note that both implicit conversion and display conversion of C # Do Not Call constructors. In C ++, only explicit use of the new operator can create a new object. The C # constructor does not need to explicitly use keywords .)
The conversion operation of fields returned from the type does not display the type behavior, which may cause some problems. You have left several serious vulnerabilities for the Type encapsulation principle. By forcibly converting a type to another type, you can access internal variables of the type. This is exactly what should be avoided for all reasons discussed in principle 23.
A conversion operation provides an alternative type, but this causes some problems to the code. You should have understood all this: the user wants to reasonably replace your type with a certain type. When this alternative type is accessed, you can allow the user to work on the temporary object, or the internal field replaces the class you created. Then you may have modified the temporary object and lost it. Because these conversion codes are generated by the compiler, it is difficult to find these potential bugs. The conversion operation should be avoided as much as possible.
======================================
Item 28: avoid conversion Operators
Conversion operators introduce a kind of substitutability between classes. substitutability means that one class can be substituted for another. this can be a benefit: An object of a derived class can be substituted for an object of its base class, as in the classic example of the Shape hierarchy. you create a shape base class and derive a variety of mizmizations: rectangle, ellipse, circle, and so on. you can substitute a circle anywhere a shape is expected. that's using polymorphism for substitutability. it works because a circle is a specific type of shape. when you create a class, certain conversions are allowed automatically. any object can be substituted for an instance of system. object, the root of. net class hierarchy. in the same fashion, any object of a class that you create will be substituted implicitly for an interface that it implements, any of its base interfaces, or any of its base classes. the language also supports a variety of Numeric conversions.
when you define a conversion operator for your type, you tell the compiler that your type may be substituted for the target type. these substitutions often result in subtle errors because your type probably isn't a perfect substitute for the target type. side effects that modify the state of the target type won't have the same effect on your type. worse, if your conversion operator returns a temporary object, the side effects will modify the temporary object and be lost forever to the garbage collector. finally, the rules for invoking conversion operators are based on the compile-time type of an object, not the runtime type of an object. users of your type might need to perform multiple casts to invoke the conversion operators, a practice that leads to unmaintainable code.
If you want to convert another type into your type, use a constructor. this more clearly reflects the action of creating a new object. conversion operators can introduce hard-to-find problems in your code. suppose that you inherit the code for a library shown in Figure 3.1. both the circle class and the ellipse class are derived from the shape class. you decide to leave that hierarchy in place because you believe that, although the circle and ellipse are related, you don't want to have nonabstract leaf classes in your hierarchy, and several implementation problems occur when you try to derive the circle class from the ellipse class. however, you realize that every circle cocould be an ellipse. in addition, some ellipses cocould be substituted for circles.
Figure 3.1. basic shape hierarchy.
That leads you to add two conversion operators. every circle is an ellipse, So you add an implicit conversion to create a new ellipse from a circle. an implicit conversion operator will be called whenever one type needs to be converted to another type. by contrast, an explicit conversion will be called only when the programmer puts a cast operator in the source code.
Public class circle: Shape
{
Private pointf _ center;
Private float _ radius;
Public circle ():
This (pointf. Empty, 0)
{
}
Public circle (pointf C, float R)
{
_ Center = C;
_ Radius = R;
}
Public override void draw ()
{
//...
}
Static public implicit operator ellipse (Circle C)
{
Return new ellipse (C. _ center, C. _ center,
C. _ radius, C. _ radius );
}
}
Now that you 've got the implicit conversion operator, you can use a circle anywhere an ellipse is expected. Furthermore, the conversion happens automatically:
Public double computearea (ellipse E)
{
// Return the area of the ellipse.
}
// Call it:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
Computearea (C );
This sample shows what I mean by substitutability: A circle has been substituted for an ellipse. The computearea function works even with the substitution. You got lucky. But examine this function:
Public void flatten (ellipse E)
{
E. R1/= 2;
E. R2 * = 2;
}
// Call it using a circle:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
Flatten (C );
This won't work. the flatten () method takes an ellipse as an argument. the compiler must somehow convert a circle to an ellipse. you 've created an implicit conversion that does exactly that. your conversion gets called, and the flatten () function named es as its parameter the ellipse created by your implicit conversion. this temporary object is modified by the flatten () function and immediately becomes garbage. the side effects expected from your flatten () function occur, but only on a temporary object. the end result is that nothing happens to the circle, C.
Changing the conversion from implicit to explicit only forces users to add a cast to the call:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
Flatten (ellipse) C );
The original problem remains. you just forced your users to add a cast to cause the problem. you still create a temporary object, flatten the temporary object, and throw it away. the circle, C, is not modified at all. instead, if you create a constructor to convert the circle to an ellipse, the actions are clearer:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
Flatten (New ellipse (c ));
Most programmers wocould see the previous two lines and immediately realize that any modifications to the ellipse passed to flatten () are lost. They wocould fix the problem by keeping track of the new object:
Circle C = new circle (New pointf (3.0f, 0), 5.0f );
// Work with the circle.
//...
// Convert to an ellipse.
Ellipse E = new ellipse (C );
Flatten (E );
The variable e holds the flattened ellipse. by replacing the conversion operator with a constructor, you have not lost any functionality; you 've merely made it clearer when new objects are created. (veteran C ++ programmers shocould note that C # does not call constructors for implicit or explicit conversions. you create new objects only when you explicitly use the new operator, and at no other time. there is no need for the explicit keyword on constructors in C #.)
Conversion operators that return fields inside your objects will not exhibit this behavior. they have other problems. you 've poked a serious hole in the encapsulation of your class. by casting your type to some other object, clients of your class can access an internal variable. that's best avoided for all the reasons discussed in item 23.
conversion operators introduce a form of substitutability that causes problems in your code. you're indicating that, in all cases, users can reasonably just CT that another class can be used in place of the one you created. when this substituted object is accessed, you cause clients to work with temporary objects or internal fields in place of the class you created. you then modify temporary objects and discard the results. these subtle bugs are hard to find because the compiler generates code to convert these objects. avoid conversion operators.