This chapter explores the System namespace. System is a top-level namespace of the .NET Framework class library. It directly contains those classes, structures, interfaces, delegates, and enumerations that are most commonly used by a C# program or that are deemed otherwise integral to the .NET Framework. Thus, System defines the core of the library.
System also contains many nested namespaces that support specific subsystems, such as System.Net. Several of these subsystems are described later in this book. This chapter is concerned only with the members of System, itself.
In addition to a large number of exception classes, System contains the following classes:
System defines the following structures:
System defines the following interfaces:
System defines the following delegates:
System defines these enumerations:
As the preceding tables show, System is quite large. It is not possible to examine all of its constituents in detail in a single chapter. Furthermore, several of System’s members, such as Nullable<T>, Type, Exception, and Attribute, are discussed in Part I. Finally, because System.String, which defines the C# string type, is such a large and important topic, it is covered in Chapter 22 along with formatting. For these reasons, this chapter explores only those members that are commonly used by a wide range of applications and that are not fully covered elsewhere.
Math defines several standard mathematical operations, such as square root, sine, cosine, and logarithms. The Math class is static, which means all of the methods defined by Math are static and no object of type Math can be constructed. It also means Math is implicitly sealed and cannot be inherited. The methods defined by Math are shown in Table 21-1. All angles are in radians.
Math also defines these two fields:
public const double E
public const double PI
E is the value of the natural logarithm base, commonly referred to as e. PI is the value of pi.
Here is an example that uses Sqrt( ) to help implement the Pythagorean theorem. It computes the length of the hypotenuse given the lengths of the two opposing sides of a right triangle.
// Implement the Pythagorean Theorem.
using System;
class Pythagorean {
static void Main() {
double s1;
double s2;
double hypot;
string str;
Console.WriteLine("Enter length of first side: ");
str = Console.ReadLine();
s1 = Double.Parse(str);
Console.WriteLine("Enter length of second side: ");
str = Console.ReadLine();
s2 = Double.Parse(str);
hypot = Math.Sqrt(s1*s1 + s2*s2);
Console.WriteLine("Hypotenuse is ” + hypot);
}
}
Here is a sample run:
Enter length of first side: 3
Enter length of second side: 4
Hypotenuse is 5
Next is an example that uses the Pow( ) method to compute the initial investment required to achieve a desired future value given the annual rate of return and the number of years. The formula to compute the initial investment is shown here:
InitialInvestment = FutureValue / (1 + InterestRate) Years
Because Pow( ) requires double arguments, the interest rate and the number of years are held in double values. The future value and initial investment use the decimal type.
/* Compute the initial investment needed to attain
a known future value given annual rate of return
and the time period in years. */
using System;
class InitialInvestment {
static void Main() {
decimal initInvest; // initial investment
decimal futVal; // future value
double numYears; // number of years
double intRate; // annual rate of return as a decimal
string str;
Console.Write("Enter future value:");
str = Console.ReadLine();
try {
futVal = Decimal.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message);
return;
}
Console.Write("Enter interest rate (such as 0.085):");
str = Console.ReadLine();
try {
intRate = Double.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message);
return;
}
Console.Write("Enter number of years:");
str = Console.ReadLine();
try {
numYears = Double.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message);
return;
}
initInvest = futVal / (decimal) Math.Pow(intRate+1.0, numYears);
Console.WriteLine("Initial investment required: {0:C}",
initInvest);
}
}
Here is a sample run:
Enter future value: 10000
Enter interest rate (such as 0.085): 0.07
Enter number of years: 10
Initial investment required: $5,083.49
The structures that correspond to C#’s built-in value types were introduced in Chapter 14 when they were used to convert strings holding human-readable numeric values into their equivalent binary values. Here these structures are examined in detail.
The .NET structure names and their C# keyword equivalents are shown in the following table:
By using the members defined by these structures, you can perform operations relating to the value types. The following sections examine each of these structures.
NOTE Some methods defined by the structures that correspond to the built-in value types take a parameter of type IFormatProvider or NumberStyles. IFormatProvider is briefly described later in this chapter. NumberStyles is an enumeration found in the System.Globalization namespace. The topic of formatting is discussed in Chapter 22.
The integer structures are
Each of these structures contains the same methods. The ones for Int32 are shown in Table 21-2 as an example, but the others are similar except for the integer type that they represent.
The integer structures also define the following const fields:
MaxValue
MinValue
For each structure, these fields contain the largest and smallest value that type of integer can hold.
All of the integer structures implement the following interfaces: IComparable, IComparable<T>, IConvertible, IFormattable, and IEquatable<T>, where T is replaced by the corresponding data type. For example, T will be replaced with int for Int32.
There are two floating-point structures: Double and Single. Single represents float. Its methods are shown in Table 21-3, and its fields are shown in Table 21-4. Double represents double. Its methods are shown in Table 21-5, and its fields are shown in Table 21-6. As is the case with the integer structures, you can specify culture-specific information and format information in a call to Parse( ) or ToString( ).
The floating-point structures implement the following interfaces: IComparable, IComparable<T>, IConvertible, IFormattable, and IEquatable<T>, where T is replaced by either double for Double or float for Single.
The Decimal structure is a bit more complicated than its integer and floating-point relatives. It contains many constructors, fields, methods, and operators that help integrate decimal with the other numeric types supported by C#. For example, several of the methods provide conversions between decimal and the other numeric types.
Decimal offers eight public constructors. The following six are the most commonly used:
public Decimal (int value)
public Decimal (uint value)
public Decimal (long value)
public Decimal (ulong value)
public Decimal (float value)
public Decimal (double value)
Each constructs a Decimal from the specified value.
You can also construct a Decimal by specifying its constituent parts using this constructor:
public Decimal (int lo, int mid, int hi, bool isNegative, byte scale)
A decimal value consists of three parts. The first is a 96-bit integer, the second is a sign flag, and the third is a scaling factor. The 96-bit integer is passed in 32-bit chunks through lo, mid, and hi. The sign is passed through isNegative, which is false for a positive number and true for a negative number. The scaling factor is passed in scale, which must be a value between 0 and 28. This factor specifies the power of 10 (that is, 10scale) by which the number is divided, thus yielding its fractional component.
Instead of passing each component separately, you can specify the constituents of a Decimal in an array of integers, using this constructor:
public Decimal (int[ ] bits)
The first three ints in bits contain the 96-bit integer value. In bits[3], bit 31 specifies the sign flag (0 for positive, 1 for negative), and bits 16 through 23 contain the scale factor.
Decimal implements the following interfaces: IComparable, IComparable<decimal>, IConvertible, IFormattable, IEquatable<decimal>, and IDeserializationCallback.
Here is an example that constructs a decimal value by hand:
// Manually create a decimal number.
using System;
class CreateDec {
static void Main() {
decimal d = new Decimal (12345, 0, 0, false, 2);
Console.WriteLine(d);
}
}
The output is shown here:
123.45
In this example, the value of the 96-bit integer is 12345. Its sign is positive, and it has two decimal fractions.
The methods defined by Decimal are shown in Table 21-7. The fields defined by Decimal are shown in Table 21-8. Decimal also defines a large number of operators and conversions that allow decimal values to be used in expressions with other numeric types. The rules governing the use of decimal in expressions and assignments are described in Chapter 3.
The structure corresponding to the char type is Char. It is quite useful because it supplies a large number of methods that allow you to process and categorize characters. For example, you can convert a lowercase character to uppercase by calling ToUpper( ). You can determine if a character is a digit by calling IsDigit( ).
The methods defined by Char are shown in Table 21-9. Notice that several, such as ConvertFromUtf32( ) and ConvertToUtf32( ), give you the ability to work with both UTF-16 and UTF-32 Unicode characters. In the past, all Unicode characters could be represented by 16 bits, which is the size of a char. However, a few years ago the Unicode character set was expanded and more than 16 bits are required. Each Unicode character is represented by a code point. The way that a code point is encoded depends on the Unicode Transformation Format (UTF) being used. In UTF-16, the most common code points require one 16-bit value, but some need two 16-bit values. When two 16-bit values are needed, two char values are used to represent it. The first character is called the high surrogate and the second is called the low surrogate. In UTF-32, each code point uses one 32-bit value. Char provides the necessary conversions between UTF-16 and UTF-32.
One other point about the Char methods. The default forms of ToUpper( ) and ToLower( ) use the current cultural settings to determine how to upper- or lowercase a character. At the time of this writing, the recommended style is to explicitly specify the cultural setting by using a second form of these methods that has a CultureInfo parameter. CultureInfo is in System.Globalization. You can pass the property CultureInfo.CurrentCulture to specify the current culture.
Char defines the following fields:
public const char MaxValue
public const char MinValue
These represent the largest and smallest values that a char variable can hold.
Char implements the following interfaces: IComparable, IComparable<char>, IConvertible, and IEquatable<char>.
Here is a program that demonstrates several of the methods defined by Char:
// Demonstrate several Char methods.
using System;
using System.Globalization;
class CharDemo {
static void Main() {
string str = "This is a test. $23";
int i;
for(i=0; i < str.Length; i++) {
Console.Write(str[i] + " is");
if(Char.IsDigit(str[i]))
Console.Write(" digit");
if(Char.IsLetter(str[i]))
Console.Write(" letter");
if(Char.IsLower(str[i]))
Console.Write(" lowercase");
if(Char.IsUpper(str[i]))
Console.Write(" uppercase");
if(Char.IsSymbol(str[i]))
Console.Write(" symbol");
if(Char.IsSeparator(str[i]))
Console.Write(" separator");
if(Char.IsWhiteSpace(str[i]))
Console.Write(" whitespace");
if(Char.IsPunctuation(str[i]))
Console.Write(" punctuation");
Console.WriteLine();
}
Console.WriteLine("Original: " + str);
// Convert to uppercase.
string newstr = "";
for(i=0; i < str.Length; i++)
newstr += Char.ToUpper(str[i], CultureInfo.CurrentCulture);
Console.WriteLine("Uppercased: " + newstr);
}
}
The output is shown here:
T is letter uppercase
h is letter lowercase
i is letter lowercase
s is letter lowercase
is separator whitespace
i is letter lowercase
s is letter lowercase
is separator whitespace
a is letter lowercase
is separator whitespace
t is letter lowercase
e is letter lowercase
s is letter lowercase
t is letter lowercase
. is punctuation
is separator whitespace
$ is symbol
2 is digit
3 is digit
Original: This is a test. $23
Uppercased: THIS IS A TEST. $23
The Boolean structure supports the bool data type. The methods defined by Boolean are shown in Table 21-10. It defines these fields:
public static readonly string FalseString
public static readonly string TrueString
These contain the human-readable forms of true and false. For example, if you output FalseString using a call to WriteLine( ), the string “False” is displayed.
Boolean implements the following interfaces: IComparable, IComparable<bool>, IConvertible, and IEquatable<bool>.
One very useful class in System is Array. Array is a base class for all arrays in C#. Thus, its methods can be applied to arrays of any of the built-in types or to arrays of types that you create. Array defines the properties shown in Table 21-11. It defines the methods shown in Table 21-12.
Array implements the following interfaces: ICloneable, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable, and IList. All but ICloneable are defined in the System.Collections namespace and are described in Chapter 24.
Several methods use a parameter of type IComparer or I Comparer<T>. The IComparer interface is in System.Collections. It defines a method called Compare( ), which compares the values of two objects. It is shown here:
int Compare(object x, object y)
It returns greater than zero if x is greater than y, less than zero if x is less than y, and zero if the two values are equal.
IComparer<T> is in System.Collections.Generic. It defines a generic form of Compare( ), which is shown here:
int Compare(T x, T y)
It works the same as its non-generic relative: returning greater than zero if x is greater than y, less than zero if x is less than y, and zero if the two values are equal. The advantage to IComparer<T> is type safety, because the type of data being operated upon is explicitly specified. Thus, no casts from object are required.
The next few sections demonstrate several commonly used array operations.
Often you will want to sort the contents of an array. To handle this, Array supports a rich complement of sorting methods. Using Sort( ), you can sort an entire array, a range within an array, or a pair of arrays that contain corresponding key/value pairs. Once an array has been sorted, you can efficiently search it using BinarySearch( ). Here is a program that demonstrates the Sort( ) and BinarySearch( ) methods by sorting an array of ints:
// Sort an array and search for a value.
using System;
class SortDemo {
static void Main() {
int[] nums = { 5, 4, 6, 3, 14, 9, 8, 17, 1, 24, -1, 0 };
// Display original order.
Console.Write("Original order:");
foreach(int i in nums)
Console.Write(i + "");
Console.WriteLine();
// Sort the array.
Array.Sort(nums);
// Display sorted order.
Console.Write("Sorted order: ");
foreach(int i in nums)
Console.Write(i + "");
Console.WriteLine();
// Search for 14.
int idx = Array.BinarySearch(nums, 14);
Console.WriteLine("Index of 14 is " + idx);
}
}
The output is shown here:
Original order: 5 4 6 3 14 9 8 17 1 24 -1 0
Sorted order: -1 0 1 3 4 5 6 8 9 14 17 24
Index of 14 is 9
In the preceding example, the array has an element type of int, which is a value type. All methods defined by Array are automatically available to all of the built-in value types. However, this may not be the case for arrays of object references. To sort or search an array of object references, the class type of those objects must implement either the IComparable or IComparable<T> interface. If the class does not implement one of these interfaces, a runtime exception will occur when attempting to sort or search the array. Fortunately, both IComparable and IComparable<T> are easy to implement.
IComparable defines just one method:
int CompareTo(object obj)
This method compares the invoking object against the value in obj. It returns greater than zero if the invoking object is greater than obj, zero if the two objects are equal, and less than zero if the invoking object is less than obj.
IComparable<T> is the generic version of IComparable. It defines the generic version of CompareTo( ):
int CompareTo(T other)
The generic version of CompareTo( ) works like the non-generic version. It compares the invoking object against the value in other. It returns greater than zero if the invoking object is greater than other, zero if the two objects are equal, and less than zero if the invoking object is less than other. The advantage of using IComparable<T> is type-safety because the type of data being operated upon is explicitly specified. There is no need to cast the object being compared from object into the desired type. Here is an example that illustrates sorting and searching an array of user-defined class objects:
// Sort and search an array of objects.
using System;
class MyClass : IComparable<MyClass> {
public int i;
public MyClass(int x) { i = x; }
// Implement IComparable<MyClass>.
public int CompareTo(MyClass v) {
return i - v.i;
}
}
class SortDemo {
static void Main() {
MyClass[] nums = new MyClass[5];
nums[0] = new MyClass(5);
nums[1] = new MyClass(2);
nums[2] = new MyClass(3);
nums[3] = new MyClass(4);
nums[4] = new MyClass(1);
// Display original order.
Console.Write("Original order: ");
foreach(MyClass o in nums)
Console.Write(o.i + "");
Console.WriteLine();
// Sort the array.
Array.Sort(nums);
// Display sorted order.
Console.Write("Sorted order: ");
foreach(MyClass o in nums)
Console.Write(o.i + "");
Console.WriteLine();
// Search for MyClass(2).
MyClass x = new MyClass(2);
int idx = Array.BinarySearch(nums, x);
Console.WriteLine("Index of MyClass(2) is " + idx);
}
}
The output is shown here:
Original order: 5 2 3 4 1
Sorted order: 1 2 3 4 5
Index of MyClass(2) is 1
When sorting or searching an array of strings, you may need to explicitly specify how those strings are compared. For example, if the array will be sorted using one cultural setting and searched under another, then explicitly specifying the comparison method may be necessary to avoid errors. Or, you might want to sort an array of strings using a cultural setting that is different than the current setting. To handle these (and other) types of situations, you can pass an instance of StringComparer to the IComparer parameter supported by several overloads of Sort( ) and BinarySearch( ).
NOTE See Chapter 22 for a discussion of issues related to string comparisons.
StringComparer is declared in System, and among other interfaces, it implements the IComparer and IComparer<T> interfaces. Thus, an instance of StringComparer can be passed to an IComparer parameter as an argument. StringComparer defines several readonly properties that return an instance of StringComparer and that support various types of string comparisons. They are shown here:
By explicitly passing a StringComparer, you unambiguously determine how sorting or searching will be accomplished. For example, the following sorts and searches an array of strings by using StringComparer.Ordinal:
string[] strs = { "xyz", "one" , "beta", "Alpha" };
// ...
Array.Sort(strs, StringComparer.Ordinal);
int idx = Array.BinarySearch(strs, "beta", StringComparer.Ordinal);
Sometimes it is useful to reverse the contents of an array. For example, you might want to change an array that has been sorted into ascending order into one sorted in descending order. Reversing an array is easy: Simply call Reverse( ). Using Reverse( ), you can reverse all or part of an array. The following program demonstrates the process:
// Reverse an array.
using System;
class ReverseDemo {
static void Main() {
int[] nums = { 1, 2, 3, 4, 5 };
// Display original order.
Console.Write("Original order:");
foreach(int i in nums)
Console.Write(i + "");
Console.WriteLine();
// Reverse the entire array.
Array.Reverse(nums);
// Display reversed order.
Console.Write("Reversed order:");
foreach(int i in nums)
Console.Write(i + "");
Console.WriteLine();
// Reverse a range.
Array.Reverse(nums, 1, 3);
// Display reversed order.
Console.Write("Range reversed:");
foreach(int i in nums)
Console.Write(i + "");
Console.WriteLine();
}
}
The output is shown here:
Original order: 1 2 3 4 5
Reversed order: 5 4 3 2 1
Range reversed: 5 2 3 4 1
Copying all or part of one array to another is another common array operation. To copy an array, use Copy( ). Copy( ) can put elements at the start of the destination array or in the middle, depending upon which version of Copy( ) you use. Copy( ) is demonstrated by the following program:
// Copy an array.
using System;
class CopyDemo {
static void Main() {
int[] source = { 1, 2, 3, 4, 5 };
int[] target = { 11, 12, 13, 14, 15 };
int[] source2 = { -1, -2, -3, -4, -5 };
// Display source.
Console.Write("source:");
foreach(int i in source)
Console.Write(i + "");
Console.WriteLine();
// Display original target.
Console.Write("Original contents of target:");
foreach(int i in target)
Console.Write(i + "");
Console.WriteLine();
// Copy the entire array.
Array.Copy(source, target, source.Length);
// Display copy.
Console.Write("target after copy: ");
foreach(int i in target)
Console.Write(i + "");
Console.WriteLine();
// Copy into middle of target.
Array.Copy(source2, 2, target, 3, 2);
// Display copy.
Console.Write("target after copy: ");
foreach(int i in target)
Console.Write(i + "");
Console.WriteLine();
}
}
The output is shown here:
source: 1 2 3 4 5
Original contents of target: 11 12 13 14 15
target after copy: 1 2 3 4 5
target after copy: 1 2 3 -3 -4
A predicate is a delegate of type System.Predicate that returns either true or false, based upon some condition. It is declared as shown here:
public delegate bool Predicate<in T> (T obj)
The object to be tested against the condition is passed in obj. If obj satisfies that condition, the predicate must return true. Otherwise, it must return false. Predicates are used by several methods in Array, including Exists( ), Find( ), FindIndex( ), and FindAll( ).
The following program demonstrates using a predicate to determine if an array of integers contains a negative value. If a negative value is found, the program then obtains the first negative value in the array. To accomplish this, the program uses Exists( ) and Find( ).
// Demonstrate Predicate delegate.
using System;
class PredDemo {
// A predicate method.
// It returns true if v is negative.
static bool IsNeg(int v) {
if(v < 0) return true;
return false;
}
static void Main() {
int[] nums = { 1, 4, -1, 5, -9 };
Console.Write("Contents of nums:");
foreach(int i in nums)
Console.Write(i + "");
Console.WriteLine();
// First see if nums contains a negative value.
if(Array.Exists(nums, PredDemo.IsNeg)) {
Console.WriteLine("nums contains a negative value.");
// Now, find first negative value.
int x = Array.Find(nums, PredDemo.IsNeg);
Console.WriteLine("First negative value is : " + x);
}
else
Console.WriteLine("nums contains no negative values.");
}
}
The output is shown here:
Contents of nums: 1 4 -1 5 -9
nums contains a negative value.
First negative value is : -1
In the program, the method passed to Exists( ) and Find( ) for the predicate is IsNeg( ). Notice that IsNeg( ) is declared like this:
static bool IsNeg(int v) {
The methods Exists( ) and Find( ) will automatically pass the elements of the array (in sequence) to v. Thus, each time IsNeg( ) is called, v will contain the next element in the array.
The Action delegate is used by Array.ForEach( ) to perform an action on each element of an array. There are various forms of Action, each taking a different number of type parameters. The one used here is
public delegate void Action<in T> (T obj)
The object to be acted upon is passed in obj. When used with ForEach( ), each element of the array is passed to obj in turn. Thus, through the use of ForEach( ) and Action, you can, in a single statement, perform an operation over an entire array.
The following program demonstrates both ForEach( ) and Action. It first creates an array of MyClass objects, and then uses the method Show( ) to display the values. Next, it uses Neg( ) to negate the values. Finally, it uses Show( ) again to display the negated values. These operations all occur through calls to ForEach( ).
// Demonstrate an Action.
using System;
class MyClass {
public int i;
public MyClass(int x) { i = x; }
}
class ActionDemo {
// An Action method.
// It displays the value it is passed.
static void Show(MyClass o) {
Console.Write(o.i + "");
}
// Another Action method.
// It negates the value it is passed.
static void Neg(MyClass o) {
o.i = -o.i;
}
static void Main() {
MyClass[] nums = new MyClass[5];
nums[0] = new MyClass(5);
nums[1] = new MyClass(2);
nums[2] = new MyClass(3);
nums[3] = new MyClass(4);
nums[4] = new MyClass(1);
Console.Write("Contents of nums:");
// Use action to show the values.
Array.ForEach(nums, ActionDemo.Show);
Console.WriteLine();
// Use action to negate the values.
Array.ForEach(nums, ActionDemo.Neg);
Console.Write("Contents of nums negated:");
// Use action to negate the values again.
Array.ForEach(nums, ActionDemo.Show);
Console.WriteLine();
}
}
Contents of nums: 5 2 3 4 1
Contents of nums negated: -5 -2 -3 -4 -1
In programming, one often needs to convert a built-in data type into an array of bytes. For example, some hardware device might require an integer value, but that value must be sent one byte at a time. The reverse situation also frequently occurs. Sometimes data will be received as an ordered sequence of bytes that needs to be converted into one of the built-in types. For example, a device might output integers, sent as a stream of bytes. Whatever your conversion needs, .NET provides the BitConverter class to meet them.
BitConverter is a static class. It contains the methods shown in Table 21-13. It defines the following field:
public static readonly bool IsLittleEndian
This field is true if the current environment stores a word with the least significant byte first and the most significant byte last. This is called “little-endian” format. IsLittleEndian is false if the current environment stores a word with the most significant byte first and the least significant byte last. This is called “big-endian” format. Intel Pentium–based machines use little-endian format.
To generate a sequence of pseudorandom numbers, you will use the Random class. Sequences of random numbers are useful in a variety of situations, including simulations and modeling. The starting point of the sequence is determined by a seed value, which can be automatically provided by Random or explicitly specified.
Random defines these two constructors:
public Random( )
public Random(int Seed)
The first version creates a Random object that uses the system time to compute the seed value. The second uses the value of Seed as the seed value.
Random defines the methods shown in Table 21-14.
Here is a program that demonstrates Random by creating a pair of computerized dice:
// An automated pair of dice.
using System;
class RandDice {
static void Main() {
Random ran = new Random();
Console.Write(ran.Next(1, 7) + "");
Console.WriteLine(ran.Next(1, 7));
}
}
Here are three sample runs:
5 2
4 4
1 6
The program works by first creating a Random object. Then it requests the two random values, each between 1 and 6.
The GC class encapsulates the garbage-collection facility. The methods defined by GC are shown in Table 21-15. It defines the read-only property shown here:
public static int MaxGeneration { get; }
MaxGeneration contains the maximum generation number available to the system. A generation number indicates the age of an allocation. Newer allocations have a lower number than older ones. Generation numbers help improve the efficiency of the garbage collector.
For most applications, you will not use any of the capabilities of GC. However, in specialized cases, they can be very useful. For example, you might want to use Collect( ) to force garbage collection to occur at a time of your choosing. Normally, garbage collection occurs at times unspecified by your program. Since garbage collection takes time, you might not want it to occur during some time-critical task, or you might want to take advantage of idle time to perform garbage collection and other types of “housekeeping” chores. You can also register for notifications about the approach and completion of garbage collection.
There are two methods that are especially important if you have unmanaged code in your project. AddMemoryPressure( ) and RemoveMemoryPressure( ). These are used to indicate that a large amount of unmanaged memory has been allocated or released by the program. They are important because the memory management system has no oversight on unmanaged memory. If a program allocates a large amount of unmanaged memory, then performance might be affected because the system has no way of knowing that free memory has been reduced. By calling AddMemoryPressure( ) when allocating large amounts of unmanaged memory, you let the CLR know that memory has been reduced. By calling RemoveMemoryPressure( ), you let the CLR know the memory has been freed. Remember: RemoveMemoryPressure( ) must be called only to indicate that memory reported by a call to AddMemoryPressure( ) has been released.
Object is the class that underlies the C# object type. The members of Object were discussed in Chapter 11, but because of its central role in C#, its methods are repeated in Table 21-16 for your convenience. Object defines one constructor, which is shown here:
public Object( )
It constructs an empty object.
.NET Framework 4.0 adds a convenient way to create groups (tuples) of objects. At the core is the static class Tuple, which defines several Create( ) methods that create tuples, and various Tuple<...> classes that encapsulate tuples. For example, here is a version of Create( ) that returns a tuple with three members:
public static Tuple<T1, T2, T3>
Create<T1, T2, T3>(T1 item1, T2 item2, T3 item3)
Notice that the method returns a Tuple<T1, T2, T3> object. This object encapsulates item1, item2, and item3. In general, tuples are useful whenever you want to treat a group of values as a unit. For example, you might pass a tuple to a method, return a tuple from a method, or store tuples in a collection or array.
Many classes will need to implement either the IComparable or IComparable<T> interface because they enable one object to be compared to another (for the purpose of ordering) by various methods defined by the .NET Framework. Chapter 18 introduced the IComparable and IComparable<T> interfaces, where they were used to enable two objects of a generic type parameter to be compared. They were also mentioned in the discussion of Array, earlier in this chapter. However, because of their importance and applicability to many situations, they are formally examined here.
IComparable is especially easy to implement because it consists of just this one method:
int CompareTo(object obj)
This method compares the invoking object against the value in obj. It returns greater than zero if the invoking object is greater than obj, zero if the two objects are equal, and less than zero if the invoking object is less than obj.
The generic version of IComparable is declared like this:
public interface IComparable<in T>
In this version, the type of data being compared is passed as a type argument to T. This causes the declaration of CompareTo( ) to be changed, as shown next:
int CompareTo(T other)
Here, the type of data that CompareTo( ) operates on can be explicitly specified. This makes IComparable<T> type-safe. For this reason, IComparable<T> is now preferable to IComparable.
IEquatable<T> is implemented by those classes that need to define how two objects should be compared for equality. It defines only one method, Equals( ), which is shown here:
bool Equals(T other)
The method returns true if other is equal to the invoking object and false otherwise.
IEquatable<T> is implemented by several classes and structures in the .NET Framework, including the numeric structures and the String class. When implementing IEquatable<T>, you will usually also need to override Equals(Object) and GetHashCode( ) defined by Object.
The IConvertible interface is implemented by all of the value-type structures, string, and DateTime. It specifies various type conversions. Normally, classes that you create will not need to implement this interface.
By implementing the ICloneable interface, you enable a copy of an object to be made. ICloneable defines only one method, Clone( ), which is shown here:
object Clone( )
This method makes a copy of the invoking object. How you implement Clone( ) determines how the copy is made. In general, there are two types of copies: deep and shallow. When a deep copy is made, the copy and original are completely independent. Thus, if the original object contained a reference to another object O, then a copy of O will also be made. In a shallow copy, members are copied, but objects referred to by members are not. If an object refers to some other object O, then after a shallow copy, both the copy and the original will refer to the same O, and any changes to O affect both the copy and the original. Usually, you will implement Clone( ) so that it performs a deep copy. Shallow copies can be made by using MemberwiseClone( ), which is defined by Object.
Here is an example that illustrates ICloneable. It creates a class called Test that contains a reference to an object of a class called X. Test uses Clone( ) to create a deep copy.
// Demonstrate ICloneable.
using System;
class X {
public int a;
public X(int x) { a = x; }
}
class Test : ICloneable {
public X o;
public int b;
public Test(int x, int y) {
o = new X(x);
b = y;
}
public void Show(string name) {
Console.Write(name + "values are");
Console.WriteLine("o.a: {0}, b: {1}", o.a, b);
}
// Make a deep copy of the invoking object.
public object Clone() {
Test temp = new Test(o.a, b);
return temp;
}
}
class CloneDemo {
static void Main() {
Test ob1 = new Test(10, 20);
ob1.Show("ob1");
Console.WriteLine("Make ob2 a clone of ob1.");
Test ob2 = (Test) ob1.Clone();
ob2.Show("ob2");
Console.WriteLine("Changing ob1.o.a to 99 and ob1.b to 88.");
ob1.o.a = 99;
ob1.b = 88;
ob1.Show("ob1");
ob2.Show("ob2");
}
}
The output is shown here:
ob1 values are o.a: 10, b: 20
Make ob2 a clone of ob1.
ob2 values are o.a: 10, b: 20
Changing ob1.o.a to 99 and ob1.b to 88.
ob1 values are o.a: 99, b: 88
ob2 values are o.a: 10, b: 20
As the output shows, ob2 is a clone of ob1, but ob1 and ob2 are completely separate objects. Changing one does not affect the other. This is accomplished by constructing a new Test object, which allocates a new X object for the copy. The new X instance is given the same value as the X object in the original.
To implement a shallow copy, simply have Clone( ) call MemberwiseClone( ) defined by Object. For example, try changing Clone( ) in the preceding program as shown here:
// Make a shallow copy of the invoking object.
public object Clone() {
Test temp = (Test) MemberwiseClone();
return temp;
}
After making this change, the output of the program will look like this:
ob1 values are o.a: 10, b: 20
Make ob2 a clone of ob1.
ob2 values are o.a: 10, b: 20
Changing ob1.o.a to 99 and ob1.b to 88.
ob1 values are o.a: 99, b: 88
ob2 values are o.a: 99, b: 20
Notice that o in ob1 and o in ob2 both refer to the same X object. Changing one affects both. Of course, the int field b in each is still separate because the value types are not accessed via references.
The IFormatProvider interface defines one method called GetFormat( ), which returns an object that controls the formatting of data into a human-readable string. The general form of GetFormat( ) is shown here:
object GetFormat(Type formatType)
Here, formatType specifies the format object to obtain.
The IFormattable interface supports the formatting of human-readable output. IFormattable defines this method:
string ToString(string format, IFormatProvider formatProvider)
Here, format specifies formatting instructions and formatProvider specifies the format provider.
NOTE Formatting is described in detail in Chapter 22.
.NET Framework 4.0 adds two interfaces that support the observer pattern. These are IObservable<T> and IObserver<T>. In the observer pattern, one class (the observable) provides notifications to another (the observer). This is accomplished by registering an object of the observing class with an object of the observable class. An observer is registered by calling Subscribe( ), which is specified by IObservable<T>, passing in the IObserver<T> object that will receive notification. More than one observer can be registered to receive notifications. To send notifications to all registered observers, three methods defined by IObserver<T> are used. OnNext( ) sends data to the observer, OnError( ) indicates an error, and OnCompleted( ) indicates the observable object has stopped sending notifications.