C# Objects

Everything in C# is an Object.

Note

One exception to this generalization does exist. In an unsafe context, pointer types are not objects because they do not derive from System.Object.


Because everything is an Object, everything has four base methods:

  • Equals—Method to determine if two instances are equal

  • GetHashCode—Method to uniquely identify an Object

  • GetType—Method for obtaining the type of the Object

  • ToString—Method for converting the Object to a string

Would you expect the following to compile?

Console.WriteLine("{0}  {1} ", 1.ToString(), 1.GetType());

It not only compiles, but it also outputs this:

1 System.Int32

The following

Console.WriteLine("{0}  {1} ", 1.23.ToString(), 1.23.GetType());

outputs this

1.23 System.Double

To really prove the point, you could try the following:

Console.WriteLine("{0}  {1}  {2}  {3} ",
                  1.23.ToString(),
                  1.23.GetType(),
                  1.23.GetType().BaseType,
                  1.23.GetType().BaseType.BaseType);

This outputs the following:

1.23 System.Double System.ValueType System.Object

Just a few layers down, even a basic floating point value is an object. Two types of objects exist: value objects and class objects. These two types are mainly differentiated by the way that they are stored in memory and how they are passed as arguments to methods or functions.

Value Type Objects

Two types of value objects are used and supported by C#: built-in types such as System.Int32 and System.Double, and user-defined types.

Built-In Value Types

C# has a rich set of built-in value types. The built-in types are listed in Table A.1.

Table A.1. Built-In Value Types
Category C# Data Type Description Size Range
Integer Byte An 8-bit unsigned integer. 8 0 to 255
 sbyte An 8-bit signed integer. Not CLS compliant. 8 –128 to 127
 short A 16-bit signed integer. 16 –32768 to 32767
 int A 32-bit signed integer. 32 –2147483648 to 2147483647
 long A 64-bit signed integer. 64 –9223372036854775808 to 9223372036854775807
 ushort A 16-bit unsigned integer. Not CLS compliant. 16 0 to 65535
 uint A 32-bit unsigned integer. Not CLS compliant. 32 0 to 4294967295
 ulong A 64-bit unsigned integer. Not CLS compliant. 64 0 to 18446744073709551615
Floating point float A single-precision (32-bit) floating-point number. 7 digits of precision. 32 –3.402823E+38 to 3.402823E+38
 double A double-precision (64-bit) floating-point number. 15–16 digits of precision. 64 –1.79769313486232E+308 to to 1.79769313486232E+308
Logical bool A Boolean value (true or false). 1 true (1), false(0)
Other char A Unicode (16-bit) character. 16 0 to 65535
 decimal A 96-bit decimal value. 28–29 bits of precision. 128 –79228162514264337593543950335 to 79228162514264337593543950335
 enum A user-defined name for a list of values. Depends on the base type. Valid base types are byte, sbyte, short, ushort, int, uint, long, and ulong. Default is int.

Listing A.6 shows some examples of using these built-in value types.

Listing A.6. Using Built-In Value Types
byte a = 2;
sbyte b = -2;
short c = 0x1FFF;
int d = -1234;
long e = 49;
ushort f = 0xFFFE;
uint g = 4294967294;
ulong h = 18446744073709551614;
float i = -1.0F;
float j = 1.0e2F;
double k = 10.8;
double l = 1234.5678D;
bool m = true;
char n = 'A';
char o = 'x456';
char p = 'u0924';
decimal q = 90.45M;

Enumerated types require a definition like the examples in Listing A.7.

Listing A.7. Defining an Enumeration Type
enum Cars
{
    Ford,
    Chevrolet,
    Buick,
    Dodge
}
enum Boats : byte
{
    Sail,
    Yacht,
    Speed,
    Fishing
}
enum Motorcycle : long
{
    Honda = 1,
    Yamaha,
    Kawasaki,
    BMW
}

After an enumeration type is defined, it can be used in switch statements, if statements, and other flow control, as in Listing A.8.

Listing A.8. Using an Enumeration Type
Boats eb = Boats.Sail;
switch(eb)
{
    case Boats.Sail:
        Console.WriteLine("Sail boat");
    break;
}

