The recipes in this chapter show you how to implement patterns you will use frequently during the development of Microsoft .NET Framework applications. Some of these patterns are formalized using interfaces defined in the .NET Framework class library. Others are less rigid, but still require you to take specific approaches to their design and implementation of your types. The recipes in this chapter describe how to do the following:
Create custom serializable types that you can easily store to disk, send across the network, or pass by value across application domain boundaries (recipe 13-1)
Provide a mechanism that creates accurate and complete copies (clones) of objects (recipe 13-2)
Implement types that are easy to compare and sort (recipe 13-3)
Support the enumeration of the elements contained in custom collections using a default or custom iterator (recipes 13-4 and 13-5)
Ensure that a type that uses unmanaged resources correctly releases those resources when they are no longer needed (recipe 13-6)
Display string representations of objects that vary based on format specifiers (recipe 13-7)
Correctly implement custom exception and event argument types, which you will use frequently in the development of your applications (recipes 13-8 and 13-9)
Implement the commonly used Singleton and Observer design patterns using the built-in features of C# and the .NET Framework class library (recipes 13-10 and 13-11)
Implement the producer/consumer pattern to coordinate multiple threads or tasks safely (recipe 13-12)
Defer initialization of a type until the first time it is used (recipe 13-13)
Define a method that has optional parameters with default values (recipe 13-14).
Define an extension method (recipe 13-15)
Invoke type members dynamically and without static checking (recipe 13-16)
Create variant generic types (recipe 13-18)
You need to implement a custom type that is serializable, allowing you to do the following:
Store instances of the type to persistent storage (for example, a file or a database)
Transmit instances of the type across a network
Pass instances of the type by value across application domain boundaries
For serialization of simple types, apply the attribute System.SerializableAttribute
to the type declaration. For types that are more complex, or to control the content and structure of the serialized data, implement the interface System.Runtime.Serialization.ISerializable
.
Recipe 2-13 showed how to serialize and deserialize an object using the formatter classes provided with the .NET Framework class library. However, types are not serializable by default. To implement a custom type that is serializable, you must apply the attribute SerializableAttribute
to your type declaration. As long as all of the data fields in your type are serializable types, applying SerializableAttribute
is all you need to do to make your custom type serializable. If you are implementing a custom class that derives from a base class, the base class must also be serializable.
Classes that derive from a serializable type don't inherit the attribute SerializableAttribute
. To make derived types serializable, you must explicitly declare them as serializable by applying the SerializableAttribute
attribute.
Each formatter class contains the logic necessary to serialize types decorated with SerializableAttribute
, and will correctly serialize all public, protected, and private fields. You can exclude specific fields from serialization by applying the attribute System.NonSerializedAttribute
to those fields. As a rule, you should exclude the following fields from serialization:
Fields that contain unserializable data types
Fields that contain values that might be invalid when the object is deserialized, such as database connections, memory addresses, thread IDs, and unmanaged resource handles
Fields that contain sensitive or secret information, such as passwords, encryption keys, and the personal details of people and organizations
Fields that contain data that is easily re-creatable or retrievable from other sources, especially if there is a lot of data
See Recipe 2-14 for an example of using a slightly different method to exclude members from serialization when using the JSON format.
If you exclude fields from serialization, you must implement your type to compensate for the fact that some data will not be present when an object is deserialized. Unfortunately, you cannot create or retrieve the missing data fields in an instance constructor, because formatters do not call constructors during the process of deserializing objects. The best approach for achieving fine-grained control of the serialization of your custom types is to use the attributes from the System.Runtime.Serialization
namespace described in Table 13-1. These attributes allow you to identify methods of the serializable type that the serialization process should execute before and after serialization and deserialization. Any method annotated with one of these attributes must take a single System.Runtime.Serialization. StreamingContext
argument, which contains details about the source or intended destination of the serialized object so that you can determine what to serialize. For example, you might be happy to serialize secret data if it's destined for another application domain in the same process, but not if the data will be written to a file.
Table 13.1. Attributes to Customize the Serialization and Deserialization Processs
Attribute | Description |
---|---|
| Apply this attribute to a method to have it executed before the object is serialized. This is useful if you need to modify object state before it is serialized. For example, you may need to convert a |
| Apply this attribute to a method to have it executed after the object is serialized. This is useful in case you need to revert the object state to what it was before the method annotated with |
| Apply this attribute to a method to have it executed before the object is deserialized. This is useful if you need to modify the object state prior to deserialization. |
| Apply this attribute to a method to have it executed after the object is deserialized. This is useful if you need to re-create additional object state that depends on the data that was deserialized with the object or modify the deserialized state before the object is used. |
As types evolve, you often add new member variables to support new features. This new state causes a problem when deserializing old objects because the new member variables are not part of the serialized object. The.NET Framework supports the attribute System.Runtime.Serialization. OptionalFieldAttribute
. When you create a new version of a type and add data members, annotate them with OptionalFieldAttribute
, and the deserialization process will not fail if they are not present. You can then use a method annotated with OnDeserializedAttribute
(see Table 13-1) to configure the new member variables appropriately.
For the majority of custom types, the mechanisms described will be sufficient to meet your serialization needs. If you require more control over the serialization process, you can implement the interface ISerializable
. The formatter classes use different logic when serializing and deserializing instances of types that implement ISerializable
. To implement ISerializable
correctly, you must do the following:
Declare that your type implements ISerializable
Apply the attribute SerializableAttribute
to your type declaration as just described. Do not use NonSerializedAttribute
, because it will have no effect.
Implement the ISerializable.GetObjectData
method (used during serialization), which takes the argument types System.Runtime.Serialization. SerializationInfo
and System.Runtime.Serialization.StreamingContext
.
Implement a nonpublic constructor (used during deserialization) that accepts the same arguments as the GetObjectData
method. Remember that if you plan to derive classes from your serializable class, you should make the constructor protected.
During serialization, the formatter calls the GetObjectData
method and passes it SerializationInfo
and StreamingContext
references as arguments. Your type must populate the SerializationInfo
object with the data you want to serialize.
If you are creating a serializable class from a base class that also implements ISerializable
, your type's GetObjectData
method and deserialization constructor must call the equivalent method and constructor in the parent class.
The SerializationInfo
class acts as a list of field/value pairs and provides the AddValue
method to let you store a field with its value. In each call to AddValue
, you must specify a name for the field/value pair; you use this name during deserialization to retrieve the value of each field. The AddValue
method has 16 overloads that allow you to add values of different data types to the SerializationInfo
object.
The StreamingContext
object, as described earlier, provides information about the purpose and destination of the serialized data, allowing you to choose which data to serialize.
When a formatter deserializes an instance of your type, it calls the deserialization constructor, again passing a SerializationInfo
and a StreamingContext
reference as arguments. Your type must extract the serialized data from the SerializationInfo
object using one of the SerializationInfo.Get*
methods; for example, using GetString, GetInt32
, or GetBoolean
. During deserialization, the StreamingContext
object provides information about the source of the serialized data, allowing you to mirror the logic you implemented for serialization.
This following example demonstrates a serializable Employee
class that implements the ISerializable
interface. In this example, the Employee
class does not serialize the address
field if the provided StreamingContext
object specifies that the destination of the serialized data is a file. The Main
method demonstrates the serialization and deserialization of an Employee
object.
using System; using System.IO; using System.Text; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; namespace Apress.VisualCSharpRecipes.Chapter13 { [Serializable] public class Employee : ISerializable { private string name; private int age; private string address; // Simple Employee constructor. public Employee(string name, int age, string address) { this.name = name; this.age = age; this.address = address; } // Constructor required to enable a formatter to deserialize an // Employee object. You should declare the constructor private or at // least protected to ensure it is not called unnecessarily. private Employee(SerializationInfo info, StreamingContext context) { // Extract the name and age of the employee, which will always be // present in the serialized data regardless of the value of the // StreamingContext. name = info.GetString("Name"); age = info.GetInt32("Age"); // Attempt to extract the employee's address and fail gracefully // if it is not available. try { address = info.GetString("Address"); }
catch (SerializationException) { address = null; } } // Public property to provide access to employee's name. public string Name { get { return name; } set { name = value; } } // Public property to provide access to employee's age. public int Age { get { return age; } set { age = value; } } // Public property to provide access to employee's address. // Uses lazy initialization to establish address because // a deserialized object will not have an address value. public string Address { get { if (address == null) { // Load the address from persistent storage. // In this case, set it to an empty string. address = String.Empty; } return address; } set { address = value; } } // Declared by the ISerializable interface, the GetObjectData method // provides the mechanism with which a formatter obtains the object // data that it should serialize. public void GetObjectData(SerializationInfo inf, StreamingContext con) { // Always serialize the employee's name and age. inf.AddValue("Name", name); inf.AddValue("Age", age);
// Don't serialize the employee's address if the StreamingContext // indicates that the serialized data is to be written to a file. if ((con.State & StreamingContextStates.File) == 0) { inf.AddValue("Address", address); } } // Override Object.ToString to return a string representation of the // Employee state. public override string ToString() { StringBuilder str = new StringBuilder(); str.AppendFormat("Name: {0} ", Name); str.AppendFormat("Age: {0} ", Age); str.AppendFormat("Address: {0} ", Address); return str.ToString(); } } // A class to demonstrate the use of Employee. public class Recipe13_01 { public static void Main(string[] args) { // Create an Employee object representing Roger. Employee roger = new Employee("Roger", 56, "London"); // Display Roger. Console.WriteLine(roger); // Serialize Roger specifying another application domain as the // destination of the serialized data. All data including Roger's // address is serialized. Stream str = File.Create("roger.bin"); BinaryFormatter bf = new BinaryFormatter(); bf.Context = new StreamingContext(StreamingContextStates.CrossAppDomain); bf.Serialize(str, roger); str.Close(); // Deserialize and display Roger. str = File.OpenRead("roger.bin"); bf = new BinaryFormatter(); roger = (Employee)bf.Deserialize(str); str.Close(); Console.WriteLine(roger);
// Serialize Roger specifying a file as the destination of the // serialized data. In this case, Roger's address is not included // in the serialized data. str = File.Create("roger.bin"); bf = new BinaryFormatter(); bf.Context = new StreamingContext(StreamingContextStates.File); bf.Serialize(str, roger); str.Close(); // Deserialize and display Roger. str = File.OpenRead("roger.bin"); bf = new BinaryFormatter(); roger = (Employee)bf.Deserialize(str); str.Close(); Console.WriteLine(roger); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You need to create a custom type that provides a simple mechanism for programmers to create copies of type instances.
When you assign one value type to another, you create a copy of the value. No link exists between the two values—a change to one will not affect the other. However, when you assign one reference type to another (excluding strings, which receive special treatment by the runtime), you do not create a new copy of the reference type. Instead, both reference types refer to the same object, and changes to the value of the object are reflected in both references. To create a true copy of a reference type, you must clone the object to which it refers.
The ICloneable
interface identifies a type as cloneable and declares the Clone
method as the mechanism through which you obtain a clone of an object. The Clone
method takes no arguments, and returns a System.Object
, regardless of the implementing type. This means that once you clone an object, you must explicitly cast the clone to the correct type.
The approach you take to implement the Clone
method for a custom type depends on the data members declared within the type. If the custom type contains only value-type data members (int, byte
, and so on) and System.String
data members, you can implement the Clone
method by instantiating a new object and setting its data members to the same values as the current object. The Object
class (from which all types derive) includes the protected method MemberwiseClone
, which automates this process.
If your custom type contains reference-type data members, you must decide whether your Clone
method will perform a shallow copy or a deep copy. A shallow copy means that any reference-type data members in the clone will refer to the same objects as the equivalent reference-type data members in the original object. A deep copy means that you must create clones of the entire object graph so that the reference-type data members of the clone refer to physically independent copies (clones) of the objects referenced by the original object.
A shallow copy is easy to implement using the MemberwiseClone
method just described. However, a deep copy is often what programmers expect when they first clone an object, but it's rarely what they get. This is especially true of the collection classes in the System.Collections
namespace, which all implement shallow copies in their Clone
methods. Although it would often be useful if these collections implemented a deep copy, there are two key reasons why types (especially generic collection classes) do not implement deep copies:
Creating a clone of a large object graph is processor-intensive and memory-intensive.
General-purpose collections can contain wide and deep object graphs consisting of any type of object. Creating a deep-copy implementation to cater to such variety is not feasible because some objects in the collection might not be cloneable, and others might contain circular references, which would send the cloning process into an infinite loop.
For strongly typed collections in which the nature of the contained elements are understood and controlled, a deep copy can be a very useful feature; for example, System.Xml.XmlNode
implements a deep copy in its Clone
method. This allows you to create true copies of entire XML object hierarchies with a single statement.
If you need to clone an object that does not implement ICloneable
but is serializable, you can often serialize and then deserialize the object to achieve the same result as cloning. However, be aware that the serialization process might not serialize all data members (as discussed in recipe 13-1). Likewise, if you create a custom serializable type, you can potentially use the serialization process just described to perform a deep copy within your ICloneable.Clone
method implementation. To clone a serializable object, use the class System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
to serialize the object to, and then deserialize the object from a System.IO.MemoryStream
object.
The following example demonstrates various approaches to cloning. The simple class Employee
contains only string
and int
members, and so relies on the inherited MemberwiseClone
method to create a clone. The Team
class contains an implementation of the Clone
method that performs a deep copy. The Team
class contains a collection of Employee
objects, representing a team of people. When you call the Clone
method of a Team
object, the method creates a clone of every contained Employee
object and adds it to the cloned Team
object. The Team
class provides a private constructor to simplify the code in the Clone
method. The use of constructors is a common approach to simplifying the cloning process.
using System; using System.Text; using System.Collections.Generic; namespace Apress.VisualCSharpRecipes.Chapter13 { public class Employee : ICloneable { public string Name; public string Title; public int Age; // Simple Employee constructor. public Employee(string name, string title, int age) { Name = name; Title = title; Age = age; } // Create a clone using the Object.MemberwiseClone method because the // Employee class contains only string and value types. public object Clone() { return MemberwiseClone(); } // Returns a string representation of the Employee object. public override string ToString() { return string.Format("{0} ({1}) - Age {2}", Name, Title, Age); } } public class Team : ICloneable { // A List to hold the Employee team members. public List<Employee> TeamMembers = new List<Employee>();
public Team() { } // Private constructor called by the Clone method to create a new Team // object and populate its List with clones of Employee objects from // a provided List. private Team(List<Employee> members) { foreach (Employee e in members) { // Clone the individual employee objects and // add them to the List. TeamMembers.Add((Employee)e.Clone()); } } // Adds an Employee object to the Team. public void AddMember(Employee member) { TeamMembers.Add(member); } // Override Object.ToString to return a string representation of the // entire Team. public override string ToString() { StringBuilder str = new StringBuilder(); foreach (Employee e in TeamMembers) { str.AppendFormat(" {0} ", e); } return str.ToString(); } // Implementation of ICloneable.Clone. public object Clone() { // Create a deep copy of the team by calling the private Team // constructor and passing the ArrayList containing team members. return new Team(this.TeamMembers); // The following command would create a shallow copy of the Team. // return MemberwiseClone(); } }
// A class to demonstrate the use of Employee. public class Recipe13_02 { public static void Main() { // Create the original team. Team team = new Team(); team.AddMember(new Employee("Frank", "Developer", 34)); team.AddMember(new Employee("Kathy", "Tester", 78)); team.AddMember(new Employee("Chris", "Support", 18)); // Clone the original team. Team clone = (Team)team.Clone(); // Display the original team. Console.WriteLine("Original Team:"); Console.WriteLine(team); // Display the cloned team. Console.WriteLine("Clone Team:"); Console.WriteLine(clone); // Make change. Console.WriteLine("*** Make a change to original team ***"); Console.WriteLine(Environment.NewLine); team.TeamMembers[0].Name = "Luke"; team.TeamMembers[0].Title = "Manager"; team.TeamMembers[0].Age = 44; // Display the original team. Console.WriteLine("Original Team:"); Console.WriteLine(team); // Display the cloned team. Console.WriteLine("Clone Team:"); Console.WriteLine(clone); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You need to provide a mechanism that allows you to compare custom types, enabling you to easily sort collections containing instances of those types.
To provide a standard comparison mechanism for a type, implement the generic System.IComparable<T>
interface. To support the comparison of a type based on more than one characteristic, create separate types that implement the generic System.Collections.Generic.IComparer<T>
interface.
If you need to sort your type into only a single order, such as ascending ID number, or alphabetically based on surname, you should implement the IComparable<T>
interface. IComparable<T>
defines a single method named CompareTo
, shown here:
int CompareTo(T other);
The value returned by CompareTo
should be calculated as follows:
If the current object is less than other
, return less than zero (for example,−1
).
If the current object has the same value as other
, return zero.
If the current object is greater than other
, return greater than zero (for example, 1
).
What these comparisons mean depends on the type implementing the IComparable
interface. For example, if you were sorting people based on their surname, you would do a String
comparison on this field. However, if you wanted to sort by birthday, you would need to perform a comparison of the corresponding System.DateTime
fields.
To support a variety of sort orders for a particular type, you must implement separate helper types that implement the IComparer<T>
interface, which defines the Compare
method shown here:
int Compare(T x, T y);
These helper types must encapsulate the necessary logic to compare two objects and return a value based on the following logic:
If x
is less than y
, return less than zero (for example, −1
).
If x
has the same value as y
, return zero.
If x
is greater than y
, return greater than zero (for example, 1
).
The Newspaper
class listed here demonstrates the implementation of both the IComparable
and IComparer
interfaces. The Newspaper.CompareTo
method performs a case-insensitive comparison of two Newspaper
objects based on their name
fields. A private nested class named AscendingCirculationComparer
implements IComparer
and compares two Newspaper
objects based on their circulation
fields. An AscendingCirculationComparer
object is obtained using the static Newspaper.CirculationSorter
property.
The Main
method shown here demonstrates the comparison and sorting capabilities provided by implementing the IComparable
and IComparer
interfaces. The method creates a System.Collections. ArrayList
collection containing five Newspaper
objects. Main
then sorts the ArrayList
twice using the ArrayList.Sort
method. The first Sort
operation uses the default Newspaper
comparison mechanism provided by the IComparable.CompareTo
method. The second Sort
operation uses an AscendingCirculationComparer
object to perform comparisons through its implementation of the IComparer.Compare
method.
using System; using System.Collections.Generic; namespace Apress.VisualCSharpRecipes.Chapter13 { public class Newspaper : IComparable<Newspaper> { private string name; private int circulation; private class AscendingCirculationComparer : IComparer<Newspaper> { // Implementation of IComparer.Compare. The generic definition of // IComparer allows us to ensure both arguments are Newspaper // objects. public int Compare(Newspaper x, Newspaper y) { // Handle logic for null reference as dictated by the // IComparer interface. Null is considered less than // any other value. if (x == null && y == null) return 0; else if (x == null) return −1; else if (y == null) return 1; // Short-circuit condition where x and y are references // to the same object. if (x == y) return 0; // Compare the circulation figures. IComparer dictates that: // return less than zero if x < y // return zero if x = y // return greater than zero if x > y
// This logic is easily implemented using integer arithmetic. return x.circulation - y.circulation; } } // Simple Newspaper constructor. public Newspaper(string name, int circulation) { this.name = name; this.circulation = circulation; } // Declare a read-only property that returns an instance of the // AscendingCirculationComparer. public static IComparer<Newspaper> CirculationSorter { get { return new AscendingCirculationComparer(); } } // Override Object.ToString. public override string ToString() { return string.Format("{0}: Circulation = {1}", name, circulation); } // Implementation of IComparable.CompareTo. The generic definition // of IComparable allows us to ensure that the argument provided // must be a Newspaper object. Comparison is based on a // case-insensitive comparison of the Newspaper names. public int CompareTo(Newspaper other) { // IComparable dictates that an object is always considered greater // than null. if (other == null) return 1; // Short-circuit the case where the other Newspaper object is a // reference to this one. if (other == this) return 0; // Calculate return value by performing a case-insensitive // comparison of the Newspaper names. // Because the Newspaper name is a string, the easiest approach // is to rely on the comparison capabilities of the String // class, which perform culture-sensitive string comparisons. return string.Compare(this.name, other.name, true); } }
// A class to demonstrate the use of Newspaper. public class Recipe13_03 { public static void Main() { List<Newspaper> newspapers = new List<Newspaper>(); newspapers.Add(new Newspaper("The Echo", 125780)); newspapers.Add(new Newspaper("The Times", 55230)); newspapers.Add(new Newspaper("The Gazette", 235950)); newspapers.Add(new Newspaper("The Sun", 88760)); newspapers.Add(new Newspaper("The Herald", 5670)); Console.Clear(); Console.WriteLine("Unsorted newspaper list:"); foreach (Newspaper n in newspapers) { Console.WriteLine(" " + n); } Console.WriteLine(Environment.NewLine); Console.WriteLine("Newspaper list sorted by name (default order):"); newspapers.Sort(); foreach (Newspaper n in newspapers) { Console.WriteLine(" " + n); } Console.WriteLine(Environment.NewLine); Console.WriteLine("Newspaper list sorted by circulation:"); newspapers.Sort(Newspaper.CirculationSorter); foreach (Newspaper n in newspapers) { Console.WriteLine(" " + n); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
Running the example will produce the results shown here. The first list of newspapers is unsorted, the second is sorted using the IComparable
interface, and the third is sorted using a comparer class that implements IComparer
.
Unsorted newspaper list: The Echo: Circulation = 125780 The Times: Circulation = 55230 The Gazette: Circulation = 235950 The Sun: Circulation = 88760 The Herald: Circulation = 5670 Newspaper list sorted by name (default order): The Echo: Circulation = 125780 The Gazette: Circulation = 235950 The Herald: Circulation = 5670 The Sun: Circulation = 88760 The Times: Circulation = 55230 Newspaper list sorted by circulation: The Herald: Circulation = 5670 The Times: Circulation = 55230 The Sun: Circulation = 88760 The Echo: Circulation = 125780 The Gazette: Circulation = 235950
Implement the generic interface System.Collections.Generic.IEnumerable<T>
on your collection type. The GetEnumerator
method of the IEnumerable
interface returns an enumerator, which is an object that implements the interface System.Collections.Generic.IEnumerator<T>
. Within the GetEnumerator
method, traverse the items in the collection using whatever logic is appropriate to your data structure and return the next value using the yield return
statement. The C# compiler will automatically generate the necessary code to enable enumeration across the contents of your type.
A numeric indexer allows you to iterate through the elements of most standard collections using a for
loop. However, this technique does not always provide an appropriate abstraction for nonlinear data structures, such as trees and multidimensional collections. The foreach
statement provides an easy-to-use and syntactically elegant mechanism for iterating through a collection of objects, regardless of their internal structures.
In order to support foreach
semantics, the type containing the collection of objects should implement the IEnumerable<T>
interface. The IEnumerable<T>
interface declares a single method named GetEnumerator
, which takes no arguments and returns an object that implements IEnumerator<T>
. All you need to do in your GetEnumerator
method is write the code necessary to iterate through the items in your collection using logic appropriate to the data structure. Each time you want to return an item, call the yield return
statement and specify the value to return. The compiler generates code that returns the specified value and maintains appropriate state for the next time a value is requested. If you need to stop partway through the enumeration, call the yield break
statement instead, and the enumeration will terminate as if it had reached the end of the collection.
You do not actually need to explicitly implement IEnumerable
on your type to make it enumerable. As long as it has a GetEnumerator
method that returns an IEnumerator
instance, the compiler will allow you to use the type in a foreach
statement. However, it is always good practice to explicitly declare the capabilities of a type by declaring the interfaces it implements, as it allows users of your class to more easily understand its capabilities and purpose.
The GetEnumerator
method is used automatically whenever you use an instance of your collection type in a foreach
statement. However, if you want to provide multiple ways to enumerate the items in your collection, you can implement multiple methods or properties that are declared to return IEnumerable<T>
instances. Within the body of the member, use the yield return
statement just mentioned, and the C# compiler will generate the appropriate code automatically. To use one of the alternative enumerations from a foreach
statement, you must directly reference the appropriate member, as in this example:
foreach (node n in Tree.BreadthFirst)
The following example demonstrates the creation of an enumerable collection using the IEnumerable<T>
and IEnumerator<T>
interfaces in conjunction with the yield return
and yield break
statements. The Team
class, which represents a team of people, is a collection of enumerable TeamMember
objects.
using System; using System.Collections.Generic; namespace Apress.VisualCSharpRecipes.Chapter13 { // The TeamMember class represents an individual team member. public class TeamMember { public string Name; public string Title; // Simple TeamMember constructor. public TeamMember(string name, string title) { Name = name; Title = title; } // Returns a string representation of the TeamMember. public override string ToString() { return string.Format("{0} ({1})", Name, Title); } } // Team class represents a collection of TeamMember objects. public class Team { // A List to contain the TeamMember objects. private List<TeamMember> teamMembers = new List<TeamMember>();
// Implement the GetEnumerator method, which will support // iteration across the entire team member List. public IEnumerator<TeamMember> GetEnumerator() { foreach (TeamMember tm in teamMembers) { yield return tm; } } // Implement the Reverse method, which will iterate through // the team members in alphabetical order. public IEnumerable<TeamMember> Reverse { get { for (int c = teamMembers.Count - 1; c >= 0; c--) { yield return teamMembers[c]; } } } // Implement the FirstTwo method, which will stop the iteration // after only the first two team members. public IEnumerable<TeamMember> FirstTwo { get { int count = 0; foreach (TeamMember tm in teamMembers) { if (count >= 2) { // Terminate the iterator. yield break; } else { // Return the TeamMember and maintain the iterator. count++; yield return tm; } } } }
// Adds a TeamMember object to the Team. public void AddMember(TeamMember member) { teamMembers.Add(member); } } // A class to demonstrate the use of Team. public class Recipe13_04 { public static void Main() { // Create and populate a new Team. Team team = new Team(); team.AddMember(new TeamMember("Curly", "Clown")); team.AddMember(new TeamMember("Nick", "Knife Thrower")); team.AddMember(new TeamMember("Nancy", "Strong Man")); // Enumerate the entire Team using the default iterator. Console.Clear(); Console.WriteLine("Enumerate using default iterator:"); foreach (TeamMember member in team) { Console.WriteLine(" " + member.ToString()); } // Enumerate the first two Team members only. Console.WriteLine(Environment.NewLine); Console.WriteLine("Enumerate using the FirstTwo iterator:"); foreach (TeamMember member in team.FirstTwo) { Console.WriteLine(" " + member.ToString()); } // Enumerate the entire Team in reverse order. Console.WriteLine(Environment.NewLine); Console.WriteLine("Enumerate using the Reverse iterator:"); foreach (TeamMember member in team.Reverse) { Console.WriteLine(" " + member.ToString()); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You need to create an enumerable type but do not want to rely on the built-in iterator support provided by the .NET Framework (described in recipe 13-4).
Implement the interface System.Collections.IEnumerable
on your collection type. The GetEnumerator
method of the IEnumerable
interface returns an enumerator, which is an object that implements the interface System.Collections.IEnumerator
. The IEnumerator
interface defines the methods used by the foreach
statement to enumerate the collection.
Implement a private inner class within the enumerable type that implements the interface IEnumerator
and can iterate over the enumerable type while maintaining appropriate state information. In the GetEnumerator
method of the enumerable type, create and return an instance of the iterator class.
The automatic iterator support built into C# is very powerful and will be sufficient in the majority of cases. However, in some cases you may want to take direct control of the implementation of your collection's iterators. For example, you may want an iterator that supports changes to the underlying collection during enumeration.
Whatever your reason, the basic model of an enumerable collection is the same as that described in recipe 13-4. Your enumerable type should implement the IEnumerable
interface, which requires you to implement a method named GetEnumerator
. However, instead of using the yield return
statement in GetEnumerator
, you must instantiate and return an object that implements the IEnumerator
interface. The IEnumerator
interface provides a read-only, forward-only cursor for accessing the members of the underlying collection. Table 13-2 describes the members of the IEnumerator
interface. The IEnumerator
instance returned by GetEnumerator
is your custom iterator—the object that actually supports enumeration of the collection's data elements.
Table 13.2. Members of the IEnumerator Interface
Member | Description |
---|---|
| Property that returns the current data element. When the enumerator is created, |
| Method that moves the enumerator to the next data element in the collection. Returns |
| Method that moves the enumerator to a position preceding the first element in the data collection. If the underlying source of data changes during the life of the enumerator, |
If your collection class contains different types of data that you want to enumerate separately, implementing the IEnumerable
interface on the collection class is insufficient. In this case, you would implement a number of properties that return different IEnumerator
instances.
The TeamMember, Team
, and TeamMemberEnumerator
classes in the following example demonstrate the implementation of a custom iterator using the IEnumerable
and IEnumerator
interfaces. The TeamMember
class represents a member of a team. The Team
class, which represents a team of people, is a collection of TeamMember
objects. Team
implements the IEnumerable
interface and declares a separate class, named TeamMemberEnumerator
, to provide enumeration functionality. Team
implements the Observer pattern using delegate and event members to notify all TeamMemberEnumerator
objects if their underlying Team
changes. (See recipe 13-11 for a detailed description of the Observer pattern.) The TeamMemberEnumerator
class is a private nested class, so you cannot create instances of it other than through the Team.GetEnumerator
method.
using System; using System.Collections; namespace Apress.VisualCSharpRecipes.Chapter13 { // TeamMember class represents an individual team member. public class TeamMember { public string Name; public string Title;
// Simple TeamMember constructor. public TeamMember(string name, string title) { Name = name; Title = title; } // Returns a string representation of the TeamMember. public override string ToString() { return string.Format("{0} ({1})", Name, Title); } } // Team class represents a collection of TeamMember objects. Implements // the IEnumerable interface to support enumerating TeamMember objects. public class Team : IEnumerable { // TeamMemberEnumerator is a private nested class that provides // the functionality to enumerate the TeamMembers contained in // a Team collection. As a nested class, TeamMemberEnumerator // has access to the private members of the Team class. private class TeamMemberEnumerator : IEnumerator { // The Team that this object is enumerating. private Team sourceTeam; // Boolean to indicate whether underlying Team has changed // and so is invalid for further enumeration. private bool teamInvalid = false; // Integer to identify the current TeamMember. Provides // the index of the TeamMember in the underlying ArrayList // used by the Team collection. Initialize to −1, which is // the index prior to the first element. private int currentMember = −1; // Constructor takes a reference to the Team that is the source // of enumerated data. internal TeamMemberEnumerator(Team team) { this.sourceTeam = team; // Register with sourceTeam for change notifications. sourceTeam.TeamChange += new TeamChangedEventHandler(this.TeamChange); }
// Implement the IEnumerator.Current property. public object Current { get { // If the TeamMemberEnumerator is positioned before // the first element or after the last element, then // throw an exception. if (currentMember == −1 || currentMember > (sourceTeam.teamMembers.Count - 1)) { throw new InvalidOperationException(); } //Otherwise, return the current TeamMember. return sourceTeam.teamMembers[currentMember]; } } // Implement the IEnumerator.MoveNext method. public bool MoveNext() { // If underlying Team is invalid, throw exception. if (teamInvalid) { throw new InvalidOperationException("Team modified"); } // Otherwise, progress to the next TeamMember. currentMember++; // Return false if we have moved past the last TeamMember. if (currentMember > (sourceTeam.teamMembers.Count - 1)) { return false; } else { return true; } } // Implement the IEnumerator.Reset method. // This method resets the position of the TeamMemberEnumerator // to the beginning of the TeamMembers collection. public void Reset() {
// If underlying Team is invalid, throw exception. if (teamInvalid) { throw new InvalidOperationException("Team modified"); } // Move the currentMember pointer back to the index // preceding the first element. currentMember = −1; } // An event handler to handle notifications that the underlying // Team collection has changed. internal void TeamChange(Team t, EventArgs e) { // Signal that the underlying Team is now invalid. teamInvalid = true; } } // A delegate that specifies the signature that all team change event // handler methods must implement. public delegate void TeamChangedEventHandler(Team t, EventArgs e); // An ArrayList to contain the TeamMember objects. private ArrayList teamMembers; // The event used to notify TeamMemberEnumerators that the Team // has changed. public event TeamChangedEventHandler TeamChange; // Team constructor. public Team() { teamMembers = new ArrayList(); } // Implement the IEnumerable.GetEnumerator method. public IEnumerator GetEnumerator() { return new TeamMemberEnumerator(this); } // Adds a TeamMember object to the Team. public void AddMember(TeamMember member) { teamMembers.Add(member);
// Notify listeners that the list has changed. if (TeamChange != null) { TeamChange(this, null); } } } // A class to demonstrate the use of Team. public class Recipe13_05 { public static void Main() { // Create a new Team. Team team = new Team(); team.AddMember(new TeamMember("Curly", "Clown")); team.AddMember(new TeamMember("Nick", "Knife Thrower")); team.AddMember(new TeamMember("Nancy", "Strong Man")); // Enumerate the Team. Console.Clear(); Console.WriteLine("Enumerate with foreach loop:"); foreach (TeamMember member in team) { Console.WriteLine(member.ToString()); } // Enumerate using a While loop. Console.WriteLine(Environment.NewLine); Console.WriteLine("Enumerate with while loop:"); IEnumerator e = team.GetEnumerator(); while (e.MoveNext()) { Console.WriteLine(e.Current); } // Enumerate the Team and try to add a Team Member. // (This will cause an exception to be thrown.) Console.WriteLine(Environment.NewLine); Console.WriteLine("Modify while enumerating:"); foreach (TeamMember member in team) { Console.WriteLine(member.ToString()); team.AddMember(new TeamMember("Stumpy", "Lion Tamer")); }
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
The example enumerates through the data with foreach
and while
loops and then attempts to modify the data during an enumeration, resulting in an exception. The output from the example is as follows:
Enumerate with foreach loop: Curly (Clown) Nick (Knife Thrower) Nancy (Strong Man) Enumerate with while loop: Curly (Clown) Nick (Knife Thrower) Nancy (Strong Man) Modify while enumerating: Curly (Clown)
Unhandled Exception: System.InvalidOperationException: Team modified at Apress.VisualCSharpRecipes.Chapter13.Team.TeamMemberEnumerator.MoveNext() in C:UsersAdamDocumentsWorkC# CookbookRepositoryCSHARPRECIPESSourceCode Chapter13Recipe13-05Recipe13-05.cs:line 85 at Apress.VisualCSharpRecipes.Chapter13.Recipe13_05.Main() in C:UsersAdam DocumentsWorkC# CookbookRepositoryCSH ARPRECIPESSourceCodeChapter13Recipe13-05Recipe13-05.cs:line 195 Press any key to continue . . .
You need to create a class that references unmanaged resources and provide a mechanism for users of the class to free those unmanaged resources deterministically.
Implement the System.IDisposable
interface and release the unmanaged resources when client code calls the IDisposable.Dispose
method.
An unreferenced object continues to exist on the managed heap and consume resources until the garbage collector releases the object and reclaims the resources. The garbage collector will automatically free managed resources (such as memory), but it will not free unmanaged resources (such as file handles and database connections) referenced by managed objects. If an object contains data members that reference unmanaged resources, the object must free those resources explicitly.
One solution is to declare a destructor—or finalizer—for the class (destructor is a C++ term equivalent to the more general .NET term finalizer). Prior to reclaiming the memory consumed by an instance of the class, the garbage collector calls the object's finalizer. The finalizer can take the necessary steps to release any unmanaged resources. Unfortunately, because the garbage collector uses a single thread to execute all finalizers, use of finalizers can have a detrimental effect on the efficiency of the garbage collection process, which will affect the performance of your application. In addition, you cannot control when the runtime frees unmanaged resources because you cannot call an object's finalizer directly, and you have only limited control over the activities of the garbage collector using the System.GC
class.
As a complementary mechanism to using finalizers, the .NET Framework defines the Dispose pattern as a means to provide deterministic control over when to free unmanaged resources. To implement the Dispose pattern, a class must implement the IDisposable
interface, which declares a single method named Dispose
. In the Dispose
method, you must implement the code necessary to release any unmanaged resources and remove the object from the list of objects eligible for finalization if a finalizer has been defined.
Instances of classes that implement the Dispose pattern are called disposable objects. When code has finished with a disposable object, it calls the object's Dispose
method to free all resources and make it unusable, but still relies on the garbage collector to eventually release the object memory. It's important to understand that the runtime does not enforce disposal of objects; it's the responsibility of the client to call the Dispose
method. However, because the .NET Framework class library uses the Dispose pattern extensively, C# provides the using
statement to simplify the correct use of disposable objects. The following code shows the structure of a using
statement:
using (FileStream fileStream = new FileStream("SomeFile.txt", FileMode.Open)) { // Do something with the fileStream object. }
When the code reaches the end of the block in which the disposable object was declared, the object's Dispose
method is automatically called, even if an exception is raised. Here are some points to consider when implementing the Dispose pattern:
Client code should be able to call the Dispose
method repeatedly with no adverse effects.
In multithreaded applications, it's important that only one thread execute the Dispose
method at a time. It's normally the responsibility of the client code to ensure thread synchronization, although you could decide to implement synchronization within the Dispose
method.
The Dispose
method should not throw exceptions.
Because the Dispose
method does all necessary cleaning up, you do not need to call the object's finalizer. Your Dispose
method should call the GC.SuppressFinalize
method to ensure that the finalizer is not called during garbage collection.
Implement a finalizer that calls the unmanaged cleanup part of your Dispose
method as a safety mechanism in case client code does not call Dispose
correctly. However, avoid referencing managed objects in finalizers, because you cannot be certain of the object's state.
If a disposable class extends another disposable class, the Dispose
method of the child must call the Dispose
method of its base class. Wrap the child's code in a try
block and call the parent's Dispose
method in a finally
clause to ensure execution.
Other instance methods and properties of the class should throw a System.ObjectDisposedException
exception if client code attempts to execute a method on an already disposed object.
The following example demonstrates a common implementation of the Dispose pattern.
using System; namespace Apress.VisualCSharpRecipes.Chapter13 { // Implement the IDisposable interface. public class DisposeExample : IDisposable { // Private data member to signal if the object has already been // disposed. bool isDisposed = false; // Private data member that holds the handle to an unmanaged resource. private IntPtr resourceHandle; // Constructor. public DisposeExample() { // Constructor code obtains reference to unmanaged resource. resourceHandle = default(IntPtr); } // Destructor/finalizer. Because Dispose calls GC.SuppressFinalize, // this method is called by the garbage collection process only if // the consumer of the object does not call Dispose as it should. ~DisposeExample() { // Call the Dispose method as opposed to duplicating the code to // clean up any unmanaged resources. Use the protected Dispose // overload and pass a value of "false" to indicate that Dispose is // being called during the garbage collection process, not by // consumer code. Dispose(false); } // Public implementation of the IDisposable.Dispose method, called // by the consumer of the object in order to free unmanaged resources // deterministically. public void Dispose() { // Call the protected Dispose overload and pass a value of "true" // to indicate that Dispose is being called by consumer code, not // by the garbage collector. Dispose(true); // Because the Dispose method performs all necessary cleanup, // ensure the garbage collector does not call the class destructor. GC.SuppressFinalize(this); }
// Protected overload of the Dispose method. The disposing argument // signals whether the method is called by consumer code (true), or by // the garbage collector (false). Note that this method is not part of // the IDisposable interface because it has a different signature to the // parameterless Dispose method. protected virtual void Dispose(bool disposing) { // Don't try to dispose of the object twice. if (!isDisposed) { // Determine if consumer code or the garbage collector is // calling. Avoid referencing other managed objects during // finalization. if (disposing) { // Method called by consumer code. Call the Dispose method // of any managed data members that implement the // IDisposable interface. // ... } // Whether called by consumer code or the garbage collector, // free all unmanaged resources and set the value of managed // data members to null. // Close(resourceHandle); // In the case of an inherited type, call base.Dispose(disposing). } // Signal that this object has been disposed. isDisposed = true; } // Before executing any functionality, ensure that Dispose has not // already been executed on the object. public void SomeMethod() { // Throw an exception if the object has already been disposed. if (isDisposed) { throw new ObjectDisposedException("DisposeExample"); } // Execute method functionality. // . . . } } // A class to demonstrate the use of DisposeExample. public class Recipe13_06 {
public static void Main() { // The using statement ensures the Dispose method is called // even if an exception occurs. using (DisposeExample d = new DisposeExample()) { // Do something with d. } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You need to implement a type that can create different string representations of its content based on the use of format specifiers, for use in formatted strings.
The following code fragment demonstrates the use of format specifiers in the WriteLine
method of the System.Console
class. The format specifiers are inside the braces (shown in bold in the example).
double a = 345678.5678; uint b = 12000; byte c = 254; Console.WriteLine("a = {0}, b = {1}, and c = {2}", a, b, c); Console.WriteLine("a = {0:c0}, b = {1:n4}, and c = {2,10:x5}", a, b, c);
When run on a machine configured with English (UK) regional settings, this code will result in the output shown here:
a = 345678.5678, b = 12000, and c = 254 a = £345,679, b = 12,000.0000, and c = 000fe
As you can see, changing the contents of the format specifiers changes the format of the output significantly, even though the data has not changed. To enable support for format specifiers in your own types, you must implement the IFormattable
interface. IFormattable
declares a single method named ToString
with the following signature:
string ToString(string format, IFormatProvider formatProvider);
The format
argument is a System.String
containing a format string. The format string is the portion of the format specifier that follows the colon. For example, in the format specifier {2,10:x5}
used in the previous example, x5
is the format string. The format string contains the instructions that the IFormattable
instance should use when it's generating the string representation of its content. The .NET Framework documentation for IFormattable
states that types that implement IFormattable
must support the G
(general) format string, but that the other supported format strings depend on the implementation. The format
argument will be null
if the format specifier does not include a format string component; for example, {0}
or {1,20}
.
The formatProvider
argument is a reference to an instance of a type that implements System.IFormatProvider
, and that provides access to information about the cultural and regional preferences to use when generating the string representation of the IFormattable
object. This information includes data such as the appropriate currency symbol or number of decimal places to use. By default, formatProvider
is null
, which means you should use the current thread's regional and cultural settings, available through the static method CurrentCulture
of the System.Globalization.CultureInfo
class. Some methods that generate formatted strings, such as String.Format
, allow you to specify an alternative IFormatProvider
to use such as CultureInfo, DateTimeFormatInfo
, or NumberFormatInfo
.
The .NET Framework uses IFormattable
primarily to support the formatting of value types, but it can be used to good effect with any type.
The following example contains a class named Person
that implements the IFormattable
interface. The Person
class contains the title and names of a person and will render the person's name in different formats depending on the format strings provided. The Person
class does not make use of regional and cultural settings provided by the formatProvider
argument. The Main
method demonstrates how to use the formatting capabilities of the Person
class.
using System; namespace Apress.VisualCSharpRecipes.Chapter13 { public class Person : IFormattable {
// Private members to hold the person's title and name details. private string title; private string[] names; // Constructor used to set the person's title and names. public Person(string title, params string[] names) { this.title = title; this.names = names; } // Override the Object.ToString method to return the person's // name using the general format. public override string ToString() { return ToString("G", null); } // Implementation of the IFormattable.ToString method to return the // person's name in different forms based on the format string // provided. public string ToString(string format, IFormatProvider formatProvider) { string result = null; // Use the general format if none is specified. if (format == null) format = "G"; // The contents of the format string determine the format of the // name returned. switch (format.ToUpper()[0]) { case 'S': // Use short form - first initial and surname. result = names[0][0] + ". " + names[names.Length - 1]; break; case 'P': // Use polite form - title, initials, and surname. // Add the person's title to the result. if (title != null && title.Length != 0) { result = title + ". "; } // Add the person's initials and surname. for (int count = 0; count < names.Length; count++) { if (count != (names.Length - 1)) { result += names[count][0] + ". "; }
else { result += names[count]; } } break; case 'I': // Use informal form - first name only. result = names[0]; break; case 'G': default: // Use general/default form - first name and surname. result = names[0] + " " + names[names.Length - 1]; break; } return result; } } // A class to demonstrate the use of Person. public class Recipe13_07 { public static void Main() { // Create a Person object representing a man with the name // Mr. Richard Glen David Peters. Person person = new Person("Mr", "Richard", "Glen", "David", "Peters"); // Display the person's name using a variety of format strings. System.Console.WriteLine("Dear {0:G},", person); System.Console.WriteLine("Dear {0:P},", person); System.Console.WriteLine("Dear {0:I},", person); System.Console.WriteLine("Dear {0},", person); System.Console.WriteLine("Dear {0:S},", person); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You need to create a custom exception class so that you can use the runtime's exception-handling mechanism to handle application-specific exceptions.
Create a serializable class that extends the System.Exception
class. Add support for any custom data members required by the exception, including constructors and properties required to manipulate the data members.
Exception classes are unique in the fact that you do not declare new classes solely to implement new or extended functionality. The runtime's exception-handling mechanism—exposed by the C# statements try, catch
, and finally
—works based on the type of exception thrown, not the functional or data members implemented by the thrown exception.
If you need to throw an exception, you should use an existing exception class from the .NET Framework class library, if a suitable one exists. For example, some useful exceptions include the following:
System.ArgumentNullException
, when code passes a null
argument value that does not support null
arguments to your method
System.ArgumentOutOfRangeException
, when code passes an inappropriately large or small argument value to your method
System.FormatException
, when code attempts to pass your method a String
argument containing incorrectly formatted data
If none of the existing exception classes meet your needs, or you feel your application would benefit from using application-specific exceptions, it's a simple matter to create your own exception class. In order to integrate your custom exception with the runtime's exception-handling mechanism and remain consistent with the pattern implemented by .NET Framework–defined exception classes, you should do the following:
Give your exception class a meaningful name ending in the word Exception
, such as TypeMismatchException
or RecordNotFoundException
.
Mark your exception class as sealed
if you do not intend other exception classes to extend it.
Implement additional data members and properties to support custom information that the exception class should provide.
Implement three public constructors with the signatures shown here and ensure that they call the base class constructor:
public CustomException() : base() {} public CustomException(string msg): base(msg) {} public CustomException(string msg, Exception inner) : base(msg, inner) {}
Make your exception class serializable so that the runtime can marshal instances of your exception across application domain and machine boundaries. Applying the attribute System.SerializableAttribute
is sufficient for exception classes that do not implement custom data members. However, because Exception
implements the interface System.Runtime.Serialization.ISerializable
, if your exception declares custom data members, you must override the ISerializable.GetObjectData
method of the Exception
class as well as implement a deserialization constructor with this signature. If your exception class is sealed, mark the deserialization constructor as private
; otherwise, mark it as protected
. The GetObjectData
method and deserialization constructor must call the equivalent base class method to allow the base class to serialize and deserialize its data correctly. (See recipe 13-1 for details on making classes serializable.)
In large applications, you will usually implement quite a few custom exception classes. It pays to put significant thought into how you organize your custom exceptions and how code will use them. Generally, avoid creating new exception classes unless code will make specific efforts to catch that exception; use data members to achieve informational granularity, not additional exception classes. In addition, avoid deep class hierarchies when possible in favor of broad, shallow hierarchies.
The following example is a custom exception named CustomException
that extends Exception
and declares two custom data members: a string
named stringInfo
and a bool
named booleanInfo
.
using System; using System.Runtime.Serialization; namespace Apress.VisualCSharpRecipes.Chapter13 { // Mark CustomException as Serializable. [Serializable] public sealed class CustomException : Exception { // Custom data members for CustomException. private string stringInfo; private bool booleanInfo; // Three standard constructors and simply call the base class. // constructor (System.Exception). public CustomException() : base() { } public CustomException(string message) : base(message) { } public CustomException(string message, Exception inner) : base(message, inner) { } // The deserialization constructor required by the ISerialization // interface. Because CustomException is sealed, this constructor // is private. If CustomException were not sealed, this constructor // should be declared as protected so that derived classes can call // it during deserialization. private CustomException(SerializationInfo info, StreamingContext context) : base(info, context) { // Deserialize each custom data member. stringInfo = info.GetString("StringInfo"); booleanInfo = info.GetBoolean("BooleanInfo"); } // Additional constructors to allow code to set the custom data // members. public CustomException(string message, string stringInfo, bool booleanInfo) : this(message) { this.stringInfo = stringInfo; this.booleanInfo = booleanInfo; }
public CustomException(string message, Exception inner, string stringInfo, bool booleanInfo): this(message, inner) { this.stringInfo = stringInfo; this.booleanInfo = booleanInfo; } // Read-only properties that provide access to the custom data members. public string StringInfo { get { return stringInfo; } } public bool BooleanInfo { get { return booleanInfo; } } // The GetObjectData method (declared in the ISerializable interface) // is used during serialization of CustomException. Because // CustomException declares custom data members, it must override the // base class implementation of GetObjectData. public override void GetObjectData(SerializationInfo info, StreamingContext context) { // Serialize the custom data members. info.AddValue("StringInfo", stringInfo); info.AddValue("BooleanInfo", booleanInfo); // Call the base class to serialize its members. base.GetObjectData(info, context); } // Override the base class Message property to include the custom data // members. public override string Message { get { string message = base.Message; if (stringInfo != null) { message += Environment.NewLine + stringInfo + " = " + booleanInfo; } return message; } } }
// A class to demonstrate the use of CustomException. public class Recipe13_08 { public static void Main() { try { // Create and throw a CustomException object. throw new CustomException("Some error", "SomeCustomMessage", true); } catch (CustomException ex) { Console.WriteLine(ex.Message); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
Create a custom event argument class derived from the System.EventArg
class. When you raise the event, create an instance of your event argument class and pass it to the event handlers.
When you declare your own event types, you will often want to pass event-specific state to any listening event handlers. To create a custom event argument class that complies with the Event pattern defined by the .NET Framework, you should do the following:
Derive your custom event argument class from the EventArgs
class. The EventArgs
class contains no data and is used with events that do not need to pass event state.
Give your event argument class a meaningful name ending in EventArgs
, such as DiskFullEventArgs
or MailReceivedEventArgs
.
Mark your argument class as sealed
if you do not intend other event argument classes to extend it.
Implement additional data members and properties that you need to pass to event handlers to support event state. It's best to make event state immutable, so you should use private readonly
data members and public properties to provide read-only access to the data members.
Implement a public constructor that allows you to set the initial configuration of the event state.
Make your event argument class serializable so that the runtime can marshal instances of it across application domain and machine boundaries. Applying the attribute System.SerializableAttribute
is usually sufficient for event argument classes. However, if your class has special serialization requirements, you must also implement the interface System.Runtime.Serialization.ISerializable
. (See recipe 13-1 for details on making classes serializable.)
The following example demonstrates the implementation of an event argument class named MailReceivedEventArgs
. Theoretically, an e-mail server passes instances of the MailReceivedEventArgs
class to event handlers in response to the receipt of an e-mail message. The MailReceivedEventArgs
class contains information about the sender and subject of the received e-mail message.
using System; namespace Apress.VisualCSharpRecipes.Chapter13 { [Serializable] public sealed class MailReceivedEventArgs : EventArgs { // Private read-only members that hold the event state that is to be // distributed to all event handlers. The MailReceivedEventArgs class // will specify who sent the received mail and what the subject is. private readonly string from; private readonly string subject; // Constructor, initializes event state. public MailReceivedEventArgs(string from, string subject) { this.from = from; this.subject = subject; } // Read-only properties to provide access to event state. public string From { get { return from; } } public string Subject { get { return subject; } } }
// A class to demonstrate the use of MailReceivedEventArgs. public class Recipe13_09 { public static void Main() { MailReceivedEventArgs args = new MailReceivedEventArgs("Danielle", "Your book"); Console.WriteLine("From: {0}, Subject: {1}", args.From, args.Subject); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You need to ensure that only a single instance of a type exists at any given time and that the single instance is accessible to all elements of your application.
Of all the identified patterns, the Singleton pattern is perhaps the most widely known and commonly used. The purposes of the Singleton pattern are to ensure that only one instance of a type exists at a given time and to provide global access to the functionality of that single instance. You can implement the type using the Singleton pattern by doing the following:
Implement a private static member within the type to hold a reference to the single instance of the type.
Implement a publicly accessible static property in the type to provide read-only access to the singleton instance.
Implement only a private constructor so that code cannot create additional instances of the type.
The following example demonstrates an implementation of the Singleton pattern for a class named SingletonExample
:
using System; namespace Apress.VisualCSharpRecipes.Chapter13 { public class SingletonExample { // A static member to hold a reference to the singleton instance. private static SingletonExample instance; // A static constructor to create the singleton instance. Another // alternative is to use lazy initialization in the Instance property. static SingletonExample() { instance = new SingletonExample(); } // A private constructor to stop code from creating additional // instances of the singleton type. private SingletonExample() { } // A public property to provide access to the singleton instance. public static SingletonExample Instance { get { return instance; } } // Public methods that provide singleton functionality. public void SomeMethod1() { /*..*/ } public void SomeMethod2() { /*..*/ } } }
To invoke the functionality of the SingletonExample
class, you can obtain a reference to the singleton using the Instance
property and then call its methods. Alternatively, you can execute members of the singleton directly through the Instance
property. The following code shows both approaches.
// Obtain reference to singleton and invoke methods SingletonExample s = SingletonExample.Instance; s.SomeMethod1(); // Execute singleton functionality without a reference SingletonExample.Instance.SomeMethod2();
You need to implement an efficient mechanism for an object (the subject) to notify other objects (the observers) about changes to its state.
Implement the Observer pattern using delegate types as type-safe function pointers and event types to manage and notify the set of observers.
The traditional approach to implementing the Observer pattern is to implement two interfaces: one to represent an observer (IObserver
) and the other to represent the subject (ISubject
). Objects that implement IObserver
register with the subject, indicating that they want to be notified of important events (such as state changes) affecting the subject. The subject is responsible for managing the list of registered observers and notifying them in response to events affecting the subject. The subject usually notifies observers by calling a Notify
method declared in the IObserver
interface. The subject might pass data to the observer as part of the Notify
method, or the observer might need to call a method declared in the ISubject
interface to obtain additional details about the event.
Although you are free to implement the Observer pattern in C# using the approach just described, the Observer pattern is so pervasive in modern software solutions that C# and the .NET Framework include event and delegate types to simplify its implementation. The use of events and delegates means that you do not need to declare IObserver
and ISubject
interfaces. In addition, you do not need to implement the logic necessary to manage and notify the set of registered observers—the area where most coding errors occur.
The .NET Framework uses one particular implementation of the event-based and delegate-based Observer pattern so frequently that it has been given its own name: the Event pattern. (Pattern purists might prefer the name Event idiom, but Event pattern is the name most commonly used in Microsoft documentation.)
The example for this recipe contains a complete implementation of the Event pattern, which includes the following types:
Thermostat
class (the subject of the example), which keeps track of the current temperature and notifies observers when a temperature change occurs
TemperatureChangeEventArgs
class, which is a custom implementation of the System.EventArgs
class used to encapsulate temperature change data for distribution during the notification of observers
TemperatureEventHandler
delegate, which defines the signature of the method that all observers of a Thermostat
object must implement and that a Thermostat
object will call in the event of temperature changes
TemperatureChangeObserver
and TemperatureAverageObserver
classes, which are observers of the Thermostat
class
The TemperatureChangeEventArgs
class (in the following listing) derives from the class System.EventArgs
. The custom event argument class should contain all of the data that the subject needs to pass to its observers when it notifies them of an event. If you do not need to pass data with your event notifications, you do not need to define a new argument class; simply pass EventArgs.Empty
or null
as the argument when you raise the event. (See recipe 13-9 for details on implementing custom event argument classes.)
namespace Apress.VisualCSharpRecipes.Chapter13 { // An event argument class that contains information about a temperature // change event. An instance of this class is passed with every event. public class TemperatureChangedEventArgs : EventArgs { // Private data members contain the old and new temperature readings. private readonly int oldTemperature, newTemperature; // Constructor that takes the old and new temperature values. public TemperatureChangedEventArgs(int oldTemp, int newTemp) { oldTemperature = oldTemp; newTemperature = newTemp; } // Read-only properties provide access to the temperature values. public int OldTemperature { get { return oldTemperature; } } public int NewTemperature { get { return newTemperature; } } } }
The following code shows the declaration of the TemperatureEventHandler
delegate. Based on this declaration, all observers must implement a method (the name is unimportant) that returns void
and takes two arguments: an Object
instance as the first argument and a TemperatureChangeEventArgs
object as the second. During notification, the Object
argument is a reference to the Thermostat
object that raises the event, and the TemperatureChangeEventArgs
argument contains data about the old and new temperature values.
namespace Apress.VisualCSharpRecipes.Chapter13 { // A delegate that specifies the signature that all temperature event // handler methods must implement. public delegate void TemperatureChangedEventHandler(Object sender, TemperatureChangedEventArgs args); }
For the purpose of demonstrating the Observer pattern, the example contains two different observer types: TemperatureAverageObserver
and TemperatureChangeObserver
. Both classes have the same basic implementation. TemperatureAverageObserver
keeps a count of the number of temperature change events and the sum of the temperature values, and displays an average temperature when each event occurs. TemperatureChangeObserver
displays information about the change in temperature each time a temperature change event occurs.
The following listing shows the TemperatureChangeObserver
and TemperatureAverageObserver
classes. Notice that the constructors take references to the Thermostat
object that the TemperatureChangeObserver
or TemperatureAverageObserver
object should observe. When you instantiate an observer, pass it a reference to the subject. The observer must create a delegate instance containing a reference to the observer's event-handler method. To register as an observer, the observer object must then add its delegate instance to the subject using the subject's public event member. This is made even easier with the simplified delegate syntax provided by C#, where it is no longer required to explicitly instantiate a delegate to wrap the listening method.
Once the TemperatureChangeObserver
or TemperatureAverageObserver
object has registered its delegate instance with the Thermostat
object, you need to maintain a reference to this Thermostat
object only if you want to stop observing it later on. In addition, you do not need to maintain a reference to the subject, because a reference to the event source is included as the first argument each time the Thermostat
object raises an event through the TemperatureChange
method.
namespace Apress.VisualCSharpRecipes.Chapter13 { // A Thermostat observer that displays information about the change in // temperature when a temperature change event occurs. public class TemperatureChangeObserver { // A constructor that takes a reference to the Thermostat object that // the TemperatureChangeObserver object should observe. public TemperatureChangeObserver(Thermostat t) { // Create a new TemperatureChangedEventHandler delegate instance and // register it with the specified Thermostat. t.TemperatureChanged += this.TemperatureChange; } // The method to handle temperature change events. public void TemperatureChange(Object sender, TemperatureChangedEventArgs temp) { Console.WriteLine ("ChangeObserver: Old={0}, New={1}, Change={2}", temp.OldTemperature, temp.NewTemperature, temp.NewTemperature - temp.OldTemperature); } } // A Thermostat observer that displays information about the average // temperature when a temperature change event occurs. public class TemperatureAverageObserver {
// Sum contains the running total of temperature readings. // Count contains the number of temperature events received. private int sum = 0, count = 0; // A constructor that takes a reference to the Thermostat object that // the TemperatureAverageObserver object should observe. public TemperatureAverageObserver(Thermostat t) { // Create a new TemperatureChangedEventHandler delegate instance and // register it with the specified Thermostat. t.TemperatureChanged += this.TemperatureChange; } // The method to handle temperature change events. public void TemperatureChange(Object sender, TemperatureChangedEventArgs temp) { count++; sum += temp.NewTemperature; Console.WriteLine ("AverageObserver: Average={0:F}", (double)sum / (double)count); } } }
Finally, the Thermostat
class is the observed object in this Observer (Event) pattern. In theory, a monitoring device sets the current temperature by calling the Temperature
property on a Thermostat
object. This causes the Thermostat
object to raise its TemperatureChange
event and send a TemperatureChangeEventArgs
object to each observer.
The example contains a Recipe13_11
class that defines a Main
method to drive the example. After creating a Thermostat
object and two different observer objects, the Main
method repeatedly prompts you to enter a temperature. Each time you enter a new temperature, the Thermostat
object notifies the listeners, which display information to the console. The following is the code for the Thermostat
class:
namespace Apress.VisualCSharpRecipes.Chapter13 { // A class that represents a Thermostat, which is the source of temperature // change events. In the Observer pattern, a Thermostat object is the // subject that Observers listen to for change notifications. public class Thermostat { // Private field to hold current temperature. private int temperature = 0; // The event used to maintain a list of observer delegates and raise // a temperature change event when a temperature change occurs. public event TemperatureChangedEventHandler TemperatureChanged; // A protected method used to raise the TemperatureChanged event. // Because events can be triggered only from within the containing
// type, using a protected method to raise the event allows derived // classes to provide customized behavior and still be able to raise // the base class event. virtual protected void OnTemperatureChanged (TemperatureChangedEventArgs args) { // Notify all observers. A test for null indicates whether any // observers are registered. if (TemperatureChanged != null) { TemperatureChanged(this, args); } } // Public property to get and set the current temperature. The "set" // side of the property is responsible for raising the temperature // change event to notify all observers of a change in temperature. public int Temperature { get { return temperature; } set { // Create a new event argument object containing the old and // new temperatures. TemperatureChangedEventArgs args = new TemperatureChangedEventArgs(temperature, value); // Update the current temperature. temperature = value; // Raise the temperature change event. OnTemperatureChanged(args); } } } // A class to demonstrate the use of the Observer pattern. public class Recipe13_11 { public static void Main() { // Create a Thermostat instance. Thermostat t = new Thermostat(); // Create the Thermostat observers. new TemperatureChangeObserver(t); new TemperatureAverageObserver(t);
// Loop, getting temperature readings from the user. // Any noninteger value will terminate the loop. do { Console.WriteLine(Environment.NewLine); Console.Write("Enter current temperature: "); try { // Convert the user's input to an integer and use it to set // the current temperature of the Thermostat. t.Temperature = Int32.Parse(Console.ReadLine()); } catch (Exception) { // Use the exception condition to trigger termination. Console.WriteLine("Terminating Observer Pattern Example."); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); return; } } while (true); } } }
The following listing shows the kind of output you should expect if you build and run the previous example. The bold values show your input.
Enter current temperature: 50 ChangeObserver: Old=0, New=50, Change=50 AverageObserver: Average=50.00 Enter current temperature: 20 ChangeObserver: Old=50, New=20, Change=-30 AverageObserver: Average=35.00
Enter current temperature: 40 ChangeObserver: Old=20, New=40, Change=20 AverageObserver: Average=36.67
You need to coordinate several threads using a collection, such that one or more producer threads places items into the collection as one or more consumer threads removes items from it.
The BlockingCollection
is a wrapper class that provides the foundation for the parallel producer-consumer pattern. Consumer threads are blocked when trying to take data from the collection until there are data items available. Optionally, producer threads are blocked when trying to add data to the collection if there are too many items already in the collection.
BlockingCollection
wraps around collections classes that implement the System.Collections.Concurrent.IProducerConsumerCollection
interface—this includes the ConcurrentQueue, ConcurrentStack
, and ConcurrentBag
collections in the System.Collections.Concurrent
namespace.
To create a new instance of BlockingCollection
, pass in an instance of the collection that you want to wrap and, if required, the maximum number of items you wish to be in the collection before producers will block when adding. For example, the following statement creates a new instance wrapped around a ConcurrentQueue
with a size limit of three pending items:
new BlockingCollection<string>(new ConcurrentStack<string>(), 3);
The default constructor for BlockingCollection
(which takes no arguments) uses the ConcurrentQueue
class as the underlying collection, and uses no size limit—meaning that items will be taken out of the collection in the same order in which they were added, and also that producer threads will not block when adding items, irrespective of how many items are in the collection.
There are two ways for consumers to take items out of the collection. If you have one consumer thread, then the simplest way is to call the GetConsumingEnumerable
method and use the resulting IEnumerable
in a foreach
loop. The loop will block when there are no items in the collection to be consumed. If you have multiple consumers, then they should call the Take
method, which will return an item if one is available in the collection or block until such time as one becomes available.
If you don't want your producers and consumers to block, you can use the BlockingCollection.TryAdd
and BlockingCollection.TryTake
methods. These methods won't block, regardless of the state of the collection, and they return a bool
to indicate whether the add or take operations succeeded.
When using this pattern, there often comes a point when your producers have added all of the items that you require and their tasks or threads have completed. However, your consumers will still be blocking because they continue to wait for new items to arrive in the collection. To avoid this situation, you should call the BlockingCollection.CompleteAdding
instance method, which stops the methods the consumers are using from blocking—see the following code example for an illustration of this.
The following example creates a BlockingCollection
using a ConcurrentQueue
as the underlying collection. Using the .NET parallel programming features (see Chapter 15 for further information about parallel programming), a single consumer reads items from the collection while four producers add items. The main application thread waits for the producers to add their items and finish, before calling CompleteAdding
on the collection. This causes the consumer's foreach
method to stop blocking when all of the items are read from the collection.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Collections.Concurrent; using System.Threading.Tasks; namespace Apress.VisualCSharpRecipes.Chapter13 { class Recipe13_12 { static void Main(string[] args) { // Create the collection. BlockingCollection<string> bCollection = new BlockingCollection<string>(new ConcurrentQueue<string>(), 3); // Start the consumer. Task consumerTask = Task.Factory.StartNew( () => performConsumerTasks(bCollection)); // Start the producers. Task producer0 = Task.Factory.StartNew( () => performProducerTasks(bCollection, 0)); Task producer1 = Task.Factory.StartNew( () => performProducerTasks(bCollection, 1)); Task producer2 = Task.Factory.StartNew( () => performProducerTasks(bCollection, 2)); Task producer3 = Task.Factory.StartNew( () => performProducerTasks(bCollection, 3)); // Wait for the producers to complete. Task.WaitAll(producer0, producer1, producer2, producer3); Console.WriteLine("Producer tasks complete.");
// Indicate that we will not add anything further. bCollection.CompleteAdding(); // Wait for the consumer task to complete. consumerTask.Wait(); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } static void performConsumerTasks(BlockingCollection<string> collection) { Console.WriteLine("Consumer started"); foreach (string collData in collection.GetConsumingEnumerable()) { // Write out the data. Console.WriteLine("Consumer got message {0}", collData); } Console.WriteLine("Consumer completed"); } static void performProducerTasks(BlockingCollection<string> collection, int taskID) { Console.WriteLine("Producer started"); for (int i = 0; i < 100; i++) { // Put something into the collection. collection.Add("TaskID " + taskID + " Message" + i++); Console.WriteLine("Task {0} wrote message {1}", taskID, i); } } } }
Use the System.Lazy
class to wrap the creation of your data type and call the Lazy.Value
instance method to access the type instance—the type will not be initialized until Lazy.Value
is first called.
The .NET Framework performs eager initialization by default, which means that types are initialized as soon as they are created. By contrast, lazy initialization lets you defer object initialization until you need to access one of the members of your type. Eager initialization tends to create applications that create lots of objects when they start, even though the objects themselves may not be used for some time—this can consume resources unnecessarily and slow down your application, at least until all of the objects are created. To use lazy initialization, you simply pass your normal object instantiation as a delegate argument to the constructor of the System.Lazy
class, so that
MyDataType mytype = new MydataType();
becomes
Lazy<MyDataType> myType = new Lazy<MyDataType<(() => new MyDataType());
In order to access the type within the Lazy
instance, you call the Value
property—the first time that Value
is called, the type will be initialized—in this way, you can defer initializing your object until you need to use it.
The following example defines a type called MyDataType
, which has a constructor and a method called sayHello
. The Main
method called when the application starts creates an instance of MyDataType
using eager initialization, prints out a message simulating performing other tasks, and then calls sayHello
. This process is then repeated using lazy initialization. The result is that the constructor is called as soon as the object reference is created using eager initialization, whereas the constructor is not called until the sayHello
method is invoked when using lazy initialization.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter13 { class Recipe13_13 { static void Main(string[] args) { // Create an instance using eager initialization. MyDataType eagerInstance = new MyDataType(false); Console.WriteLine("...do other things..."); eagerInstance.sayHello(); Lazy<MyDataType> lazyInstance = new Lazy<MyDataType>(() => new MyDataType(true)); Console.WriteLine("...do other things..."); lazyInstance.Value.sayHello();
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } class MyDataType { public MyDataType(bool lazy) { Console.WriteLine("Initializing MyDataType - lazy instance: {0}", lazy); } public void sayHello() { Console.WriteLine("MyDataType Says Hello"); } } }
Supply a default value for the parameters you wish to make optional when defining your method.
The optional parameters feature allows you to simplify a common programming pattern, where several slightly different methods exist to allow a caller to use default values, such as the following:
void printMessage() { printMessage("Adam"); } void printMessage(string from) { printMessage(from, "Hello"); }
void printMessage(string from, string message) { printMessage(from, message, false); } void printMessage(string from, string message, bool urgent) { // Do something. }
This approach allows callers of the method to rely on default values—this helps to simplify the code of the calling classes. C# supports optional parameters so that you can achieve the same effect with only one method in your class—you do this by setting the default values when defining the parameters—for example:
void printMessage(string from = "Adam", string message = "Hello", bool urgent = false)
Optional parameters must be defined after normal parameters.
The following example defines a method with three optional parameters:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter13 { class Recipe13_14 { static void Main(string[] args) { printMessage(); printMessage("Allen"); printMessage("Bob", "Goodbye"); printMessage("Joe", "Help", true); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } static void printMessage(string from = "Adam", string message = "Hello", bool urgent = false) {
Console.WriteLine("From: {0}, Urgent: {1}, Message: {2}", from, message, urgent); } } }
You want to add a method to a type without modifying it, most likely because you didn't write the type you want to modify.
Extension types allow you to extend a type by providing new methods in a separate class file and associating them with the type you wish to apply them to. The main need for this C# feature is when you want to associate new features with a type that you didn't write—one from the .NET Framework class library, for example. To create an extension method, start by creating a static class—a static class has the keyword static
before class
in the declaration. A static class is like a regular class, except the class cannot be instantiated and all of the methods must be static.
Add a static method to the static class with the name and result type you require. The first parameter of the method must be of the type that you wish to extend and be prefaced with the word this
.
Implement the method body, performing the tasks you require. The parameter you prefaced with this
represents the instance of the type you have extended on which your method has been invoked. For example, suppose we define an extension for string
like this:
public static int countVowels(this string str)
The str
parameter is the string
instance that the extension has been invoked to process. To use an extension method, you must ensure that the namespace in which you created the static class is available to the calling class with the using
keyword, just as you would for any other namespace. Then you simply call the extension method as though it is an instance method of the type you have extended—for example:
String mystring = "Hello"; Mystring.countVowels();
Note that you don't need to provide the first argument you declared for your extension method (the one prefaced with this
).
The following example defines two extension methods for the string
type in the namespace Apress.VisualCSharpRecipes.Chapter13.Extensions
:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Apress.VisualCSharpRecipes.Chapter13.Extensions; namespace Apress.VisualCSharpRecipes.Chapter13.Extensions { public static class MyStringExtentions { public static string toMixedCase(this String str) { StringBuilder builder = new StringBuilder(str.Length); for (int i = 0; i < str.Length; i += 2) { builder.Append(str.ToLower()[i]); builder.Append(str.ToUpper()[i + 1]); } return builder.ToString(); } public static int countVowels(this String str) { char[] vowels = { 'a', 'e', 'i', 'o', 'u' }; int vowelcount = 0; foreach (char c in str) { if (vowels.Contains(c)) { vowelcount++; } } return vowelcount; } } } namespace Apress.VisualCSharpRecipes.Chapter13 { class Recipe13_15 { static void Main(string[] args) { string str = "The quick brown fox jumped over the..."; Console.WriteLine(str.toMixedCase()); Console.WriteLine("There are {0} vowels", str.countVowels());
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
Usually, the C# compiler checks to see that calls you make to type members are valid—that they exist, that they are accessible to the type you are calling from, that you have supplied the right number of arguments, that the arguments are of the right type, and so on.
C# also supports dynamic calls to type members in which these checks are not performed until the program is running and the call needs to be made. In order to take advantage of this feature, you declare an instance of dynamic
—for example:
dynamic myobject = new MyObject();
You can then use the object reference you have created as you would normally—however, the calls you make against the dynamic instance are not checked by the compiler, and errors will not be detected until the calls are executed at runtime.
The following example defines a type. The program Main
method creates a dynamic instance of that type and calls countVowels
, which exists, and thisMethodDoesNotExist
, which does not.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Dynamic;
namespace Apress.VisualCSharpRecipes.Chapter13 { class myType { public myType(string strval) { str = strval; } public string str {get; set;} public int countVowels() { char[] vowels = { 'a', 'e', 'i', 'o', 'u' }; int vowelcount = 0; foreach (char c in str) { if (vowels.Contains(c)) { vowelcount++; } } return vowelcount; } } class Recipe13_16 { static void Main(string[] args) { // create a dynamic type dynamic dynInstance = new myType("The quick brown fox jumped over the..."); // call the countVowels method int vowels = dynInstance.countVowels(); Console.WriteLine("There are {0} vowels", vowels); // call a method that does not exist dynInstance.thisMethodDoesNotExist(); } } }
The code compiles, even though we have called a method that does not exist. When we execute the program, we get the following output:
There are 10 vowels Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'Apress.VisualCSharpRecipes.Chapter13.myType ' does not contain a definition for 'thisMethodDoesNotExist' at CallSite.Target(Closure , CallSite , Object ) at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid1[T0]( CallSite site, T0 arg0) at Apress.VisualCSharpRecipes.Chapter13.Recipe13_16.Main(String[] args) in C:UsersAdamDocumentsWorkC# CookbookChapter13Recipe13-16 Recipe13-16.cs:line 44 Press any key to continue . . .
Generic types allow you to provide strict controls on types that collections and classes will accept, but can create some unexpected behaviors. For example, suppose we define two classes, one of which derives from the other, and a generic interface, as follows:
class BaseType { } class DerivedType : BaseType { } public interface IMyInterface<T> { T getValue(); }
The following program illustrates the problem—the compiler won't allow us to treat a IMyInterface<DerivedType>
as a IMyInterface<BaseType>
so that we can call the processData
method:
class Recipe13_17 { static void Main(string[] args) { IMyInterface<DerivedType> variant = // implementation class //; processData(variant); } static void processData(IMyInterface<BaseType> data) { ...do something... } }
Covariance allows you to change this behavior when there is no possibility of breaking type safety. You use covariance by applying the out
keyword to your interface definition, such as
public interface IMyInterface<
out T>
Now the preceding code will work. Covariance can only be used for interfaces that only contain methods that return the generic type—if you define a method that accepts the type as a parameter, the compiler will generate an error. Contravariance is the complement to covariance—in order to handle parameters, you must use the in
keyword, either in a separate interface or as a different type in the same interface—for example:
public interface IMyInterface<out T1, in T2 >
The generic interfaces in the .NET Framework class library have been updated in .NET 4.0 to use variance so that you can perform safe conversion using those types.
The following example is similar to the fragments shown preceding, and contains two types (one derived from the other) and a covariant generic interface. In the Main
method, we create an implementation of the interface that is typed using the derived class, and then call a method that requires an implementation of the interface that is typed using the base class.
using System; namespace Apress.VisualCSharpRecipes.Chapter13 { class Recipe13_17 { static void Main(string[] args) { // Create an implementation of the interface that is typed // (and contains) the derived type. IMyInterface<DerivedType> variant = new ImplClass<DerivedType>(new DerivedType()); // Call a method that accepts an instance of the interface typed. // with the base type - if the interface has been // defined with the out keyword // This will work; otherwise, the compiler will report an error.m processData(variant); } static void processData(IMyInterface<BaseType> data) { data.getValue().printMessage(); } } class BaseType { public virtual void printMessage() { Console.WriteLine("BaseType Message"); } } class DerivedType : BaseType { public override void printMessage() { Console.WriteLine("DerivedType Message"); } }
public interface IMyInterface<out T> { T getValue(); } public class ImplClass<T> : IMyInterface<T> { private T value; public ImplClass(T val) { value = val; } public T getValue() { return value; } } }