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.
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.
C# has a rich set of built-in value types. The built-in types are listed in Table A.1.
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.
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.
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.
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.10 shows what the output for these operations would look like.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
A common operation with strings is concatenation. It enables you to do what is illustrated in Listing A.21.
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.
using System.Text; . . . String astr = "This is"; String bstr = " a test"; StringBuilder sb = new StringBuilder(); sb.Append(astr); sb.Append(bstr); |
Reference types are everything that is not a value type in a safe managed environment. This includes classes, interfaces, delegates, and 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.
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.
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.
A jagged array is literally an array of arrays. Figure A.2 shows conceptually how the array looks.
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.
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.
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.
delegates and events were covered in detail in Chapter 14, “Delegates and Events.”
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.
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.
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
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.
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
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.
3.16.66.206