if(eb == Boats.Sail)
{
    Console.WriteLine("Sail boat");
}

Finally, a number of operations can be performed with enumeration types. Listing A.9 gives an example of some of these operations.

Listing A.9. Operations That Are Available on an Enumeration Type
Boats b = Boats.Sail;

// Print the string name of the enum
Console.WriteLine("{0} ", b);

// Print the value and name for each enum
foreach(byte i in Enum.GetValues(b.GetType()))
{
    Console.WriteLine("Boat Value: {0}  -> {1} ",
                          i,
                          Enum.GetName(b.GetType(), i));
}

// Iterate through each of the string names for the enum
foreach(string s in Enum.GetNames(b.GetType()))
{
    Console.WriteLine("Enum: {0} ", s);
}

// Try to get a enum with a particular name
b = (Boats)Enum.Parse(typeof(Boats), "Fishing", true);
Console.WriteLine("Parse enum: {0} ", b);

// See if a value is represented in the enum
if(Enum.IsDefined(typeof(Boats), (byte)3))
{
    Console.WriteLine("The value is 3 is defined for Boats");
}

Listing A.10 shows what the output for these operations would look like.

Listing A.10. Output for Enumeration Operations
Sail
Boat Value: 0 -> Sail
Boat Value: 1 -> Yacht
Boat Value: 2 -> Speed
Boat Value: 3 -> Fishing
Enum: Sail
Enum: Yacht
Enum: Speed
Enum: Fishing
Parse enum: Fishing
The value is 3 is defined for Boats

Value types are stored on the stack of a running program.

When a value type is boxed, it inherits from System.ValueType, and methods and properties of that class can be used to describe the particular value type. Boxing is simply the conversion of a value type to a class or reference type. Unboxing is the reverse operation. Boxing and unboxing are C#'s support for the IL box and unbox instructions.

User-Defined Value Types

It is also possible to define your own value type. When defining your own value type, you use a struct. Unlike C++ where the difference between a struct and a class is primarily member default access permissions, C# makes a larger distinction between a struct and a class. Following are some of the differences between a struct and a reference type or a class.

  • A struct is allocated on the stack and passed as an argument by value.

  • A struct cannot derive from a struct. A struct can only derive from an interface.

  • A struct cannot have an explicit parameterless constructor otherwise known as a default constructor.

When an object is defined as a struct, it assumes value semantics. It is created on the stack and passed by value when given as an argument to a function. A struct in C# is sealed; therefore, it cannot be used as a base for inheritance. A simple structure that represents the data members for the representation of a complex number is shown in Listing A.11.

Listing A.11. A Complex Value
struct Complex
{
    public double real;
    public double imag;
}

In a real application, you could add methods so that this object has more utility. Listing A.11 only shows the data, and it is not a functional object. For example, if you have a Complex value and want to print the value, you have code that looks like Listing A.12.

Listing A.12. Using the Default ToString Method
Complex cn = new Complex();
cn.real = 1.0;
cn.imag = 0.0;
Console.WriteLine("{0} ", cn);

Listing A.12 prints the following:

Testing.Complex

This is not what was expected. To show the actual values associated with this object, you must override the default ToString method. The resulting structure looks like Listing A.13.

Listing A.13. Printing a Complex Value
struct Complex
{
    public double real;
    public double imag;
    public override string ToString()
    {
        return (string.Format("({0} , {1} i)", real, imag));
    }
}

Listing A.12 outputs the following:

(1, 0i)

This is probably more what you had in mind.

It is generally not good practice to make fields public as in Listings A.11 and A.13. This violates one of the key principles of object-oriented programming: encapsulation. To support encapsulation, C# introduces the concept of an accessor. An accessor is a set of read or write methods that allows access to the fields in your object. If only a read accessor is defined, then the object is read-only. If both a read and write accessor are defined, then that field is read-write. The Complex struct to take advantage of accessors results in a Complex value that looks like Listing A.14.

Listing A.14. Adding Accessors to the Complex Value
struct Complex
{
    double real;
    double imag;
    public override string ToString()
    {
        return (string.Format("({0} , {1} i)", real, imag));
    }
    public double Real
    {
        get
        {
            return real;
        }
        set
        {
            real = value;
        }
    }
    public double Imaginary
    {
        get
        {
            return imag;
        }
        set
        {
            imag = value;
        }
    }
}

