Chapter 2. Creating Versatile Types

Whenever you create your own classes, you need to consider the circumstances under which they could be used. For example, will two instances of your Item struct ever be compared for equality? Will your Person class need to be serializable, or sortable?

Note

Versatility means being able to do many things well. When you’re creating your own types, it means outfitting your objects with enough “extra” stuff that they can easily be used in a wide variety of situations.

This chapter is all about making your own objects as useful and versatile as possible. In many cases, this means implementing the standard interfaces that .NET provides or simply overriding base class methods.

Format a Type with ToString()

Solution: By default, ToString() will display the type’s name. To show your own values, you must override the method with one of your own. To illustrate this, let’s continue our Vertex3d class example from the previous chapter.

Assume the class initially looks like this:

image

Override ToString() for Simple Output

To get a simple string representation of the vertex, override ToString() to return a string of your choosing.

image

The code

Vertex3d v = new Vertex3d(1.0, 2.0, 3.0);
Trace.WriteLine(v.ToString());

produces the following output:

(1, 2, 3)

Implement Custom Formatting for Fine Control

Solution: Although the ToString()implementation gets the job done, and is especially handy for debugging (Visual Studio will automatically call ToString() on objects in the debugger windows), it is not very flexible. By implementing IFormattable on your type, you can create a version of ToString() that is as flexible as you need.

Let’s create a simple format syntax that allows us to specify which of the three values to print. To do this, we’ll define the following format string:

"X, Y"

This tells Vertex3d to print out X and Y. The comma and space (and any other character) will be output as-is.

The struct definition will now be as follows:

image

image

The formatProvider argument allows you to pass in a formatter that does something different from the type’s own formatting (say, if you can’t change the implementation of ToString() on Vertex3d for some reason, or you need to apply different formatting in specific situations). You’ll see how to define a custom formatter in the next section.

Formatting with ICustomFormatter and StringBuilder

Solution: Use ICustomFormatter and StringBuilder. This example prints out type information, as well as whatever the custom format string specifies for the given types.

image

The class can be used like this:

Vertex3d v = new Vertex3d(1.0, 2.0, 3.0);
Vertex3d v2 = new Vertex3d(4.0, 5.0, 6.0);
TypeFormatter formatter = new TypeFormatter();
StringBuilder sb = new StringBuilder();
sb.AppendFormat(formatter, "{0:(X Y)}; {1:[X, Y, Z]}", v, v2);
Console.WriteLine(sb.ToString());

The following output is produced:

Type: ch02.Vertex3d, Value: (1 2); Type: ch02.Vertex3d, Value: [4, 5, 6]

Make Types Equatable

Solution: You should override Object.Equals() and also implement the IEquatable<T> interface.

By default, Equals() on a reference type checks to see if the objects refer to the same location in memory. This may be acceptable in some circumstances, but often, you’ll want to provide your own behavior. With value types, the default behavior is to reflect over each field and do a bit-by-bit comparison. This can have a very negative impact on performance, and in nearly every case you should provide your own implementation of Equals().

image

Note

Pay special attention to the note in Equals(Vertex3d other). If Vertex3d was a reference type and other was null, the type-safe version of the function would be called, not the Object version. You also need to call all the base classes in the hierarchy so they have an opportunity to check their own fields.

There’s nothing stopping you from also implementing IEquatable<string> (or any other type) on your type—you can define it however you want. Use with caution, however, because this may confuse people who have to use your code.

Make Types Hashable with GetHashCode()

Solution: You almost always want to override GetHashCode(), especially with value types, for performance reasons. Generating a hash value is generally done by somehow distilling the data values in your class to an integer representation that is different for every value your class can have. You should override GetHashCode() whenever you override Equals().

image

Note

Hash codes are not supposed to be unique for every possible set of values your type can have. This is actually impossible, as you can deduce from the previous code sample. For this reason, comparing hash values is not a good way to compute equality.

Make Types Sortable

