You can find the wrox.com
code downloads for this chapter at www.wrox.com/go/beginningvisualc#2015programming
on the Download Code tab. The code is in the Chapter 11 download and individually named according to the names throughout the chapter.
You've covered all the basic OOP techniques in C# now, but there are some more advanced techniques that are worth becoming familiar with. These techniques relate to certain problems that you must solve regularly when you are writing code. Learning about them will make it much easier to progress and allow you to concentrate on other, potentially more important aspects of your applications. In this chapter, you look at the following:
IComparable
and IComparer
interface to sort collections.In Chapter 5, you learned how to use arrays to create variable types that contain a number of objects or values. Arrays, however, have their limitations. The biggest limitation is that once arrays have been created, they have a fixed size, so you can't add new items to the end of an existing array without creating a new one. This often means that the syntax used to manipulate arrays can become overly complicated. OOP techniques enable you to create classes that perform much of this manipulation internally, simplifying the code that uses lists of items or arrays.
Arrays in C# are implemented as instances of the
class and are just one type of what are known as collection classes. Collection classes in general are used for maintaining lists of objects, and they may expose more functionality than simple arrays. Much of this functionality comes through implementing interfaces from the System.Array
namespace, thus standardizing collection syntax. This namespace also contains some other interesting things, such as classes that implement these interfaces in ways other than System.Collections
.System.Array
Because the collection's functionality (including basic functions such as accessing collection items by using
syntax) is available through interfaces, you aren't limited to using basic collection classes such as [index]
. Instead, you can create your own customized collection classes. These can be made more specific to the objects you want to enumerate (that is, the objects you want to maintain collections of). One advantage of doing this, as you will see, is that custom collection classes can be strongly typed. That is, when you extract items from the collection, you don't need to cast them into the correct type. Another advantage is the capability to expose specialized methods. For example, you can provide a quick way to obtain subsets of items. In the deck of cards example, you could add a method to obtain all System.Array
items of a particular suit.Card
Several interfaces in the
namespace provide basic collection functionality:System.Collections
IEnumerable
— Provides the capability to loop through items in a collectionICollection
— Provides the capability to obtain the number of items in a collection and copy items into a simple array type (inherits from IEnumerable
)IList
— Provides a list of items for a collection along with the capabilities for accessing these items, and some other basic capabilities related to lists of items (inherits from IEnumerable
and ICollection
)IDictionary
— Similar to IList
, but provides a list of items accessible via a key value, rather than an index (inherits from IEnumerable
and ICollection
)The
class implements System.Array
, IList
, and ICollection
. However, it doesn't support some of the more advanced features of IEnumerable
, and it represents a list of items by using a fixed size.IList
One of the classes in the
namespace, Systems.Collections
, also implements System.Collections.ArrayList
, IList
, and ICollection
, but does so in a more sophisticated way than IEnumerable
. Whereas arrays are fixed in size (you can't add or remove elements), this class can be used to represent a variable-length list of items. To give you more of a feel for what is possible with such a highly advanced collection, the following Try It Out uses this class, as well as a simple array.System.Array
Now that you know what is possible using more advanced collection classes, it's time to learn how to create your own strongly typed collection. One way of doing this is to implement the required methods manually, but this can be a time-consuming and complex process. Alternatively, you can derive your collection from a class, such as
, an abstract class that supplies much of the implementation of a collection for you. This option is strongly recommended.System.Collections.CollectionBase
The
class exposes the interfaces CollectionBase
, IEnumerable
, and ICollection
but provides only some of the required implementation — notably, the IList
and Clear()
methods of RemoveAt()
and the IList
property of Count
. You need to implement everything else yourself if you want the functionality provided.ICollection
To facilitate this,
provides two protected properties that enable access to the stored objects themselves. You can use CollectionBase
, which gives you access to the items through an List
interface, and IList
, which is the InnerList
object used to store items.ArrayList
For example, the basics of a collection class to store
objects could be defined as follows (you'll see a fuller implementation shortly):Animal
public class Animals : CollectionBase
{
public void Add(Animal newAnimal)
{
List.Add(newAnimal);
}
public void Remove(Animal oldAnimal)
{
List.Remove(oldAnimal);
}
public Animals() {}
}
Here,
and Add()
have been implemented as strongly typed methods that use the standard Remove()
method of the Add()
interface used to access the items. The methods exposed will now only work with IList
classes or classes derived from Animal
, unlike the Animal
implementations shown earlier, which work with any object.ArrayList
The
class enables you to use the CollectionBase
syntax with your derived collections. For example, you can use code such as this:foreach
WriteLine("Using custom collection class Animals:");
Animals animalCollection = new Animals();
animalCollection.Add(new Cow("Lea"));
foreach (Animal myAnimal in animalCollection)
{
WriteLine($"New { myAnimal.ToString()} object added to custom " +
$"collection, Name = {myAnimal.Name}");
}
You can't, however, do the following:
animalCollection[0].Feed();
To access items via their indices in this way, you need to use an indexer.
An indexer is a special kind of property that you can add to a class to provide array-like access. In fact, you can provide more complex access via an indexer, because you can define and use complex parameter types with the square bracket syntax as you want. Implementing a simple numeric index for items, however, is the most common usage.
You can add an indexer to the
collection of Animals
objects as follows:Animal
public class Animals : CollectionBase
{
…
public Animal this[int animalIndex]
{
get { return (Animal)List[animalIndex]; }
Set { List[animalIndex] = value; }
}
}
The
keyword is used along with parameters in square brackets, but otherwise the indexer looks much like any other property. This syntax is logical, because you access the indexer by using the name of the object followed by the index parameter(s) in square brackets (for example, this
).MyAnimals[0]
The indexer code uses an indexer on the
property (that is, on the List
interface that provides access to the IList
in ArrayList
that stores your items):CollectionBase
return (Animal)List[animalIndex];
Explicit casting is necessary here, as the
property returns a IList.List
object. The important point to note here is that you define a type for this indexer. This is the type that will be obtained when you access an item by using this indexer. This strong typing means that you can write code such asSystem.Object
animalCollection[0].Feed();
rather than:
((Animal)animalCollection[0]).Feed();
This is another handy feature of strongly typed custom collections. In the following Try It Out, you expand the previous Try It Out to put this into action.
In the last chapter, you created a class library project called Ch10CardLib that contained a
class representing a playing card, and a Card
class representing a deck of cards — that is, a collection of Deck
classes. This collection was implemented as a simple array.Card
In this chapter, you'll add a new class to this library, renamed Ch11CardLib. This new class,
, will be a custom collection of Cards
objects, giving you all the benefits described earlier in this chapter. Create a new class library called Ch11CardLib in the Card
directory. Next, delete the autogenerated C:\BegVCSharp\Chapter11
file; select Project Class1.cs
Add Existing Item; select the
, Card.cs
, Deck.cs
, and Suit.cs
files from the Rank.cs
directory; and add the files to your project. As with the previous version of this project, introduced in Chapter 10, these changes are presented without using the standard Try It Out format. Should you want to jump straight to the code, feel free to open the version of this project included in the downloadable code for this chapter.C:\BegVCSharp\Chapter10\Ch10CardLib\Ch10CardLib
If you decide to create this project yourself, add a new class called
and modify the code in Cards
as follows:Cards.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ch11CardLib
{
public
class Cards : CollectionBase
{
public void Add(Card newCard)
{
List.Add(newCard);
}
public void Remove(Card oldCard)
{
List.Remove(oldCard);
}
public Card this[int cardIndex]
{
get { return (Card)List[cardIndex]; }
set { List[cardIndex] = value; }
}
/// <summary>
/// Utility method for copying card instances into another Cards
/// instance—used in Deck.Shuffle(). This implementation assumes that
/// source and target collections are the same size.
/// </summary>
public void CopyTo(Cards targetCards)
{
for (int index = 0; index < this.Count; index++)
{
targetCards[index] = this[index];
}
}
/// <summary>
/// Check to see if the Cards collection contains a particular card.
/// This calls the Contains() method of the ArrayList for the collection,
/// which you access through the InnerList property.
/// </summary>
public bool Contains(Card card) => InnerList.Contains(card);
}
}
Next, modify
to use this new collection, rather than an array:Deck.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Ch11CardLib
{
public class Deck
{
private Cards cards = new Cards();
public Deck()
{
// Line of code removed here
for (int suitVal = 0; suitVal < 4; suitVal++)
{
for (int rankVal = 1; rankVal < 14; rankVal++)
{
cards.Add(new Card((Suit)suitVal, (Rank)rankVal));
}
}
}
public Card GetCard(int cardNum)
{
if (cardNum >= 0 && cardNum <= 51)
return cards[cardNum];
else
throw (new System.ArgumentOutOfRangeException("cardNum", cardNum,
"Value must be between 0 and 51."));
}
public void Shuffle()
{
Cards newDeck = new Cards();
bool[] assigned = new bool[52];
Random sourceGen = new Random();
for (int i = 0; i < 52; i++)
{
int sourceCard = 0;
bool foundCard = false;
while (foundCard == false)
{
sourceCard = sourceGen.Next(52);
if (assigned[sourceCard] == false)
foundCard = true;
}
assigned[sourceCard] = true;
newDeck.Add(cards[sourceCard]);
}
newDeck.CopyTo(cards);
}
}
}
Not many changes are necessary here. Most of them involve changing the shuffling logic to allow for the fact that cards are added to the beginning of the new
collection Cards
from a random index in newDeck
, rather than to a random index in cards
from a sequential position in newDeck
.cards
The client console application for the
solution, Ch10CardLib
, can be used with this new library with the same result as before, as the method signatures of Ch10CardClient
are unchanged. Clients of this class library can now make use of the Deck
collection class, however, rather than rely on arrays of Cards
objects — for example, to define hands of cards in a card game application.Card
Instead of implementing the
interface, it is also possible for collections to implement the similar IList
interface, which allows items to be indexed via a key value (such as a string name), rather than an index. This is also achieved using an indexer, although here the indexer parameter used is a key associated with a stored item, rather than an IDictionary
index, which can make the collection a lot more user-friendly.int
As with indexed collections, there is a base class you can use to simplify implementation of the
interface: IDictionary
. This class also implements DictionaryBase
and IEnumerable
, providing the basic collection-manipulation capabilities that are the same for any collection.ICollection
, like DictionaryBase
, implements some (but not all) of the members obtained through its supported interfaces. Like CollectionBase
, the CollectionBase
and Clear
members are implemented, although Count
isn't because it's a method on the RemoveAt()
interface and doesn't appear on the IList
interface. IDictionary
does, however, have a IDictionary
method, which is one of the methods you should implement in a custom collection class based on Remove()
.DictionaryBase
The following code shows an alternative version of the
class, this time derived from Animals
. Implementations are included for DictionaryBase
, Add()
, and a key-accessed indexer:Remove()
public class Animals : DictionaryBase
{
public void Add(string newID, Animal newAnimal)
{
Dictionary.Add(newID, newAnimal);
}
public void Remove(string animalID)
{
Dictionary.Remove(animalID);
}
public Animals() {}
public Animal this[string animalID]
{
get { return (Animal)Dictionary[animalID]; }
set { Dictionary[animalID] = value; }
}
}
The differences in these members are as follows:
Add()
— Takes two parameters, a key and a value, to store together. The dictionary collection has a member called Dictionary
inherited from DictionaryBase
, which is an IDictionary
interface. This interface has its own Add()
method, which takes two object parameters. Your implementation takes a string value as a key and an Animal
object as the data to store alongside this key.Remove()
— Takes a key parameter, rather than an object reference. The item with the key value specified is removed.Indexer
— Uses a string key value, rather than an index, which is used to access the stored item via the Dictionary
inherited member. Again, casting is necessary here.One other difference between collections based on
and collections based on DictionaryBase
is that CollectionBase
works slightly differently. The collection from the last section allowed you to extract foreach
objects directly from the collection. Using Animal
with the foreach
derived class gives you DictionaryBase
structs, another type defined in the DictionaryEntry
namespace. To get to the System.Collections
objects themselves, you must use the Animal
member of this struct, or you can use the Value
member of the struct to get the associated key. To get code equivalent to the earlierKey
foreach (Animal myAnimal in animalCollection)
{
WriteLine($"New {myAnimal.ToString()} object added to custom " +
$"collection, Name = {myAnimal.Name}");
}
you need the following:
foreach (DictionaryEntry myEntry in animalCollection)
{
WriteLine($"New {myEntry.Value.ToString()} object added to " +
$"custom collection, Name = {((Animal)myEntry.Value).Name}");
}
It is possible to override this behavior so that you can access
objects directly through Animal
. There are several ways to do this, the simplest being to implement an iterator.foreach
Earlier in this chapter, you saw that the
interface enables you to use IEnumerable
loops. It's often beneficial to use your classes in foreach
loops, not just collection classes such as those shown in previous sections.foreach
However, overriding this behavior, or providing your own custom implementation of it, is not always simple. To illustrate this, it's necessary to take a detailed look at
loops. The following steps show you what actually happens in a foreach
loop iterating through a collection called foreach
:collectionObject
collectionObject.GetEnumerator()
is called, which returns an IEnumerator
reference. This method is available through implementation of the IEnumerable
interface, although this is optional.MoveNext()
method of the returned IEnumerator
interface is called.MoveNext()
returns true
, then the Current
property of the IEnumerator
interface is used to get a reference to an object, which is used in the foreach
loop.two
steps repeat until MoveNext()
returns false
, at which point the loop terminates.To enable this behavior in your classes, you must override several methods, keep track of indices, maintain the
property, and so on. This can be a lot of work to achieve very little.Current
A simpler alternative is to use an iterator. Effectively, using iterators generates a lot of the code for you behind the scenes and hooks it all up correctly. Moreover, the syntax for using iterators is much easier to get a grip on.
A good definition of an iterator is a block of code that supplies all the values to be used in a
block in sequence. Typically, this block of code is a method, although you can also use property accessors and other blocks of code as iterators. To keep things simple, you'll just look at methods here.foreach
Whatever the block of code is, its return type is restricted. Perhaps contrary to expectations, this return type isn't the same as the type of object being enumerated. For example, in a class that represents a collection of
objects, the return type of the iterator block can't be Animal
. Two possible return types are the interface types mentioned earlier, Animal
or IEnumerable
. You use these types as follows:IEnumerator
GetEnumerator()
with a return type of IEnumerator
.IEnumerable
.Within an iterator block, you select the values to be used in the
loop by using the foreach
keyword. The syntax for doing this is as follows:yield
yield return <value>;
That information is all you need to build a very simple example, as follows (you can find this code in
):SimpleIterators\Program.cs
public static IEnumerable SimpleList()
{
yield return "string 1";
yield return "string 2";
yield return "string 3";
}
static void Main(string[] args)
{
foreach (string item in SimpleList())
WriteLine(item);
ReadKey();
}
Here, the static method
is the iterator block. Because it is a method, you use a return type of SimpleList()
. IEnumerable
uses the SimpleList()
keyword to supply three values to the yield
block that uses it, each of which is written to the screen. The result is shown in Figure 11.3.foreach
Obviously, this iterator isn't a particularly useful one, but it does show how this works in action and how simple the implementation can be. Looking at the code, you might wonder how the code knows to return
type items. In fact, it doesn't; it returns string
type values. As you know, object
is the base class for all types, so you can return anything from the object
statements.yield
However, the compiler is intelligent enough that you can interpret the returned values as whatever type you want in the context of the
loop. Here, the code asks for foreach
type values, so those are the values you get to work. Should you change one of the string
lines so that it returns, say, an integer, you would get a bad cast exception in the yield
loop.foreach
One more thing about iterators. It is possible to interrupt the return of information to the
loop by using the following statement:foreach
yield break;
When this statement is encountered in an iterator, the iterator processing terminates immediately, as does the
loop using it.foreach
Now it's time for a more complicated — and useful! — example. In this Try It Out, you'll implement an iterator that obtains prime numbers.
Earlier you were promised an explanation of how iterators can be used to iterate over the objects stored in a dictionary-type collection without having to deal with
objects. In the downloadable code for this chapter, you will find the code for the next project in the DictionaryItem
folder. Recall the collection class DictionaryAnimals
:Animals
public class Animals : DictionaryBase
{
public void Add(string newID, Animal newAnimal)
{
Dictionary.Add(newID, newAnimal);
}
public void Remove(string animalID)
{
Dictionary.Remove(animalID);
}
public Animal this[string animalID]
{
get { return (Animal)Dictionary[animalID]; }
set { Dictionary[animalID] = value; }
}
}
You can add this simple iterator to the code to get the desired behavior:
public new IEnumerator GetEnumerator()
{
foreach (object animal in Dictionary.Values)
yield return (Animal)animal;
}
Now you can use the following code to iterate through the
objects in the collection:Animal
foreach (Animal myAnimal in animalCollection)
{
WriteLine($"New {myAnimal.ToString()} object added to " +
$" custom collection, Name = {myAnimal.Name}");
}
Chapter 9 described how you can perform shallow copying with the
protected method, by using a method like the System.Object.MemberwiseClone()
one shown here:GetCopy()
public class Cloner
{
public int Val;
public Cloner(int newVal)
{
Val = newVal;
}
public object GetCopy() => MemberwiseClone();
}
Suppose you have fields that are reference types, rather than value types (for example, objects):
public class Content
{
public int Val;
}
public class Cloner
{
public Content MyContent = new Content();
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
public object GetCopy() => MemberwiseClone();
}
In this case, the shallow copy obtained though
has a field that refers to the same object as the original object. The following code, which uses this GetCopy()
class, illustrates the consequences of shallow copying reference types:Cloner
Cloner mySource = new Cloner(5);
Cloner myTarget = (Cloner)mySource.GetCopy();
WriteLine($"myTarget.MyContent.Val = {myTarget.MyContent.Val}");
mySource.MyContent.Val = 2;
WriteLine($"myTarget.MyContent.Val = {myTarget.MyContent.Val}");
The fourth line, which assigns a value to
, the mySource.MyContent.Val
public field of the Val
public field of the original object, also changes the value of MyContent
. That's because myTarget.MyContent.Val
refers to the same object instance as mySource.MyContent
. The output of the preceding code is as follows:myTarget.MyContent
myTarget.MyContent.Val = 5
myTarget.MyContent.Val = 2
To get around this, you need to perform a deep copy. You could just modify the
method used previously to do this, but it is preferable to use the standard .NET Framework way of doing things: implement the GetCopy()
interface, which has the single method ICloneable
. This method takes no parameters and returns an Clone()
type result, giving it a signature identical to the object
method used earlier.GetCopy()
To modify the preceding classes, try using the following deep copy code:
public class Content
{
public int Val;
}
public class Cloner : ICloneable
{
public Content MyContent = new Content();
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
public object Clone()
{
Cloner clonedCloner = new Cloner(MyContent.Val);
return clonedCloner;
}
}
This created a new
object by using the Cloner
field of the Val
object contained in the original Content
object (Cloner
). This field is a value type, so no deeper copying is necessary.MyContent
Using code similar to that just shown to test the shallow copy — but using
instead of Clone()
— gives you the following result:GetCopy()
myTarget.MyContent.Val = 5
myTarget.MyContent.Val = 5
This time, the contained objects are independent. Note that sometimes calls to
are made recursively, in more complex object systems. For example, if the Clone()
field of the MyContent
class also required deep copying, then you might need the following:Cloner
public class Cloner : ICloneable
{
public Content MyContent = new Content();
…
public object Clone()
{
Cloner clonedCloner = new Cloner();
clonedCloner.MyContent = MyContent.Clone();
return clonedCloner;
}
}
You're calling the default constructor here to simplify the syntax of creating a new
object. For this code to work, you would also need to implement Cloner
on the ICloneable
class.Content
You can put this into practice by implementing the capability to copy
, Card
, and Cards
objects by using the Deck
interface. This might be useful in some card games, where you might not necessarily want two decks with references to the same set of ICloneable
objects, although you might conceivably want to set up one deck to have the same card order as another.Card
Implementing cloning functionality for the
class in Card
is simple because shallow copying is sufficient (Ch11CardLib
contains only value-type data, in the form of fields). Begin by making the following changes to the Card
definition:class
public class Card : ICloneable
{
public object Clone() => MemberwiseClone();
This implementation of
is just a shallow copy. There is no rule determining what should happen in the ICloneable
method, and this is sufficient for your purposes.Clone()
Next, implement
on the ICloneable
collection class. This is slightly more complicated because it involves cloning every Cards
object in the original collection — so you need to make a deep copy:Card
public class Cards : CollectionBase, ICloneable
{
public object Clone()
{
Cards newCards = new Cards();
foreach (Card sourceCard in List)
{
newCards.Add((Card)sourceCard.Clone());
}
return newCards;
}
Finally, implement
on the ICloneable
class. Note a slight problem here: The Deck
class in Deck
has no way to modify the cards it contains, short of shuffling them. There is no way, for example, to modify a Ch11CardLib
instance to have a given card order. To get around this, define a new private constructor for the Deck
class that allows a specific Deck
collection to be passed in when the Cards
object is instantiated. Here's the code to implement cloning in this class:Deck
public class Deck : ICloneable
{
public object Clone()
{
Deck newDeck = new Deck(cards.Clone() as Cards);
return newDeck;
}
private Deck(Cards newCards)
{
cards = newCards;
}
Again, you can test this with some simple client code. As before, place this code within the
method of a client project for testing (you can find this code in Main()
in the chapter's online download):Ch11CardClient\Program.cs
Deck deck1 = new Deck();
Deck deck2 = (Deck)deck1.Clone();
WriteLine($"The first card in the original deck is: {deck1.GetCard(0)}");
WriteLine($"The first card in the cloned deck is: {deck2.GetCard(0)}");
deck1.Shuffle();
WriteLine("Original deck shuffled.");
WriteLine($"The first card in the original deck is: {deck1.GetCard(0)}");
WriteLine($"The first card in the cloned deck is: {deck2.GetCard(0)}");
ReadKey();
The output will be similar to what is shown in Figure 11.5.
This section covers two types of comparisons between objects:
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, for example — what happens next depends on the type of the object. You've seen this in passing 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, for which you might want to sort objects according to some condition, perhaps alphabetically or according to a more complicated algorithm.>
When comparing objects, you often need to know their type, which enables you to determine whether a value comparison is possible. In Chapter 9 you saw the
method, which all classes inherit from GetType()
, and how this method can be used in combination with the System.Object
operator to determine (and take action depending on) object types:typeof()
if (myObj.GetType() == typeof(MyComplexClass))
{
// myObj is an instance of the class MyComplexClass.
}
You've also seen how the default implementation of
, also inherited from ToString()
, will get you a string representation of an object's type. You can compare these strings too, although that's a rather messy way to accomplish this.System.Object
This section demonstrates a handy shorthand way of doing things: the
operator. This operator allows for much more readable code and, as you will see, has the advantage of examining base classes. Before looking at the is
operator, though, you need to be aware of what often happens behind the scenes when dealing with value types (as opposed to reference types): boxing and unboxing.is
In Chapter 8, you learned the difference between reference types 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
type or to an interface type that is implemented by the value type. Unboxing is the opposite conversion.System.Object
For example, suppose you have the following struct type:
struct MyStruct
{
public int Val;
}
You can box a struct of this type by placing it into an
-type variable:object
MyStruct valType1 = new MyStruct();
valType1.Val = 5;
object refType = valType1;
Here, you create a new variable (
) of type valType1
, assign a value to the MyStruct
member of this Val
, and then box it into an struct
-type variable (object
).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 original struct's contents and then unboxing the struct contained in the object into a new variable and examining its contents:
valType1.Val = 6;
MyStruct valType2 = (MyStruct)refType;
WriteLine($"valType2.Val = {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 see this by changing
into a class (ignoring the fact that the name of this class isn't appropriate now):MyStruct
class
MyStruct
{
public int Val;
}
With no changes to the client code shown previously (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
type implements the MyStruct
interface as follows:IMyInterface
interface IMyInterface {}
struct MyStruct : IMyInterface
{
public int Val;
}
You can then box the struct into an
type as follows:IMyInterface
MyStruct valType1 = new MyStruct();
IMyInterface refType = valType1;
You can unbox it by using the normal casting syntax:
MyStruct ValType2 = (MyStruct)refType;
As shown in these examples, boxing is performed without your intervention — that is, you don't have to write any code to make it 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 two very good reasons why boxing is extremely useful. First, it enables you to use value types in collections (such as
) where the items are of type ArrayList
. Second, it's the internal mechanism that enables you to call object
methods on value types, such as object
s and structs.int
It is worth noting that unboxing is necessary before access to the value type contents is possible.
Despite its name, the
operator isn't a way to determine whether an object is a certain type. Instead, the is
operator enables 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 is
.true
Earlier examples showed a
and a Cow
class, both of which inherit from Chicken
. Using the Animal
operator to compare objects with the is
type will return Animal
for objects of all three of these types, not just true
. This is something you'd have a hard time achieving with the Animal
method and GetType()
operator shown previously.typeof()
The
operator has the following syntax:is
<operand> is <type>
The possible results of this expression are as follows:
<
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.<
type
>
is an interface type, then the result is true
if <
operand
>
is of that type or it is a type that implements the interface.<
type
>
is a value type, then the result is true
if <
operand
>
is of that type or it is a type that can be unboxed into that type.The following Try It Out shows how this works in practice.
Consider two
objects representing people, each with an integer Person
property. You might want to compare them to see which person is older. You can simply use the following code:Age
if (person1.Age > person2.Age)
{
…
}
This works fine, but there are alternatives. You might prefer to use syntax such as the following:
if (person1 > person2)
{
…
}
This is possible using operator overloading, which you'll look at in this section. This is a powerful technique, but it 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
and IComparable
interfaces, which enable you to define how objects will be compared to each other in a standard way. This technique is supported by the various collection classes in the .NET Framework, making it an excellent way to sort objects in a collection.IComparer
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 because you can perform whatever processing you want in the implementation of the operator overload, which might not be as simple as, for example,
, meaning “add these two operands together.” Later, you'll see a good example of this in a further upgrade of the +
library, whereby 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).CardLib
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 wins 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, whereby trumps beat other suits even if that isn't the first suit laid down. This means that calculating that card1
>
is card2
(that is, true
will beat card1
if card2
is laid down first), doesn't necessarily imply that card1
card2
>
is card1
. If neither false
nor card1
are trumps and they belong to different suits, then both of these comparisons will be card2
.true
To start with, though, here's a look at the basic syntax for operator overloading. Operators can 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 the types of these operands. In general, you will have operands that are the same type as the class in which the operator is defined, although it's possible to define operators that work on mixed types, as you'll see shortly.-
As an example, consider the simple type
, defined as follows:AddClass1
public class AddClass1
{
public int val;
}
This is just a wrapper around an
value but it illustrates the principles. With this class, code such as the following will fail to compile:int
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 +
type. This is because you haven't defined an operation to perform yet. Code such as the following works, but it won't give you the result you might want:AddClass1
AddClass1 op1 = new AddClass1();
op1.val = 5;
AddClass1 op2 = new AddClass1();
op2.val = 5;
bool op3 = op1 == op2;
Here,
and op1
are compared by using the op2
binary operator to determine whether they refer to the same object, not to verify whether their values are equal. ==
will be op3
in the preceding code, even though false
and op1.val
are identical.op2.val
To overload the
operator, 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
method declarations, except that they use the keyword static
and the operator itself, rather than a method name. You can now successfully use the operator
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 have only 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
, then 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.AddClass2
In addition, if you mix types, then 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. For example, you can't use the operator like,
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:
+
, -
, !
, ˜
, ++
, --
, true
, false
+
, -
, *
, /
, %
, &
, |
, ^
, ![1](images/gtlt1.png)
, ![1](images/gtlt2.png)
==
, !=
, <
, >
, <=
, >=
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 can't be overloaded because it has such a fundamental usage, but this operator 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, 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), as shown in this example:>
public class AddClass1
{
public int val;
public static bool operator >=(AddClass1 op1, AddClass1 op2)
=> (op1.val >= op2.val);
public static bool operator <(AddClass1 op1, AddClass1 op2)
=> !(op1 >= op2);
// Also need implementations for <= and > operators.
}
In more complex operator definitions, this can reduce the lines of code. It also means that you have less code to change if you later decide to modify the implementation of these operators.
The same applies to == and !=, but with these operators it is often worth overriding
and Object.Equals()
, because both of these functions can 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 it's worth adding for completeness. It requires the following nonstatic override methods:Object.GetHashCode()
public class AddClass1
{
public int val;
public static bool operator ==(AddClass1 op1, AddClass1 op2)
=> (op1.val == op2.val);
public static bool operator !=(AddClass1 op1, AddClass1 op2)
=> !(op1 == op2);
public override bool Equals(object op1) => val == ((AddClass1)op1).val;
public override int GetHashCode() => val;
}
is used to obtain a unique GetHashCode()
value for an object instance based on its state. Here, using int
is fine, because it is also an val
value.int
Note that
uses an Equals()
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. Instead, you must use casting to get the required result. It is often worth checking the object type using the object
operator discussed earlier, in code such as this:is
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
is of the wrong type or cannot be converted into the correct type. Of course, this behavior might not be what you want. You might 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 might want to restrict comparisons to those in which both objects are of exactly the same type, which would require the following change to the first Equals
statement:if
if (op1.GetType() == typeof(AddClass1))
Now you'll upgrade your
project again, adding operator overloading to the Ch11CardLib
class. Again, you can find the code for the classes that follow in the Card
folder of this chapter's code download. First, though, you'll add the extra fields to the Ch11CardLib
class that allow for trump suits and an option to place aces high. You make these static, because when they are set, they apply to all Card
objects:Card
public class Card
{
/// <summary>
/// Flag for trump usage. If true, trumps are valued higher
/// than cards of other suits.
/// </summary>
public static bool useTrumps = false;
/// <summary>
/// Trump suit to use if useTrumps is true.
/// </summary>
public static Suit trump = Suit.Club;
/// <summary>
/// Flag that determines whether aces are higher than kings or lower
/// than deuces.
/// </summary>
public static bool isAceHigh = true;
These rules apply to all
objects in every Card
in an application. It's not possible to have two decks of cards with cards contained in each that obey different rules. That's 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 Deck
whenever decks are switched.Card
Because you have done this, it is worth adding a few more constructors to the
class to initialize decks with different characteristics:Deck
/// <summary>
/// Nondefault constructor. Allows aces to be set high.
/// </summary>
public Deck(bool isAceHigh) : this()
{
Card.isAceHigh = isAceHigh;
}
/// <summary>
/// Nondefault constructor. Allows a trump suit to be used.
/// </summary>
public Deck(bool useTrumps, Suit trump) : this()
{
Card.useTrumps = useTrumps;
Card.trump = trump;
}
/// <summary>
/// Nondefault constructor. Allows aces to be set high and a trump suit
/// to be used.
/// </summary>
public Deck(bool isAceHigh, bool useTrumps, Suit trump) : this()
{
Card.isAceHigh = isAceHigh;
Card.useTrumps = useTrumps;
Card.trump = trump;
}
Each of these constructors is defined by using the :
syntax shown in Chapter 9, so in all cases the default constructor is called before the nondefault one, initializing the deck.this()
Now add your operator overloads (and suggested overrides) to the
class:Card
public static bool operator ==(Card card1, Card card2)
=> card1?.suit == card2?.suit) && (card1?.rank == card2?.rank);
public static bool operator !=(Card card1, Card card2)
=> !(card1 == card2);
public override bool Equals(object card) => this == (Card)card;
public override int GetHashCode()
=> return 13 * (int)suit + (int)rank;
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)
=> !(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)
=> !(card1 > card2);
There's not much to note here, 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,
and card1
, where card2
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 non-trump even if the non-trump 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:card1
public static bool operator >(Card card1, Card card2)
{
if (card1.suit == card2.suit)
{
If the static
flag is isAceHigh
, then you can't compare the cards' ranks directly via their value in the true
enumeration, because the rank of ace has a value of Rank
in this enumeration, which is less than that of all other ranks. Instead, use the following steps:1
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);
}
The remainder of the code concerns the case where the suits of
and card1
are different. Here, the static card2
flag is important. If this flag is useTrumps
and true
is of the trump suit, then you can say definitively that card2
isn't a trump (because the two cards have different suits); and trumps always win, so card1
is the higher card:card2
else
{
if (useTrumps && (card2.suit == Card.trump))
return false;
If
isn't a trump (or card2
is useTrumps
), then false
wins, because it was the first card laid down:card1
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 more detail about them.>=
The following simple client code tests these operators. Simply place it in the
method of a client project to test it, like the client code shown earlier in the Main()
examples (you can find this code in CardLib
):Ch11CardClient\Program.cs
Card.isAceHigh = true;
WriteLine("Aces are high.");
Card.useTrumps = true;
Card.trump = Suit.Club;
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);
WriteLine($"{card1.ToString()} == {card2.ToString()} ? {card1 == card2}");
WriteLine($"{card1.ToString()} != {card3.ToString()} ? {card1 != card3}");
WriteLine($"{card1.ToString()}.Equals({card4.ToString()}) ? " +
$" { card1.Equals(card4)}");
WriteLine($"Card.Equals({card3.ToString()}, {card4.ToString()}) ? " +
$" { Card.Equals(card3, card4)}");
WriteLine($"{card1.ToString()} > {card2.ToString()} ? {card1 > card2}");
WriteLine($"{card1.ToString()} <= {card3.ToString()} ? {card1 <= card3}");
WriteLine($"{card1.ToString()} > {card4.ToString()} ? {card1 > card4}");
WriteLine($"{card4.ToString()} > {card1.ToString()} ? {card4 > card1}");
WriteLine($"{card5.ToString()} > {card4.ToString()} ? {card5 > card4}");
WriteLine($"{card4.ToString()} > {card5.ToString()} ? {card4 > card5}");
ReadKey();
The results are as shown in 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 non-trumps.
The
and IComparable
interfaces are the standard way to compare objects in the .NET Framework. The difference between the interfaces is as follows:IComparer
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 allows comparisons between any two objects.Typically, you give a class default comparison code by using
, and nondefault comparisons using other classes.IComparable
exposes the single method IComparable
, which accepts an object. You could, for example, implement it in a way that enables you to pass a CompareTo()
object to it and determine whether that person is older or younger than the current person. In fact, this method returns an Person
, so you could also determine how much older or younger the second person is:int
if (person1.CompareTo(person2) == 0)
{
WriteLine("Same age");
}
else if (person1.CompareTo(person2) > 0)
{
WriteLine("person 1 is Older");
}
else
{
WriteLine("person1 is Younger");
}
exposes the single method IComparer
, which accepts two objects and returns an integer result just like Compare()
. With an object supporting CompareTo()
, you could use code like the following:IComparer
if (personComparer.Compare(person1, person2) == 0)
{
WriteLine("Same age");
}
else if (personComparer.Compare(person1, person2) > 0)
{
WriteLine("person 1 is Older");
}
else
{
WriteLine("person1 is Younger");
}
In both cases, the parameters supplied to the methods are of the type
. This means that you can compare one object to another object of any other type, so you usually have to perform some type comparison before returning a result, and maybe even throw exceptions if the wrong types are used.System.Object
The .NET Framework includes a default implementation of the
interface on a class called IComparer
, found in the Comparer
namespace. This class is capable of performing culture-specific comparisons between simple types, as well as any type that supports the System.Collections
interface. You can use it, for example, with the following code:IComparable
string firstString = "First String";
string secondString = "Second String";
WriteLine($"Comparing '{firstString}' and '{secondString}', " +
$"result: {Comparer.Default.Compare(firstString, secondString)}");
int firstNumber = 35;
int secondNumber = 23;
WriteLine($"Comparing '{firstNumber}' and '{ secondNumber }', " +
$"result: {Comparer.Default.Compare(firstNumber, secondNumber)}");
This uses the
static member to obtain an instance of the Comparer.Default
class, and then uses the Comparer
method to compare first two strings, and then two integers.Compare()
The result is as follows:
Comparing 'First String' and 'Second String', result: -1
Comparing '35' and '23', result: 1
Because 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 do not indicate the magnitude of the difference.
When using
, you must use types that can be compared. Attempting to compare Comparer
with firstString
, for instance, will generate an exception.firstNumber
Here are a few more points about the behavior of this class:
Comparer.Compare()
are checked to determine whether they support IComparable
. If they do, then that implementation is used.Comparer
class must be instantiated using its constructor, which enables you to pass a System
.Globalization
.CultureInfo
object specifying the culture to use.CaseInsensitiveComparer
class, which otherwise works exactly the same.Many collection classes allow sorting, either by default comparisons between objects or by custom methods.
is one example. It contains the method ArrayList
, which can be used without parameters, in which case default comparisons are used, or it can be passed an Sort()
interface to use to compare pairs of objects.IComparer
When you have an
filled with simple types, such as integers or strings, the default comparer is fine. For your own classes, you must either implement ArrayList
in your class definition or create a separate class supporting IComparable
to use for comparisons.IComparer
Note that some classes in the
namespace, including System.Collections
, don't expose a method for sorting. If you want to sort a collection you have derived from this class, then you have to do a bit more work and sort the internal CollectionBase
collection yourself.List
The following Try It Out shows how to use a default and nondefault comparer to sort a list.
Thus far, you have used casting whenever you have needed to convert one type into another, but this isn't the only way to do things. Just as an
can be converted into a int
or a long
implicitly as part of a calculation, you can define how classes you have created can be converted into other classes (either implicitly or explicitly). To do this, you overload conversion operators, much like other operators were overloaded earlier in this chapter. You'll see how in the first part of this section. You'll also see another useful operator, the double
operator, which in general is preferable to casting when using reference types.as
As well as overloading mathematical operators, as shown earlier, you can define both implicit and explicit conversions between types. This is necessary if you want to convert between types that aren't related — if there is no inheritance relationship between them and no shared interfaces, for example.
Suppose you define an implicit conversion between
and ConvClass1
. This means that you can write code such as the following:ConvClass2
ConvClass1 op1 = new ConvClass1();
ConvClass2 op2 = op1;
Alternatively, you can define an explicit conversion:
ConvClass1 op1 = new ConvClass1();
ConvClass2 op2 = (ConvClass2)op1;
As an example, consider the following code:
public class ConvClass1
{
public int val;
public static implicit operator ConvClass2(ConvClass1 op1)
{
ConvClass2 returnVal = new ConvClass2();
returnVal.val = op1.val;
return returnVal;
}
}
public class ConvClass2
{
public double val;
public static explicit operator ConvClass1(ConvClass2 op1)
{
ConvClass1 returnVal = new ConvClass1();
checked {returnVal.val = (int)op1.val;};
return returnVal;
}
}
Here,
contains an ConvClass1
value and int
contains a ConvClass2
value. Because double
values can be converted into int
values implicitly, you can define an implicit conversion between double
and ConvClass1
. The reverse is not true, however, and you should define the conversion operator between ConvClass2
and ConvClass2
as explicit.ConvClass1
You specify this using the
and implicit
keywords as shown. With these classes, the following code is fine:explicit
ConvClass1 op1 = new ConvClass1();
op1.val = 3;
ConvClass2 op2 = op1;
A conversion in the other direction, however, requires the following explicit casting conversion:
ConvClass2 op1 = new ConvClass2();
op1.val = 3e15;
ConvClass1 op2 = (ConvClass1)op1;
Because you have used the
keyword in your explicit conversion, you will get an exception in the preceding code, as the checked
property of val
is too large to fit into the op1
property of val
.op2
The
operator converts a type into a specified reference type, using the following syntax:as
<operand> as <type>
This is possible only in certain circumstances:
<
operand
>
is of type <
type
>
<
operand
>
can be implicitly converted to type <
type
>
<
operand
>
can be boxed into type <
type
>
If no conversion from <
operand
to >
<
type
is possible, then the result of the expression will be >
.null
Conversion from a base class to a derived class is possible by using an explicit conversion, but it won't always work. Consider the two classes
and ClassA
from an earlier example, where ClassD
inherits from ClassD
:ClassA
class ClassA : IMyInterface {}
class ClassD : ClassA {}
The following code uses the
operator to convert from a as
instance stored in ClassA
into the obj1
type:ClassD
ClassA obj1 = new ClassA();
ClassD obj2 = obj1 as ClassD;
This will result in
being obj2
.null
However, it is possible to store
instances in ClassD
-type variables by using polymorphism. The following code illustrates this, using the ClassA
operator to convert from a as
-type variable containing a ClassA
-type instance into the ClassD
type:ClassD
ClassD obj1 = new ClassD();
ClassA obj2 = obj1;
ClassD obj3 = obj2 as ClassD;
This time the result is that
ends up containing a reference to the same object as obj3
, not obj1
.null
This functionality makes the
operator very useful, because the following code (which uses simple casting) results in an exception being thrown:as
ClassA obj1 = new ClassA();
ClassD obj2 = (ClassD)obj1;
The
equivalent of this code results in a as
value being assigned to null
— no exception is thrown. This means that code such as the following (using two of the classes developed earlier in this chapter, obj2
and a class derived from Animal
called Animal
) is very common in C# applications:Cow
public void MilkCow(Animal myAnimal)
{
Cow myCow = myAnimal as Cow;
if (myCow != null)
{
myCow.Milk();
}
else
{
WriteLine($"{myAnimal.Name} isn't a cow, and so can't be milked.");
}
}
This is much simpler than checking for exceptions!
Key Concept | Description |
Defining collections | Collections are classes that can contain instances of other classes. You can define a collection by deriving from , or implement collection interfaces such as , , and yourself. Typically, you will define an indexer for your collection in order to use syntax to access members. |
Dictionaries | You can also define keyed collections, or dictionaries, whereby each item has an associated key. In this case, the key can be used to identify an item, rather than using the item's index. You can define a dictionary by implementing or by deriving a class from . |
Iterators | You can implement an iterator to control how looping code obtains values in its loop cycles. To iterate over a class, implement a method called with a return type of . To iterate over a class member, such as a method, use a return type of . In iterator code blocks, return values with the keyword. |
Type comparisons | You can use the method to obtain the type of an object, or the operator to get the type of a class. You can then compare these type values. You can also use the operator to determine whether an object is compatible with a certain class type. |
Value comparisons | If you want to make classes whose instances can be compared using standard C# operators, you must overload those operators in the class definition. For other types of value comparison, you can use classes that implement the or interfaces. These interfaces are particularly useful for sorting collections. |
The operator |
You can use the operator to convert a value to a reference type. If no conversion is possible, the operator returns a value. |