Accessing the member values of this user-defined type requires a slight modification to the code that was directly accessing the fields. Changing Listing A.12 to use accessors becomes Listing A.15.

Listing A.15. Using Accessors Instead of Direct Field Access
Complex cn = new Complex();
cn.Real = 1;
cn.Imaginary = 0;

In Listing A.15, what looks like a field access is actually turned into an access to the field via a method call.

To compare two Complex objects, add the code shown in Listing A.16 to your user-defined value type.

Listing A.16. Comparing Complex Objects
public static bool operator==(Complex a, Complex b)
{
    if(a.real == b.real &&
       a.imag == b.imag))
    {
        return(true);
    }
    else
    {
        return(false);
    }
}
public static bool operator!=(Complex a, Complex b)
{
    return(!(a == b));
}
public override bool Equals(object o)
{
    Complex b = (Complex)o;
    return(this == b);
}
public override int GetHashCode()
{
    return(real.GetHashCode() ^ imag.GetHashCode());
}

The last two methods, Equals and GetHashCode, are overrides strictly for interoperation with the rest of the .NET Framework. They are not required to compare objects; however, if they are not defined as a pair, the compiler generates a warning message.

If you want to add two Complex objects, then you would add the code from Listing A.16 to Listing A.17.

Listing A.17. Adding Complex Objects
public static Complex operator+(Complex a, Complex b)
{
Complex r = new Complex();
r.real = a.real + b.real;
r.imag = a.imag + b.imag;
return(r);
}

To negate a Complex value, you need to build a unary operator. Listing A.18 shows how this is done.

Listing A.18. Negating a Complex Object
public static Complex operator-(Complex a)
{
    Complex r = new Complex();
    r.real = -a.real;
    r.imag = -a.imag;
    return(r);
}
. . .
Complex cnn = -cn;
Console.WriteLine("{0} ", cnn);
. . .
(-1, 0i)

You get the idea. You could make many improvements to this object to make it easier to use. You could add methods to subtract, divide, and multiply two complex numbers. You could add a constructor that takes real and imaginary arguments so that you don't have to assign the fields individually. You could add methods to return the magnitude and phase corresponding to the real and imaginary values. You could add error handling. The list could go on and on. No matter how many methods and properties you add to this object, it is still a lightweight value type object because the memory required for an individual instance is that required for two 64-bit floating-point values. This is a perfect example of the kind of object that is suited for a value type. Custom value type objects are supposed to be relatively small. Contrast that with an object that looks like Listing A.19.

Listing A.19. A Large Object
struct BigObject
{
    long value1;
    long value2;
    long value3;
    long value4;
    long value5;
    long value6;
    long value7;
. . .
}

If this object were passed often as an argument, it could be a detriment to performance because each of the fields would have to be pushed on to the stack during the call and popped off of the stack in the called function. An object like this probably should be a reference type or class.

Strings Act Like Value Types

The string class is technically a reference type for the following reasons:

  • It is not boxed or unboxed.

  • It does not derive from ValueType when boxed.

  • It is allocated from the heap.

  • It is passed to a method by reference.

  • It is a class (System.String).

  • It is immutable. (You cannot change the value of the string without reallocating a new string.)

A string acts like a value type. (It has value semantics.) Listing A.20 shows some of the operations that can be performed with a string.

Listing A.20. String Operations
static void Main(string[] args)
{
    string s = "This is a test";
    // Output each character in the string
    foreach(char c in s)
    {
        Console.WriteLine("Char: {0} ", c);
    }
    // Output each character in the string using string indexing
    for(int i = 0; i < s.Length; i++)
    {
        Console.WriteLine("Char: {0} ", s[i]);
    }
    // Output each word in the string (word delimited by space)
    foreach(string sub in s.Split())
    {
        Console.WriteLine("Word: {0} ", sub);
    }
    // Convert the whole string to upper-case
    string us = s.ToUpper();
    Console.WriteLine(us);
    // Convert the whole string to lower-case
    string ls = s.ToLower();
    Console.WriteLine(ls);
    // Insert a string so the new string reads: This is a new test
    string ins = s.Insert(s.IndexOf("test"), "new ");
    Console.WriteLine(ins);
    // Replace a string so the new string reads: This is another test
    string rs = s.Replace("a", "another");
    Console.WriteLine(rs);
    // Find a substring starting where 'a' is and 6 characters after that
    string ss = s.Substring(s.IndexOf("a"), 6);
    Console.WriteLine(ss);
}