Solution: Because you often don’t know how your type will be used, making the objects sortable is highly recommended whenever possible.

In the Vector3d class example, in order to make the objects comparable, we’ll add an _id field and implement the IComparable<Vertex3d> interface.

The _id field will be what determines the order (it doesn’t make much sense to sort on coordinates, generally).

The sorting function is simple. It takes an object of Vertex3d and returns one of three values:

image

Within the CompareTo function, you can do anything you want to arrive at those values. In our case, we can do the comparison ourself or just call the same function on the _id field.

image

Give Types an Index

Solution: You can index by any type. The most common index types are int and string.

Implement a Numerical Index

You use the array access brackets to define an index on the this object, like this sample:

image

Implement a String Index

Unlike regular arrays, however, you are not limited to integer indices. You can use any type at all, most commonly strings, as in this example:

image

Sample usage:

Vertex3d v = new Vertex3d(1, 2, 3);
Console.WriteLine(v[0]);
Console.WriteLine(v["Z"]);

Output:

1
3

Notify Clients when Changes Happen

Solution: Implement the INotifyPropertyChanged interface (located in System.ComponentModel).

image

The Windows Presentation Foundation (WPF) makes extensive use of this interface for data binding, but you can use it for your own purposes as well.

To consume such a class, use code similar to this:

image

Overload Appropriate Operators

Solution: Operator overloading is like sugar: a little is sweet, but a lot will make you sick. Ensure that you only use this technique for situations that make sense.

Implement operator +

Notice that the method is public static and takes both operators as arguments.

image

The same principal can be applied to the -, *, /, %, &, |, <<, >>, !, ~, ++, and -- operators as well.

Implement operator == and operator !=

These should always be implemented as a pair. Because we’ve already implemented a useful Equals() method, just call that instead.

image

What if the type is a reference type? In this case, you have to handle null values for both a and b, as in this example:

image

Convert One Type to Another

Solution: Implement a conversion operator. There are two types of conversion operators: implicit and explicit. To understand the difference, we’ll implement a new struct called Vertex3i that is the same as Vertex3d, except the dimensions are integers instead of doubles.

Explicit Conversion (Loss of Precision)

Explicit conversion is encouraged when the conversion will result in a loss of precision. When you’re converting from System.Double to System.Int32, for example, all of the decimal precision is lost. You don’t (necessarily) want the compiler to allow this conversion automatically, so you make it explicit. This code goes in the Vertex3d class:

image

To convert from Vertex3d to Vertex3i then, you would do the following:

Vertex3d vd = new Vertex3d(1.5, 2.5, 3.5);
Vertex3i vi = (Vertex3i)vd;

If you tried it without the cast, you would get the following:

//Vertex3i vi = vd;
Error: Cannot implicitly convert type 'Vertex3d' to 'Vertex3i'.
          An explicit conversion exists (are you missing a cast?)

Implicit Conversion (No Loss of Precision)

If there will not be any loss in precision, then the conversion can be implicit, meaning the compiler will allow you to assign the type with no explicit conversion. We can implement this type of conversion in the Vertex3i class because it can convert up to a double with no loss of precision.

image

Now we can assign without casting:

Vertex3i vi = new Vertex3i(1, 2, 3);
Vertex3d vd = vi;

Prevent Inheritance

Solution: Mark the class as sealed.

image

Structs are inherently sealed.

Prevent Overriding of a Single Method

Solution: Put sealed as part of the method or property definition.

image

Allow Value Type to Be Null

Solution: This isn’t technically something you need to implement in your class. .NET 2.0 introduced the Nullable<T> type, which wraps any value type into something that can be null. It’s useful enough that there is a special C# syntax shortcut to do this. The following two lines of code are semantically equivalent:

Nullable<int> _id;
int? _id;

Let’s make the _id field in our Vertex3d class Nullable<T> to indicate the lack of a valid value. The following code snippet demonstrates how it works:

image

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset