Comparisons


In this section, you will look at two types of comparisons between objects:

  • Type comparisons

  • Value comparisons

Type comparisons, that is, determining what an object is, or what it inherits from, are important in all areas of C# programming. Often when you pass an object to a method, say, what happens next depends on what type the object is. You've seen this in passing at various points in this and earlier chapters, but here you will see some more useful techniques.

Value comparisons are also something you've seen a lot of, at least with simple types. When it comes to comparing values of objects things get a little more complicated. You have to define what is meant by a comparison for a start, and what operators such as > mean in the context of your classes. This is especially important in collections, where you might want to sort objects according to some condition, perhaps alphabetically or perhaps according to some more complicated algorithm.

Type Comparison

When you are comparing objects you will often need to know their type, which may enable you to determine whether a value comparison is possible. Back in Chapter 9 you saw the GetType() method, which all classes inherit from System.Object, and how this method can be used in combination with the typeof() operator to determine (and take action depending on) object types:

if (myObj.GetType() == typeof(MyComplexClass)) {    // myObj is an instance of the class MyComplexClass. }

You've also seen the way that the default implementation of ToString(), also inherited from System.Object, will get you a string representation of the type of an object. You can compare these strings too, although this is a bit of a messy way of doing things.

In this section, you're going to look at a handy shorthand way of doing things: the is operator. This allows for much more readable code and also, as you will see, has the advantage of examining base classes.

Before looking at the is operator, though, you need to be aware of something that often happens behind the scenes when dealing with value types (as opposed to reference types): boxing and unboxing.

Boxing and Unboxing

In Chapter 8, you saw the difference between reference and value types, which was illustrated in Chapter 9 by comparing structs (which are value types) with classes (which are reference types). Boxing is the act of converting a value type into the System.Object type or to an interface type that is implemented by the value type. Unboxing is the opposite conversion.

For example, suppose that you have the following struct type:

 struct MyStruct { public int Val; } 

You can box a struct of this type by placing it into an object-type variable:

 MyStruct valType1 = new MyStruct(); valType1.Val = 5; object refType = valType1; 

Here, you create a new variable (valType1) of type MyStruct, assign a value to the Val member of this struct, then box it into an object-type variable (refType).

The object created by boxing a variable in this way contains a reference to a copy of the value-type variable, not a reference to the original value-type variable. You can verify this by modifying the contents of the original struct, then unboxing the struct contained in the object into a new variable and examining its contents:

 valType1.Val = 6; MyStruct valType2 = (MyStruct)refType; Console.WriteLine("valType2.Val = {0}", valType2.Val); 

This code gives you the following output:

valType2.Val = 5

When you assign a reference type to an object, however, you get a different behavior. You can illustrate this by changing MyStruct into a class (ignoring the fact that the name of this class isn't appropriate any more):

 class MyStruct {    public int Val; }

With no changes to the client code shown above (again ignoring the misnamed variables), you get the following output:

valType2.Val = 6

You can also box value types into interface types, so long as they implement that interface. For example, suppose the MyStruct type implements the IMyInterface interface as follows:

 interface IMyInterface { }     struct MyStruct : IMyInterface {    public int Val; }

You can then box the struct into an IMyInterface type as follows:

 MyStruct valType1 = new MyStruct(); IMyInterface refType = valType1; 

and you can unbox it using the normal casting syntax:

 MyStruct ValType2 = (MyStruct)refType; 

As you can see from these examples, boxing is performed without your intervention (that is, you don't have to write any code to make this possible). Unboxing a value requires an explicit conversion, however, and requires you to make a cast (boxing is implicit and doesn't have this requirement).

You might be wondering why you would actually want to do this. There are actually two very good reasons why boxing is extremely useful. First, it allows you to use value types in collections (such as ArrayList), where the items are of type object. Second, it's the internal mechanism that allows you to call object methods on value types, such as ints and structs.

As a final note, it is worth remarking that unboxing is necessary before access to the value type contents is possible.

The is Operator

Despite its name, the is operator isn't a way to tell if an object is a certain type. Instead, the is operator allows you to check whether an object either is or can be converted into a given type. If this is the case, then the operator evaluates to true.

In the earlier examples you saw a Cow and a Chicken class, both of which inherit from Animal. Using the is operator to compare objects with the Animal type will return true for objects of all three of these types, not just Animal. This is something you'd have a great deal of difficulty achieving with the GetType() method and typeof() operator seen previously.

The is operator has the syntax:

<operand> is <type>

The possible results of this expression are:

  • If <type> is a class type, then the result is true if <operand> is of that type, if it inherits from that type or if it can be boxed into that type.

  • If <type> is an interface type, then the result is true if <operand> is of that type or if it is a type that implements the interface.

  • If <type> is a value type then the result is true if <operand> is of that type or if it is a type that can be unboxed into that type.

The Try It Out that follows provides a few examples to see how this works in practice.

Try It Out – Using the is Operator

image from book
  1. Create a new console application called Ch11Ex04 in the directory C:\BegVCSharp\Chapter11.

  2. Modify the code in Program.cs as follows:

    namespace Ch11Ex04 { class Checker { public void Check(object param1) { if (param1 is ClassA) Console.WriteLine("Variable can be converted to ClassA."); else Console.WriteLine("Variable can't if (param1 is IMyInterface) Console.WriteLine("Variable can be converted to IMyInterface."); else Console.WriteLine("Variable can't if (param1 is MyStruct) Console.WriteLine("Variable can be converted to MyStruct."); else Console.WriteLine("Variable can't } } interface IMyInterface { } class ClassA : IMyInterface { } class ClassB : IMyInterface { } class ClassC { } class ClassD : ClassA { } struct MyStruct : IMyInterface { }        class Program    {       static void Main(string[] args)       { Checker check = new Checker(); ClassA try1 = new ClassA(); ClassB try2 = new ClassB(); ClassC try3 = new ClassC(); ClassD try4 = new ClassD(); MyStruct try5 = new MyStruct(); object try6 = try5; Console.WriteLine("Analyzing ClassA type variable:"); check.Check(try1); Console.WriteLine("\nAnalyzing ClassB type variable:"); check.Check(try2); Console.WriteLine("\nAnalyzing ClassC type variable:"); check.Check(try3); Console.WriteLine("\nAnalyzing ClassD type variable:"); check.Check(try4); Console.WriteLine("\nAnalyzing MyStruct type variable:"); check.Check(try5); Console.WriteLine("\nAnalyzing boxed MyStruct type variable:"); check.Check(try6); Console.ReadKey();       }    } }

  3. Execute the code. The result is shown in Figure 11-6.

    image from book
    Figure 11-6

How It Works

This example illustrates the various results possible when using the is operator. Three classes, an interface, and a structure are defined and used as parameters to a method of a class that uses the is operator to see if they can be converted into the ClassA type, the interface type, and the struct type.

Only ClassA and ClassD (which inherits from ClassA) types are compatible with ClassA. Types that don't inherit from a class are not compatible with that class.

The ClassA, ClassB, and MyStruct types all implement IMyInterface, so these are all compatible with the IMyInterface type. ClassD inherits from ClassA, so that it too is compatible. Therefore, only ClassC is incompatible.

Finally, only variables of type MyStruct itself and boxed variables of that type are compatible with MyStruct, because you can't convert reference types to value types (except, of course, that you can unbox previously boxed variables).

image from book

Value Comparison

Consider two Person objects representing people, each with an integer Age property. You might want to compare them to see which person is older. You can simply use the following code:

if (person1.Age > person2.Age) {    ... }

This works fine, but there are alternatives. You might prefer to use syntax such as:

if (person1 > person2) {    ... }

This is possible using operator overloading, which you'll look at in this section. This is a powerful technique, but should be used judiciously. In the preceding code, it is not immediately obvious that ages are being compared — it could be height, weight, IQ, or just general "greatness."

Another option is to use the IComparable and IComparer interface, which allow you to define how objects will be compared to each other in a standard way. This way of doing things is supported by the various collection classes in the .NET Framework, making it an excellent way to sort objects in a collection.

Operator Overloading

Operator overloading enables you to use standard operators, such as +, >, and so on, with classes that you design. This is called overloading, because you are supplying your own implementations for these operators when used with specific parameter types, in much the same way that you overload methods by supplying different parameters for methods with the same name.

Operator overloading is useful as you can perform whatever processing you want in the implementation of the operator overload, which might not be as simple as, say, + meaning "add these two operands together." In a little while, you'll see a good example of this in a further upgrade of the CardLib library. You'll provide implementations for comparison operators that compare two cards to see which would beat the other in a trick (one round of card game play). Because a trick in many card games depends on the suits of the cards involved, this isn't as straightforward as comparing the numbers on the cards. If the second card laid down is a different suit from the first, then the first card will win regardless of its rank. You can implement this by considering the order of the two operands. You can also take a trump suit into account, where trumps beat other suits, even if that isn't the first suit laid down. This means that calculating that card1 > card2 is true (that is, card1 will beat card2, if card1 is laid down first), doesn't necessarily imply that card2 > card1 is false. If neither card1 nor card2 are trumps and they belong to different suits, then both these comparisons will be true.

To start with, though, here's a look at the basic syntax for operator overloading.

Operators may be overloaded by adding operator type members (which must be static) to a class. Some operators have multiple uses (such as -, which has unary and binary capabilities); therefore, you also specify how many operands you are dealing with and what the types of these operands are. In general, you will have operands that are the same type as the class where the operator is defined, although it is possible to define operators that work on mixed types, as you will see shortly.

As an example, consider the simple type AddClass1, defined as follows:

 public class AddClass1 { public int val; } 

This is just a wrapper around an int value but will serve to illustrate the principles.

With this class, code such as the following, will fail to compile:

 AddClass1 op1 = new AddClass1(); op1.val = 5; AddClass1 op2 = new AddClass1(); op2.val = 5; AddClass1 op3 = op1 + op2; 

The error you get informs you that the + operator cannot be applied to operands of the AddClass1 type. This is so because you haven't defined an operation to perform yet.

Code such as the following, will work, although it won't give you the result you might want:

AddClass1 op1 = new AddClass1(); op1.val = 5; AddClass1 op2 = new AddClass1(); op2.val = 5; bool op3 = op1 == op2; 

Here, op1 and op2 are compared using the == binary operator to see if they refer to the same object, and not to verify whether their values are equal. op3 will be false in the preceding code, even though op1.val and op2.val are identical.

To overload the + operator, you use the following code:

public class AddClass1 {    public int val;     public static AddClass1 operator +(AddClass1 op1, AddClass1 op2) { AddClass1 returnVal = new AddClass1(); returnVal.val = op1.val + op2.val; return returnVal; } }

As you can see, operator overloads look much like standard static method declarations, except that they use the keyword operator and the operator itself rather than a method name.

You can now successfully use the + operator with this class, as in the previous example:

AddClass1 op3 = op1 + op2;

Overloading all binary operators fits the same pattern. Unary operators look similar but only have one parameter:

public class AddClass1 {    public int val;        public static AddClass1 operator +(AddClass1 op1, AddClass1 op2)    {       AddClass1 returnVal = new AddClass1();       returnVal.val = op1.val + op2.val;       return returnVal;    }     public static AddClass1 operator -(AddClass1 op1) { AddClass1 returnVal = new AddClass1(); returnVal.val = -op1.val; return returnVal; } }

Both these operators work on operands of the same type as the class and have return values that are also of that type. Consider, however, the following class definitions:

 public class AddClass1 { public int val; public static AddClass3 operator +(AddClass1 op1, AddClass2 op2) { AddClass3 returnVal = new AddClass3(); returnVal.val = op1.val + op2.val; return returnVal; } } public class AddClass2 { public int val; } public class AddClass3 { public int val; } 

This will allow the following code:

 AddClass1 op1 = new AddClass1(); op1.val = 5; AddClass2 op2 = new AddClass2(); op2.val = 5; AddClass3 op3 = op1 + op2; 

When appropriate, you can mix types in this way. Note, however, that if you added the same operator to AddClass2, the preceding code would fail, because it would be ambiguous as to which operator to use. You should, therefore, take care not to add operators with the same signature to more than one class.

Also, note that if you mix types, the operands must be supplied in the same order as the parameters to the operator overload. If you attempt to use your overloaded operator with the operands in the wrong order, the operation will fail. So, you can't use the operator like this:

 AddClass3 op3 = op2 + op1; 

unless, of course, you supply another overload with the parameters reversed:

 public static AddClass3 operator +(AddClass2 op1, AddClass1 op2) { AddClass3 returnVal = new AddClass3(); returnVal.val = op1.val + op2.val; return returnVal; } 

The following operators can be overloaded:

  • Unary operators: +, -, !, ~, ++, --, true, false

  • Binary operators: +, -, *, /, %, &, |, ^, <<, >>

  • Comparison operators: ==, !=, <, >, <=, >=

    Note

    If you overload the true and false operators, then you can use classes in Boolean expressions, such as if (op1) {}.

You can't overload assignment operators, such as +=, but these operators use their simple counterparts, such as +, so you don't have to worry about that. Overloading + means that += will function as expected. the = operator is included in this — it makes little sense to overload this operator, since it has such a fundamental usage. This operator, however, is related to the user-defined conversion operators, which you'll look at in the next section.

You also can't overload && and ||, but these operators use the & and | operators to perform their calculations, so overloading these is enough.

Some operators, such as < and >, must be overloaded in pairs. That is to say, you can't overload < unless you also overload >. In many cases, you can simply call other operators from these to reduce the code required (and the errors that might occur), for example:

public class AddClass1 {    public int val;     public static bool operator >=(AddClass1 op1, AddClass1 op2) { return (op1.val >= op2.val); }     public static bool operator <(AddClass1 op1, AddClass1 op2) { return !(op1 >= op2); }     // Also need implementations for <= and > operators. }

In more complex operator definitions, this can save on lines of code, and it also means that you have less code to change should you wish to change the implementation of these operators.

The same applies to == and !=, but with these operators it is often worth overriding Object.Equals() and Object.GetHashCode(), because both of these functions may also be used to compare objects. By overriding these methods, you ensure that whatever technique users of the class use, they get the same result. This isn't essential, but is worth adding for completeness. It requires the following nonstatic override methods:

public class AddClass1 {    public int val;        public static bool operator ==(AddClass1 op1, AddClass1 op2)    {       return (op1.val == op2.val);    }     public static bool operator !=(AddClass1 op1, AddClass1 op2) { return !(op1 == op2); }     public override bool Equals(object op1) { return val == ((AddClass1)op1).val; }        public override int GetHashCode()    {       return val;    } }

GetHashCode() is used to obtain a unique int value for an object instance based on its state. Here, using val is fine, because it is also an int value.

Note that Equals() uses an object type parameter. You need to use this signature or you will be overloading this method rather than overriding it, and the default implementation will still be accessible to users of the class. This means that you must use casting to get the result you require. It is often worth checking object type using the is operator discussed earlier in this chapter in code such as this:

public override bool Equals(object op1) { if (op1 is AddClass1) {       return val == ((AddClass1)op1).val; } else { throw new ArgumentException( "Cannot compare AddClass1 objects with objects of type " + op1.GetType().ToString()); } }

In this code, an exception is thrown if the operand passed to Equals is of the wrong type or cannot be converted into the correct type.

Of course, this behavior may not be what you want. You may want to be able to compare objects of one type with objects of another type, in which case more branching would be necessary. Alternatively, you may want to restrict comparisons to those where both objects are of exactly the same type, which would require the following change to the first if statement:

 if (op1.GetType() == typeof(AddClass1)) 

Next, you see how you can make use of operator overloads in CardLib.

Adding Operator Overloads to CardLib

Now, you'll upgrade your Ch11CardLib project again, adding operator overloading to the card class. First, though, you'll add the extra fields to the Card class that allow for trump suits and a choice to place Aces high. You make these static, since when they are set, they apply to all Card objects:

public class Card { // Flag for trump usage. If true, trumps are valued higher  // than cards of other suits. public static bool useTrumps = false; // Trump suit to use if useTrumps is true. public static Suit trump = Suit.Club; // Flag that determines whether aces are higher than kings or lower // than deuces. public static bool isAceHigh = true; 

One point to note here is that these rules apply to all Card objects in every Deck in an application. It is not possible to have two decks of cards with cards contained in each that obey different rules. This is fine for this class library, however, as you can safely assume that if a single application wants to use separate rules, then it could maintain these itself, perhaps setting the static members of Card whenever decks are switched.

Since you have done this, it is worth adding a few more constructors to the Deck class, in order to initialize decks with different characteristics:

public Deck() {    for (int suitVal = 0; suitVal < 4; suitVal++)    {       for (int rankVal = 1; rankVal < 14; rankVal++)       {          cards.Add(new Card((Suit)suitVal, (Rank)rankVal));       }    } }     // Nondefault constructor. Allows aces to be set high. public Deck(bool isAceHigh) : this() { Card.isAceHigh = isAceHigh; } // Nondefault constructor. Allows a trump suit to be used. public Deck(bool useTrumps, Suit trump) : this() { Card.useTrumps = useTrumps; Card.trump = trump; }     // Nondefault constructor. Allows aces to be set high and a trump suit // to be used. public Deck(bool isAceHigh, bool useTrumps, Suit trump) : this() { Card.isAceHigh = isAceHigh; Card.useTrumps = useTrumps; Card.trump = trump; } 

Each of these constructors is defined using the : this() syntax you saw in Chapter 9, so that in all cases, the default constructor is called before the nondefault one, initializing the deck.

Next, you add your operator overloads (and suggested overrides) to the Card class:

public Card(Suit newSuit, Rank newRank) {    suit = newSuit;    rank = newRank; }     public static bool operator ==(Card card1, Card card2) { return (card1.suit == card2.suit) && (card1.rank == card2.rank); }     public static bool operator !=(Card card1, Card card2) { return !(card1 == card2); }     public override bool Equals(object card) { return this == (Card)card; } public override int GetHashCode() { return 13*(int)rank + (int)suit; }     public static bool operator >(Card card1, Card card2) { if (card1.suit == card2.suit) { if (isAceHigh) { if (card1.rank == Rank.Ace) { if (card2.rank == Rank.Ace) return false; else return true; } else { if (card2.rank == Rank.Ace) return false; else return (card1.rank > card2.rank); } } else { return (card1.rank > card2.rank); } } else { if (useTrumps && (card2.suit == Card.trump)) return false; else return true; } }     public static bool operator <(Card card1, Card card2) { return !(card1 >= card2); }     public static bool operator >=(Card card1, Card card2) { if (card1.suit == card2.suit) { if (isAceHigh) { if (card1.rank == Rank.Ace) { return true; } else { if (card2.rank == Rank.Ace) return false; else return (card1.rank >= card2.rank); } } else { return (card1.rank >= card2.rank); } } else { if (useTrumps && (card2.suit == Card.trump)) return false; else return true; } }     public static bool operator <=(Card card1, Card card2) { return !(card1 > card2); } 

There's not much to note about this code, except perhaps the slightly lengthy code for the > and >= overloaded operators. If you step through the code for >, you can see how it works and why these steps are necessary.

You are comparing two cards, card1 and card2, where card1 is assumed to be the first one laid down on the table. As discussed earlier, this becomes important when you are using trump cards, because a trump will beat a nontrump, even if the nontrump has a higher rank. Of course, if the suits of the two cards are identical then whether the suit is the trump suit or not is irrelevant, so this is the first comparison you make:

public static bool operator >(Card card1, Card card2) {    if (card1.suit == card2.suit)    {

If the static isAceHigh flag is true, then you can't compare the cards' ranks directly via their value in the Rank enumeration, because the rank of ace has a value of 1 in this enumeration, which is less than that of all other ranks. Instead, you need the following steps:

  • If the first card is an ace, you check to see if the second card is also an ace. If it is, then the first card won't beat the second. If the second card isn't an ace, then the first card will win:

    if (isAceHigh) {    if (card1.rank == Rank.Ace)    {       if (card2.rank == Rank.Ace)          return false;       else          return true;    } 
  • If the first card isn't an ace you also need to check to see if the second one is. If it is, then the second card wins; otherwise, you can compare the rank values because you know that aces aren't an issue:

       else    {       if (card2.rank == Rank.Ace)          return false;       else          return (card1.rank > card2.rank);    } }
  • Alternatively, if aces aren't high, you can just compare the rank values:

    else {    return (card1.rank > card2.rank); }

The remainder of the code concerns the case where the suits of card1 and card2 are different. Here, the static useTrumps flag is important. If this flag is true and card2 is of the trump suit, then you can say definitively that card1 isn't a trump (because the two cards have different suits), and trumps always win, so card2 is the higher card:

else {    if (useTrumps && (card2.suit == Card.trump))       return false;

If card2 isn't a trump (or useTrumps is false), then card1 wins, because it was the first card laid down:

      else          return true;    } }

Only one other operator (>=) uses code similar to this, and the other operators are very simple, so there's no need to go into any more detail about them.

The following simple client code tests these operators (place it in the Main() function of a client project to test it, like the client code you saw earlier in the earlier CardLib examples):

 Card.isAceHigh = true; Console.WriteLine("Aces are high."); Card.useTrumps = true; Card.trump = Suit.Club; Console.WriteLine("Clubs are trumps."); Card card1, card2, card3, card4, card5; card1 = new Card(Suit.Club, Rank.Five); card2 = new Card(Suit.Club, Rank.Five); card3 = new Card(Suit.Club, Rank.Ace); card4 = new Card(Suit.Heart, Rank.Ten); card5 = new Card(Suit.Diamond, Rank.Ace); Console.WriteLine("{0} == {1} ? {2}", card1.ToString(), card2.ToString(), card1 == card2); Console.WriteLine("{0} != {1} ? {2}", card1.ToString(), card3.ToString(), card1 != card3); Console.WriteLine("{0}.Equals({1}) ? {2}", card1.ToString(), card4.ToString(), card1.Equals(card4)); Console.WriteLine("Card.Equals({0}, {1}) ? {2}", card3.ToString(), card4.ToString(), Card.Equals(card3, card4)); Console.WriteLine("{0} > {1} ? {2}", card1.ToString(), card2.ToString(), card1 > card2); Console.WriteLine("{0} <= {1} ? {2}", card1.ToString(), card3.ToString(), card1 <= card3); Console.WriteLine("{0} > {1} ? {2}", card1.ToString(), card4.ToString(), card1 > card4); Console.WriteLine("{0} > {1} ? {2}", card4.ToString(), card1.ToString(), card4 > card1); Console.WriteLine("{0} > {1} ? {2}", card5.ToString(), card4.ToString(), card5 > card4); Console.WriteLine("{0} > {1} ? {2}", card4.ToString(), card5.ToString(), card4 > card5); Console.ReadKey(); 

The results are as shown in Figure 11-7.

image from book
Figure 11-7

In each case, the operators are applied taking the specified rules into account. This is particularly apparent in the last four lines of output, demonstrating how trump cards always beat nontrumps.

The IComparable and IComparer Interfaces

The IComparable and IComparer interfaces are the standard way to compare objects in the .NET Framework. The difference between the interfaces is as follows:

  • IComparable is implemented in the class of the object to be compared and allows comparisons between that object and another object.

  • IComparer is implemented in a separate class, which will allow comparisons between any two objects.

Typically, you will give a class default comparison code using IComparable, and nondefault comparisons using other classes.

IComparable exposes the single method CompareTo(). This method accepts an object, so you could, for example, implement it in such a way as to enable you to pass a Person object to it and tell you if that person is older or younger than the current person. In fact, this method returns an int, so you could get it to tell you how much older or younger the second person was using code such as:

if (person1.CompareTo(person2) == 0) {    Console.WriteLine("Same age"); } else if (person1.CompareTo(person2) > 0) {    Console.WriteLine("person 1 is Older"); } else {    Console.WriteLine("person1 is Younger"); }

IComparer exposes the single method Compare(). This method accepts two objects and returns an integer result just like CompareTo(). With an object supporting IComparer, you could use code like:

if (personComparer.Compare(person1, person2) == 0) {    Console.WriteLine("Same age"); } else if (personComparer.Compare(person1, person2) > 0) {    Console.WriteLine("person 1 is Older"); } else {    Console.WriteLine("person1 is Younger"); }

In both cases, the parameters supplied to the methods are of the type System.Object. This means that you can compare one object to another object of any other type, so you'll usually have to perform some type comparison before returning a result and maybe even throw exceptions if the wrong types are used.

the .NET Framework includes a default implementation of the the IComparer interface on a class called Comparer, found in the System.Collections namespace. This class is capable of performing culture- specific comparisons between simple types, as well as any type which supports the IComparable interface. You can use it, for example, with the following code:

string firstString = "First String"; string secondString = "Second String"; Console.WriteLine("Comparing     firstString, secondString,     Comparer.Default.Compare(firstString, secondString));     int firstNumber = 35; int secondNumber = 23; Console.WriteLine("Comparing     firstNumber, secondNumber,    Comparer.Default.Compare(firstNumber, secondNumber));

Here, you use the Comparer.Default static member to obtain an instance of the Comparer class, then use the Compare() method to compare first two strings, then two integers. The result is as follows:

Comparing 'First String' and 'Second String', result: -1 Comparing '35' and '23', result: 1

Since F comes before S in the alphabet it is deemed "less than" S, so the result of the first comparison is - 1. Similarly, 35 is greater than 23, hence the result of 1. Note that the results here give you no idea of the magnitude of the difference.

When using Comparer, you must use types that can be compared. Attempting to compare firstString with firstNumber, for example, will generate an exception.

There are a few more points to note about the behavior of this class:

  • Objects passed to Comparer.Compare() are checked to see if they support IComparable. If they do, then that implementation is used.

  • null values are allowed, and interpreted as being "less than" any other object.

  • Strings are processed according to the current culture. To process strings according to a different culture (or language) the Comparer class must be instantiated using its constructor, which allows you to pass a System.Globalization.CultureInfo object specifying the culture to use.

  • Strings are processed in a case-sensitive way. To process them in a non-case-sensitive way you need to use the CaseInsensitiveComparer class, which otherwise works in exactly the same way.

You see the default Comparer class, and some nondefault comparisons, in the next section.

Sorting Collections Using the IComparable and IComparer Interfaces

Many collection classes allow sorting, either by default comparisons between objects or by custom methods. ArrayList is one example, containing the method Sort(). This method can be used with no parameters, in which case default comparisons are used, or it can be passed an IComparer interface to use to compare pairs of objects.

When you have an ArrayList filled with simple types, such as integers or strings, the default comparer is fine. For your own classes, you must either implement IComparable in your class definition or create a separate class supporting IComparer to use for comparisons.

Note that some classes in the System.Collection namespace, including CollectionBase, don't expose a method for sorting. If you want to sort a collection you have derived from this class, you'll have to put a bit more work in and sort the internal List collection yourself.

In the following Try It Out, you'll see how to use a default and a nondefault comparer to sort a list.

Try It Out – Sorting a List

image from book
  1. Create a new console application called Ch11Ex05 in the directory C:\BegVCSharp\Chapter11.

  2. Add a new class called Person, and modify the code as follows:

    namespace Ch11Ex05 { class Person : IComparable    { public string Name; public int Age; public Person(string name, int age) { Name = name; Age = age; } public int CompareTo(object obj) { if (obj is Person) { Person otherPerson = obj as Person; return this.Age - otherPerson.Age; } else { throw new ArgumentException( "Object to compare to is not a Person object."); } }    } }

  3. Add a new class called PersonComparerName, and modify the code as follows:

    using System; using System.Collections; using System.Collections.Generic; using System.Text;     namespace Ch11Ex05 { public class PersonComparerName : IComparer    { public static IComparer Default = new PersonComparerName(); public int Compare(object x, object y) { if (x is Person && y is Person) { return Comparer.Default.Compare( ((Person)x).Name, ((Person)y).Name); } else { throw new ArgumentException( "One or both objects to compare are not Person objects."); } }    } }

  4. Modify the code in Program.cs as follows:

    using System; using System.Collections; using System.Collections.Generic; using System.Text;     namespace Ch11Ex05 {    class Program    {       static void Main(string[] args)       { ArrayList list = new ArrayList(); list.Add(new Person("Jim", 30)); list.Add(new Person("Bob", 25)); list.Add(new Person("Bert", 27)); list.Add(new Person("Ernie", 22)); Console.WriteLine("Unsorted people:"); for (int i = 0; i < list.Count; i++) { Console.WriteLine("{0} ({1})",  (list[i] as Person).Name, (list[i] as Person).Age); } Console.WriteLine(); Console.WriteLine( "People sorted with default comparer (by age):"); list.Sort(); for (int i = 0; i < list.Count; i++) { Console.WriteLine("{0} ({1})",  (list[i] as Person).Name, (list[i] as Person).Age); } Console.WriteLine(); Console.WriteLine( "People sorted with nondefault comparer (by name):"); list.Sort(PersonComparerName.Default); for (int i = 0; i < list.Count; i++) { Console.WriteLine("{0} ({1})",  (list[i] as Person).Name, (list[i] as Person).Age); } Console.ReadKey();       }    } }

  5. Execute the code. The result is shown in Figure 11-8.

    image from book
    Figure 11-8

How It Works

In this example, an ArrayList containing Person objects is sorted in two different ways. By calling the ArrayList.Sort() method with no parameters, the default comparison is used, which is the CompareTo() method in the Person class (since this class implements IComparable):

public int CompareTo(object obj) {    if (obj is Person)    {       Person otherPerson = obj as Person;       return this.Age - otherPerson.Age;    }    else    {       throw new ArgumentException(          "Object to compare to is not a Person object.");    } }

This method first checks to see if its argument can be compared to a Person object, that is, that the object can be converted into a Person object. If there is a problem then an exception is thrown. Otherwise, the Age properties of the two Person objects are compared.

Next, a nondefault comparison sort is performed, using the PersonComparerName class, which implements IComparer. This class has a public static field for ease of use:

public static IComparer Default = new PersonComparerName();

This enables you to get an instance using PersonComparerName.Default, just like the Comparer class you saw earlier. the CompareTo() method of this class is:

public int Compare(object x, object y) {    if (x is Person && y is Person)    {       return Comparer.Default.Compare(          ((Person)x).Name, ((Person)y).Name);    }    else    {       throw new ArgumentException(          "One or both objects to compare are not Person objects.");    } }

Again, arguments are first checked to see if they are Person objects, and if they aren't then an exception is thrown. If they are then the default Comparer object is used to compare the two string Name fields of the Person objects.

image from book




Beginning Visual C# 2005
Beginning Visual C#supAND#174;/sup 2005
ISBN: B000N7ETVG
EAN: N/A
Year: 2005
Pages: 278

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