A common operation with strings is concatenation. It enables you to do what is illustrated in Listing A.21.

Listing A.21. String Operations
String a = "This is";
String b = " a test";
String c = a + b;

The resulting string will be what you expect (“This is a test”), but you should avoid this because a more efficient means can perform this function with the StringBuilder class in the System.Text namespace. Listing A.22 shows how to do this.

Listing A.22. Concatenating Strings with StringBuilder
using System.Text;
. . .
String astr = "This is";
String bstr = " a test";
StringBuilder sb = new StringBuilder();
sb.Append(astr);
sb.Append(bstr);

Reference Type Objects

Reference types are everything that is not a value type in a safe managed environment. This includes classes, interfaces, delegates, and arrays.

Arrays

C# supports arrays of any type. The class that supports arrays is defined in the abstract base class System.Array. There are three types of arrays: single dimensional, multi-dimensional, and jagged.

Listing A.23 shows an example of some of the operations available with a C# single dimension array.

Listing A.23. Single Dimension Array Operations
static void Single()
{
    // Declare an array
    int [] a = new int[10];
    for(int i = 0; i < a.Length; i++)
        a[i] = i;
    // Initialize and declare in one statement
    int [] b = new int [] {0, 1, 2 ,3 ,4 ,5, 6, 7, 8, 9} ;
    Console.WriteLine("{0}  {1}  {2} ", a.Equals(b),
                                     a.Length, b.Length);
    // Copy two elements from 'a' starting at index 5 to
    // 'b' starting at 0.
    Array.Copy(a, 5, b, 0, 2);
    foreach(int i in b)
        Console.WriteLine("{0} ", i);
    // Create an array of strings
    string [] c = new string [] {"Monday",
                                 "Tuesday",
                                 "Wednesday",
                                 "Thursday",
                                 "Friday",
                                 "Saturday",
                                 "Sunday"} ;
    // Iterate through the array of strings
    foreach(string s in c)
        Console.WriteLine("{0} ", s);
}

The first thing you might notice about C# arrays is the order of the [], known as the rank specifier. This is different from C++, so it might take you a little time to get used to the syntax. The compiler reminds you with an error like this if you forget the syntax:

array.cs(10,10): error CS0650: Syntax error, bad array declarator. To declare a
        managed array the rank specifier precedes the variable's identifier

The other feature of arrays that might take some getting used to is the declaration. You can declare an array with this:

int [] array;

You can initialize the array declaration with a size:

int [] array = new int [10];

You can also initialize an array with values and a size:

int [] array = new int [] { 1, 2, 3, 4, 5} ;

You can't declare an array of a specific size without new:

// Error !!!
int [10] array;

Again, this is just a convention that is valid with C++ but not valid with C#.

Listing A.24 shows an example of some of the operations available with a C# multi-dimensional array.

Listing A.24. Multi-Dimensional Array Operations
static void Multiple()
{
    // Declare a multi-dimensional array
    int [,] a = new int[5,5];
    for(int i = 0; i < a.GetLength(0); i++)
        for(int j = 0; j < a.GetLength(1); j++)
            a[i,j] = i * a.GetLength(1) + j;
    // Initialize and declare in one statement
    int [,] b = new int [,] {{0, 0, 0, 0, 0} ,
                             {1, 1, 1, 1, 1} ,
                             {2, 2, 2, 2, 2} ,
                             {3, 3, 3, 3, 3} ,
                             {4, 4, 4, 4, 4} } ;
    Console.WriteLine("{0}  {1}  dimensions {2} x{3}  {4} x{5} ", a.Equals(b),
                                                            a.Rank,
                                                            a.GetLength(0),
                                                            a.GetLength(1),
                                                            b.GetLength(0),
                                                            b.GetLength(1));
    // When copying between multi-dimensional arrays, the array
    // behaves like a long one-dimensional array, where the rows
    // (or columns) are conceptually laid end to end. For example,
    // if an array has three rows (or columns) with four elements
    // each, copying six elements from the beginning of the array
    // would copy all four elements of the first row (or column)
    // and the first two elements of the second row (or column).
    Array.Copy(a, 5, b, 5, 5);
    for(int i = 0; i < a.GetLength(0); i++)
    {
        for(int j = 0; j < a.GetLength(1); j++)
        {
            Console.Write("{0} ", b[i,j]);
        }
        Console.WriteLine();
    }
    // Create a two-dimensional array of strings
    string [,] c = new string [,]{{"0 - Monday",
                                   "0 - Tuesday",
                                   "0 - Wednesday",
                                   "0 - Thursday",
                                   "0 - Friday",
                                   "0 - Saturday",
                                   "0 - Sunday"} ,
                                  {"1 - Monday",
                                   "1 - Tuesday",
                                   "1 - Wednesday",
                                   "1 - Thursday",
                                   "1 - Friday",
                                   "1 - Saturday",
                                   "1 - Sunday"} } ;
    // Iterate through the array
    foreach(string s in c)
            Console.WriteLine(s);
    int [,,,] d = new int [5,5,5,5];
    Console.WriteLine("{0}  dimensions", d.Rank);
}

With multi-dimensional arrays, you might have trouble getting used to some of the syntax. When referencing an element within a multi-dimensional array, notice that the syntax is [1,2] to access the second row and the third element. This is unlike the [1][2] syntax that you might be used to. Notice, however, that it is easy to retrieve the number of dimensions in a given array (Rank) and to retrieve the number of elements in a given dimension (GetLength).

Listing A.24 shows only two-dimensional arrays. You could extend all of the features of a two-dimensional array to n-dimensional arrays.

A jagged array allows each row of an array to have different lengths (hence, the word jagged). Listing A.25 shows an example of some of the operations available with a C# jagged array.

Listing A.25. Jagged Array Operations
static void Jagged()
{
    // Declare a multi-dimensional array
    int [][] a = new int[5][];
    for(int i = 0; i < a.Length; i++)
        a[i] = new int [i + 1];
    for(int i = 0; i < a.Length; i++)
    {
        for(int j = 0; j < a[i].Length; j++)
        {
            a[i][j] = i;
        }
    }
    foreach(int [] ia in a)
    {
        foreach(int i in ia)
            Console.Write("{0} ", i);
        Console.WriteLine();
    }
    // Initialize and declare in one statement
    int [][] b = {new int [] {0} ,
                  new int [] {1, 1} ,
                  new int [] {2, 2, 2} ,
                  new int [] {3, 3, 3, 3} ,
                  new int [] {4, 4, 4, 4, 4} } ;
    foreach(int [] ia in b)
    {
        foreach(int i in ia)
            Console.Write("{0} ", i);
        Console.WriteLine();
    }
}

A jagged array is literally an array of arrays. Figure A.2 shows conceptually how the array looks.

Figure A.2. Jagged array.


When working with a jagged array, you need to avoid the temptation to think of it as a two-dimensional array. Again, it is an array of arrays.

Interface

An interface is like an abstract class with all the members abstract. It contains no data—just a signature of methods that should be implemented in the class that is derived from the interface. Unlike an abstract class, a class can derive from multiple interfaces. Listing A.26 shows some simple uses of interfaces.

Listing A.26. Interface Usage
using System;
namespace Testing
{
    interface ICommunicate
    {
        void Speak(string s);
        void Listen(string s);
        void Read(string s);
        void Write(string s);
    }
    interface ITravel
    {
        void Walk();
        void Car();
        void Train();
        void Plane();
    }
    class Activities: ICommunicate, ITravel
    {
        // ICommunicate
        public void Speak(string s)
        {
            Console.WriteLine("I said, "{0} ".", s);
        }
        public void Listen(string s)
        {
            Console.WriteLine("I heard you say, "{0} ".", s);
        }
        public void Read(string s)
        {
            Console.WriteLine("I just read, "{0} ".", s);
        }
        public void Write(string s)
        {
            Console.WriteLine("I just wrote, "{0} ".", s);
        }
        // ITravel
        public void Walk()
        {
            Console.WriteLine("I am walking.");
        }
        public void Car()
        {
            Console.WriteLine("I riding in a car.");
        }
        public void Train()
        {
            Console.WriteLine("I riding in a train.");
        }
        public void Plane()
        {
            Console.WriteLine("I riding in a plane.");
        }
    }
    class InterfaceMain
    {
        static void Main(string[] args)
        {
            Activities a = new Activities();
            // Look for an interface that is not implemented
            try
            {
                IComparable ic = (IComparable)a;
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
            // Checking to see if the interface is implemented
            if(a is ICommunicate)
            {
                ICommunicate ac = (ICommunicate)a;
                ac.Speak("I said that");
                ac.Listen("I am talking to you");
                ac.Read("The quick brown fox jumped over the lazy cow.");
                ac.Write("What I did on my summer vacation.");
            }
            if(a is ITravel)
            {
                ITravel at = (ITravel)a;
                at.Walk();
                at.Car();
                at.Train();
                at.Plane();
            }
            Console.WriteLine("--------------------------");
            // Checking to see if the interface is implemented
            // If it is implemented then the result is a non-null
            // interface.
            ICommunicate c = a as ICommunicate;
            if(c != null)
            {
                c.Speak("I said that");
                c.Listen("I am talking to you");
                c.Read("The quick brown fox jumped over the lazy cow.");
                c.Write("What I did on my summer vacation.");
            }
            ITravel t = a as ITravel;
            if(t != null)
            {
                t.Walk();
                t.Car();
                t.Train();
                t.Plane();
            }
        }
    }
}

If you had forgotten to implement one of the methods in the class that was deriving from these interfaces, you would get an error like this:

interface.cs(18,8): error CS0535: 'Testing.Activities' does not implement
        interface member 'Testing.ICommunicate.Read(string)'

The compiler helps you to use interfaces correctly.

Listing A.26 purposely repeats code so that you can see two different methods for getting at an interface. More precisely, you can see two different safe methods for getting at an interface. Notice the code at the beginning of Main where there is a cast to an interface that is not implemented. Here, you get an InvalidCastException because you are trying to cast to an interface that has not been implemented. If you want to deal with the exceptions, then you can simply cast the instance to the interface that you require. However, C# has provided two different methods that allow you to test for an interface without having an exception thrown. These two methods use the is and as operators.

The is operator tests for a specific interface. It returns true if the object supports that interface and false if it does not. This avoids having to catch an exception if the interface is not supported. The problem with the is operator is that after it is determined that the interface is supported, a cast still must be performed to get the interface. Thus, using the is operator usually requires two queries on the object. The as operator fixes this.

If the interface is supported by the object, then the as operator returns a reference to the interface. If the interface is not supported, then null is returned. This saves an extra query on the object if the test succeeds.

delegate

delegates and events were covered in detail in Chapter 14, “Delegates and Events.”

Class

A class defines an object. All of the other features of C# are auxiliary to and provide support for the class. It is in the class where a program becomes object oriented. Of course, simply using or defining a class does not necessarily make an object-oriented program. It is when the features of the language are used to correctly implement object-oriented principles that a program becomes object oriented. This brief overview of C# will show how C#, and in particular the class in C#, can be used to support the object-oriented tenets of encapsulation, inheritance, and polymorphism (see Inside C# by Tom Archer, Microsoft Press, 2001).

Encapsulation in its broadest sense is simply wrapping some feature or data. Earlier in this chapter, you saw a simple wrapping around two floating-point numbers in Listing A.11 that was called a Complex number. It was obvious as more features were added to that object, the two floating-point numbers became less visible, even hidden. Accessors turned the floating-point numbers into properties. At the point when the user of the object no longer has access to the internal data, the data is fully encapsulated and the programmer is free to modify the data without affecting the user. If you decided to modify the data to use two integers, two floats, or any other of a number of combinations, you could do so without affecting the user. When you can do that, your data is encapsulated.

At that point, you support the encapsulation tenet of object-oriented programming. To support encapsulation, a struct does not offer anything less in terms of features that would cause you to choose a class over a struct. A class can have accessors that follow the same syntax rules as for a struct. A class overrides ToString to support display of the object just as with a struct. A class implicitly derives from System.Object so that Equals, GetHashCode, and so on can be overridden in the same way as with a struct. In fact, you could replace the word struct with the word class in the code used to illustrate a Complex value type (see Listings A.11 through A.18) and the code would still compile and run.

A struct is implicitly sealed. It cannot be derived from another struct or class. If you try to create another struct derived from a struct, you get an error indicating that the base is not an interface. (A struct can derive from an interface.) If you try to derive from a struct to create a new class, you get an error like this:

cannot inherit from sealed class 'Testing.Complex'

Memory allocation is where a class and struct start to diverge. What if you had a complex number object as was illustrated in Listings A.11 through A.18 and you wanted to add a couple of properties to it so that it supported polar notation? If you either did not have access to the source or did not want to modify the source for Complex, you would not be able to do this. However, by applying the modifications shown in Listing A.27, this is possible.

Listing A.27. A Polar Class
class Complex
{
    protected double real;
    protected double imag;
. . .
class Polar: Complex
{
    public double Magnitude
    {
        get
        {
            return Math.Sqrt(real*real + imag*imag);
        }
    }
    public double Phase
    {
        get
        {
            return Math.Atan2(imag,real);
        }
    }
}

Once you make Complex a class (and allow access to its internal data via the protected keyword), you can derive from it as shown in the Polar class. Now all of the functionality of the Complex class is available to you. You can reuse the code to implement Complex. More importantly, this functionality has been tested; because you are not modifying it at all, you are not breaking anything. This is kind of like the Hippocratic oath for software: “As to diseases, make a habit of two things—to help, or at least do no harm.” (Hippocrates, The Epidemics, Bk. I, Sect. XI, tr. by W. H. S. Jones, cited in Familiar Medical Quotations, edited by Maurice B. Strauss, pub. by Little, Brown and Company, p. 625).

Polymorphism allows for a uniform treatment of all objects that is dependent on the object. You might want to think about that for a moment. C# uses polymorphism extensively. It is what makes something like the following possible:

Console.WriteLine("{0} ", obj);

What is exceptional about polymorphism is that obj in the preceding line can be any System.Object. Because everything is ultimately derived from System.Object in C#, this is not a problem. The default might not be what you had in mind, but it will always work. To change the default implementation, the implementation for the class that defines the obj instance needs to override the ToString method that is part of every System.Object. Because the methods in System.Object might be too restrictive or not have the functionality that you want to expose, you can create your own polymorphic system of objects.

A classic example of polymorphism is with a graphics application and shapes. Listing A.28 shows an outline of how this would be implemented.

Listing A.28. A Polymorphic Shape Drawing System
abstract class Shape
{
    public abstract void Draw();
}

class Circle: Shape
{
    double x;
    double y;
    double r;
    public Circle(double x, double y, double r)
    {
        this.x = x;
        this.y = y;
        this.r = r;
    }
    override public void Draw()
    {
        Console.WriteLine("Drawing a circle of radius {0} ", r);
    }
}

class Triangle: Shape
{
    double [,] vertices;
    public Triangle(double x1, double y1,
                    double x2, double y2,
                    double x3, double y3)
    {
        vertices = new double [,] {{x1, y1} ,
                                   {x2, y2} ,
                                   {x3, y3} } ;
    }
override public void Draw()
    {
        Console.WriteLine("Drawing a triangle");
    }
}

class Square: Shape
{
    double x;
    double y;
    double s;
    public Square(double x, double y, double s)
    {
        this.x = x;
        this.y = y;
        this.s = s;
    }
    override public void Draw()
    {
        Console.WriteLine("Drawing a {0} x{0}  square", s);
    }
}

class PolymorphicMain
{
    static void Main(string[] args)
    {
        Shape [] shapes = new Shape [] {new Circle(0,0,2),
                                        new Triangle(0,0,0,1,1,1),
                        new Square(0,0,5)} ;
        foreach(Shape s in shapes)
        {
            s.Draw();
        }
    }
}

Notice that Main has only an array of Shape objects. Each Shape object takes care of drawing itself. The programmer no longer needs to test for what type of object it is and draw it specifically. The programmer just calls the Draw method on each object in the array and each object draws itself. The output of the code in Listing A.28 looks like this:

Drawing a circle of radius 2
Drawing a triangle
Drawing a 5x5 square

Pointer Type

Value types and reference types are primarily used in a safe environment. If you have code running in an unsafe environment, then an additional type is available known as a pointer type. Listing A.29 shows a simple example of using a pointer type.

Listing A.29. A Pointer Type Example
using System;

// csc /unsafe pointer.cs
namespace Testing
{
    class PointerMain
    {
        static unsafe void FillBuffer(byte[] buffer)
        {
            int count = buffer.Length;
            fixed(byte *p = buffer)
            {
                for(int i = 0; i < count; i++)
                {
                    p[i] = (byte)i;
                }
            }
        }
        static void Main(string[] args)
        {
            byte [] buffer = new byte[256];
            FillBuffer(buffer);
        }
    }
}

This simple example fills a buffer with an increasing value based on its position in the buffer.

A more useful sample would be one that performs some useful work. When processing an image, it is frequently necessary to find out where in the image the edges of an object are. One method of doing that is to process the image with an edge-finding kernel. One particularly useful set of kernels is the set of Sobel kernels. The process involved is shown in Figure A.3

Figure A.3. Sobel edge operators.


This is a good example of when using a pointer type in an unsafe environment is necessary. If you want to process an image, you need to get at the data of the image. The first problem is that the only way you can get at the raw data is by retrieving a one-dimensional vector from the image. The second problem is to access the data. Each pixel or byte access needs to do at bare minimum an index operation, and when you add in the overhead associated with a managed call, this could slow things down quite a bit. A method in the Bitmap class allows the user to get at a pointer type that points to the data for the image. Listing A.30 shows a portion of the sample in the ImageProcessing directory that uses an unsafe pointer to find the edges of an image.

Listing A.30. Using an Unsafe Pointer to Process an Image
unsafe private void OnProcess(object sender, System.EventArgs e)
{
    BitmapData data = null;
    try
    {
        data = rawImage.LockBits(new Rectangle(new Point(0,0),
                                 new Size(rawImage.Width, rawImage.Height)),
                                 ImageLockMode.ReadWrite,
                                 rawImage.PixelFormat);
        int [,] cache = new int[data.Height,data.Width];
        Byte *p1;
        Byte *p2;
        Byte *p3;
        int max, min, temp;
        max = 0;
        min = 255;
        // Find the range
        for(int i = 1; i < data.Height - 1; i++)
        {
            // Points to the center of the kernel
            p1 = (Byte *)data.Scan0.ToPointer() + ((i - 1) * data.Stride);
            p2 = (Byte *)data.Scan0.ToPointer() + (i * data.Stride);
            p3 = (Byte *)data.Scan0.ToPointer() + ((i + 1) * data.Stride);
            for(int j = 1; j < data.Width - 1; j++)
            {
                temp = (Math.Abs((*p1 + *(p1 + 1) * 2 + *(p1 + 2)) –
                                 (*p3 + *(p3 + 1) * 2 + *(p3 + 2))) +
                        Math.Abs((*(p1 + 2) + *(p2 + 2) * 2 + *(p3 + 2)) –
                                 (*p1 + *p2 * 2 + *p3)));
                if(temp > max)
                    max = temp;
                if(temp < min)
                    min = temp;
                cache[i,j] = temp;
                p1++;
                p2++;
                p3++;
            }
        }
        for(int i = 1; i < data.Height - 1; i++)
        {
            // Points to the center of the kernel
            p2 = (Byte *)data.Scan0.ToPointer() + (i * data.Stride) + 1;
            for(int j = 1; j < data.Width - 1; j++)
            {
                temp = (int)(((float)cache[i,j]/(float)max) * 255.0);
                *p2++ = (Byte)temp;
            }
        }
    }
    finally
    {
        rawImage.UnlockBits(data);
        image.Refresh();
    }
}

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

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