Reference Types


These are some of the main reference types that exist in the Common Type System. In this section, we'll elaborate on each of these in turn.

  • Class types
    Most of the types we define in a typical application are class types. Classes specify the data and methods for the important objects in our application.

  • Delegates
    Delegates represent type-safe pointers to methods. We can use delegates to specify callbacks, and to register handlers for Graphical User Interface (GUI) events such as button clicks.

  • Arrays
    Arrays are allocated on the managed heap and accessed by reference. Arrays can be one-dimensional or multi-dimensional. You can also have arrays of arrays, which permit jagged array structures.

  • Strings
    The .NET Framework includes inbuilt comprehensive support for handling strings. This support extends deep into the CLR and includes features that help us to write applications that will work correctly on different runtime platforms and with different character sets.

Class Types

In object-oriented terminology, a class defines the methods and data for a specific type of object in the system. Many developers in the object-oriented community use the phrase abstract data type when they talk about classes. We need to be careful in .NET, because a class is only one kind of abstract data type – the term can equally be applied to structures. In addition, .NET uses the word abstract to refer to something quite different.

In the .NET Framework a class is a reference type; objects are allocated on the Common Language Runtime's managed heap, and the lifetime of these objects is controlled by the runtime. In this section we'll focus on defining classes, and during these discussions it is important to remember that classes are always reference types in the .NET Framework.

Defining Classes in C#

We use the class keyword to define a class. This is same as for C++ and Java. In fact the basic syntax for declaring a C# class is indistinguishable from Java. For example, the following code snippet will compile in either language:

    public class MyClass    {        private static int refCount = 0;        public int someNumber;        public void DoSomething()        {        }    } 

This is also not so different from C++, where the above might have been written as follows:

    class MyClass    {    public:        int someNumber;        void DoSomething()        {        }    private:        static int refCount;    }    int MyClass::refCount = 0; 
C++ Note:

Remember, in C#, there are no global variables or global functions. Everything must be defined within a class or structure. Also, if you don't supply a default constructor for a class, the C# compiler will generate one for you, which initializes all member fields to zero.

A class contains data and functionality. The following table describes the kinds of data members we can define in a class:

Kind of data member

Description

Field

A field is a piece of data that is stored in objects of this class. We can also use the static keyword, to define data that is shared by all objects of the class.

Method

A method is a function that provides some aspect of behavior for objects of this class. We can also use the static keyword to define methods that apply to the class itself, rather than to a particular object.

Property

A property looks like a field to anyone using your class, except that its value is always retrieved and modified through ‘get’ and ‘set’ methods defined in the class. A property presents functionality as if it were data.

Indexer

A type indexer is a kind of property that allows a class to be accessed using the [] notation. This is typically used to permit access to items in a collection.

Constant

A constant is a read-only value that is shared by all objects of the class.

Event

An event is a message that an object can send to interested listener objects. In the .NET Framework, we use the terminology event source to denote an object that can raise events, and event receiver to denote an object that can receive events. An event defined as a member of a type identifies all objects of that type as sources for that event.

Type

Types can contain nested types. This is only recommended for specific purposes, as illustrated in Nested Classes, later.

Constructor

A constructor is an initialization method. Every time we create an object of a particular class, a constructor is called to initialize the state of the object.

Destructor

A class can have a single finalization method. The CLR calls this method just before the garbage-collection process destroys an object. This is a good place to perform tidying-up operations before the object disappears from the scene.

C++ Note:

As in C++, destructors are specified using the ~ symbol. Destructors can only be declared for reference types and cannot be overridden. Unlike in C++, a destructor cannot be called explicitly, as the garbage collector manages the objects' lifetimes. Each destructor in an object's hierarchy is called automatically before the memory for the object is reclaimed.

Just a quick note on constructors: in C#, we can define two kinds of constructors; instance constructors, which we are all familiar with, and static constructors, also referred to as type initializers or type constructors. A static constructor is similar to a static initialization block in Java. Static constructors are declared just like instance constructors, but by adding the static keyword. A class may have at most one static constructor, which takes no parameters. If defined, a static constructor is invoked before the first invocation of a static method in the class and before the first time an instance of the class is created. Static constructors are discussed further in Chapter 5.

The following class definition illustrates some of these kinds of members. It's unlikely we'd define all these in the same class, but we've listed them here for illustrative purposes. We'll consider the design heuristics for each kind of member in later chapters.

    // class_members.cs -- compile with /t:library option    class MyClass    {      // Field (usually private, for encapsulation)      private int aField;      // Property (usually public, for ease of use)      public int AProperty      {        get { return aField; }        set { aField = value; }      }      // Constant (class-wide, read-only field)      private const int aConstant = 43;      // Delegate (defines a method signature)      public delegate void ADelegate(int aParameter);      // Event (to alert event receiver objects)      public event ADelegate AnEvent;      // Method      public void AMethod()      {        // Method implementation code      }      // Instance constructor (to initialize new objects)      public MyClass(int aValue)      {        aField = aValue;      }      // Destructor method (to tidy up unmanaged resources)      ~MyClass()      {        // Finalization code      }      // Nested class definition      private class aNestedClass      {        // class definition      }    } 

Defining Accessibility for Classes

When we define a type, we can specify its accessibility. We can make the type private to its declaration context, accessible from other code in the same assembly, or accessible from code in any assembly.

Note

Assemblies are a key concept in the .NET Framework. In the simplest case, an assembly is a single file (.exe or .dll). When we write an application, we can decide whether we want to define all our classes (and other types) in the same assembly, or spread the classes across different assemblies for deployment purposes. Assemblies, as the name implies, can themselves consist of multiple files. For more information about assemblies, including a discussion on how to organize class definitions in assemblies, see Chapter 8.

Defining the accessibility for types is an important consideration in large systems, where the number of types can easily run into the hundreds. The aim is to limit visibility as much as possible. By hiding types from other parts of the system, we make the system more decoupled and easier to maintain. We can change a type more easily if there are fewer dependencies on it elsewhere in the system.

The following examples show how to specify class accessibility using C#:

    class MyClass1    {        // members    } 

In the absence of an access modifier, MyClass1 is implicitly private, which means no other class from the same, or any other assembly can access it. On the other hand, MyClass2 is explicitly public to all assemblies:

    public class MyClass2    {         // members    } 

It is important to be aware that the default accessibility depends on the context in which it is being defined. It is good practice to make accessibility explicit by always declaring an accessibility modifier to avoid other code changes having unwanted side effects. For example, simply enclosing MyClass1 in a namespace declaration would change the default accessibility from private to internal.

Public and private accessibility of classes in an assembly is similar to Java's convention, except that the default accessibility is private rather than package. However, there are a few more access modifiers that can be used on members defined within a class, including with nested classes.

There are three additional access modifiers that may be used to control the accessibility of your classes (nested or otherwise) or their members. These are:

Access modifier

Description

internal

The member or class may only be accessed from within the same assembly.

protected

The member or class may only be accessed from its parent class or from any types derived from the parent class.

protected internal

The member or class may be accessed from anywhere within the same assembly, or from its parent class, or from any types derived from the parent class.

Accessibility is covered in more detail in the next chapter.

Nested Classes

All the things declared in a class are effectively nested within it. There is nothing to stop you nesting any type within a class, including other classes. For instance, you may want to declare a data structure that is only useful inside a particular class, and the simplest way to do this is use a nested class. A nested class is not only a self-contained class just like any other, but also has access to all of the members defined in its parent class.

Let's see a brief example using the access modifiers shown in the previous section.

    // class_visibility.cs    public class MyClass    {      class MyPrivateClass      {        // members      }      internal class MyInternalClass      {        // members      }      protected class MyProtectedClass      {        // members      }      protected internal class MyProtIntClass      {        // members      }    } 

Compile this example to a DLL, and view the MSIL code in the MSIL Disassembler:

click to expand

Notice the following in this screenshot:

  • MyClass is qualified with the MSIL keyword public

  • MyInternalClass is qualified with the MSIL keyword assembly

  • MyPrivateClass is qualified with the MSIL keyword private

  • MyProtectedClass is qualified with the MSIL keyword family

  • MyProtIntClass is qualified with the MSIL keyword famorassem

The full implications of using the protected access modifier are covered in Chapter 7 Inheritance and Polymorphism. public, private, and protected access modifiers will be familiar to both C++ and Java developers although the meaning of these terms may initially be confusing; internal is a new access modifier for everyone.

Java Note:

public and private have the same meaning as in Java. internal access is scoped to assemblies rather than namespaces (which would be more analogous to Java). internal protected is equivalent to Java's protected access, and C#'s protected access is equivalent to Java's private protected access, which was made obsolete in Java some time ago.

Java developers will be familiar with two types of nested class: inner classes and static nested classes. In C#, a nested class is akin to Java's static nested class. There is no direct C# equivalent to a Java inner class, although the effect can be replicated by creating a nested class that holds a reference to its parent class and requires an instance to be passed to its constructor. This does have the advantage that the relationship between the nested class and its parent instance is made explicit in the code. Java developers should also note that there is no C# equivalent to anonymous inner classes; and C# classes may not be defined within method bodies.

Some useful guidelines are available on MSDN to help decide when the use of nested classes is a good design choice. Among these are the following:

  • If your class is logically contained by another class and has no independent significance, then implement it as a nested class.

  • If members of your class need to access private member fields of the object containing it, implement it as a nested class.

  • If your class needs to implement an interface that returns another type of object that implements another interface (like IEnumerable.GetEnumerator()), you can create an implementation of that interface as a nested class and return the object through the required method. We'll look more at interfaces towards the end of the chapter.

  • If other classes will reasonably need access, or will want to collect or contain your class, do not implement it as a nested class. A class that is accessed by several other classes should stand alone.

  • In general, do not implement public nested classes. Nested classes should primarily be for the internal use of the containing class. If you do implement a public nested class, it should be a class that logically belongs to the containing type, and is used infrequently. One exception would be if an instance of the nested class were exposed as a property on the containing class.

Creating and Disposing of Class Objects

As with all object-oriented languages, we can write constructors in our class, to initialize objects when they are created. If we don't define any constructors in our class, the C# compiler will generate a parameterless constructor on our behalf. Static constructors are covered in Chapter 5.

We know that the Common Language Runtime tracks objects on the managed heap, and flags them for removal when they are no longer required in our application. The garbage-collection mechanism prevents memory leaks, and makes it easier for us to manage complex object relationships in our applications.

In C#, we can write a destructor method, to tidy up the object just before it is deallocated. A destructor is similar to a destructor in C++, or a class terminate method in Visual Basic 6, except that like a finalizer in Java, you have no control over the timing of this event. Destructors can also cause a significant run-time overhead because of the way the garbage-collection process executes finalization code. The garbage-collection process has to maintain a list of all defunct objects that require finalization, and ensure these objects are finalized at the appropriate time. Given the non-deterministic nature of object deallocation, we can never be sure when the finalization code will run.

Delegates

Delegates are an important part of the Common Type System in the .NET Framework. A delegate is like a type-safe pointer to a method in a class. We define a delegate to specify the signature of methods we would like to call through the delegate, since the delegate itself has no implementation. We can then create a delegate object and bind it to any method whose signature matches that of the delegate.

C++ developers can imagine delegates as type-safe function pointers. Java developers would probably use interfaces to achieve a similar effect. The availability of delegates in C# improves on the function pointer approach by being type-safe (and by being able to hold multiple methods), and it improves on the interface approach by allowing the invocation of a method without the need for inner classes.

Delegates are useful in the following scenarios:

  • When registering a method as a callback method with another object. When something important happens to that object, it can invoke our callback method. An example of this usage is defining callback methods for events on Graphical User Interface (GUI) controls such as buttons and text fields.

  • When choosing one of a series of methods (with the same signature) for use in an algorithm. For example, we might have methods to calculate the sine, cosine, and tangent of an angle. We can use a delegate to dictate which of these methods is used by the algorithm.

There are three required steps when defining and using delegates:

  • Declare the delegate

  • Create a delegate object, and bind it to a particular method

  • Invoke the method by using the delegate object

Chapter 6 examines delegates in detail.

Arrays

As we mentioned earlier, unlike in many other programming languages, arrays in .NET are actually reference types. They are not just blocks of memory containing multiple copies of another type, but are in fact true objects that are allocated on the managed heap by the CLR. All arrays have an implicit base class of System.Array, which provides some useful functionality. We'll now find out how to declare arrays in C#.

Declaring Arrays

C# allows us to declare one-dimensional arrays (sometimes referred to as vectors), multi-dimensional arrays, or jagged arrays. Jagged arrays are effectively arrays of arrays, each of which may be of a different size. As in C++ and Java, arrays in C# are declared using the [] syntax:

     // one-dimensional array     int[] oneDee = new int[10];     // two-dimensional rectangular array     int[,] twoDee = new int[10, 5];     // jagged array     int[][] jaggy = new int[3][];     jaggy[0] = new int[5];     jaggy[1] = new int[10];     jaggy[2] = new int[20]; 

Note that unlike with C++ the [] must always follow the type name, not the variable name. This is the same as the syntax used most commonly in Java. Also note that an array must be initialized using the new operator, because it is a reference type.

C++ Note:

Arrays in C# are a much more predictable and usable datatype than the C-based arrays of C++. Their size and type is discoverable at run time, and if they are multidimensional, their dimensions can also be discovered. .NET array types are defined by the element type, and the number of dimensions they have – but not by the size of the array. It's not possible therefore to specify that a method's parameter must be an array of two integers, only that it must be an array of integers.

Initializing Array Elements

When an array is created, all elements are automatically initialized to their default values. Alternatively, for all arrays (except jagged), we can use an array initializer to load some preset data. In this case, we don't need to specify the array dimensions as this can be determined at compile time:

    int[] oneDee = new int[] {4, 4, 8, 2, 5, 9};    int[,] twoDee = new int[,] {{1, 3}, {9, 4}, {4, 4}, {8, 2}, {5, 9}}; 

C# also allows us to use a slightly simplified syntax, which has exactly the same effect:

    int[] oneDee = {4, 4, 8, 2, 5, 9};    int[,] twoDee = {{1, 3}, {9, 4}, {4, 4}, {8, 2}, {5, 9}}; 

Here, each curly brace enclosed term relates to the items at indices [x,0] and [x,1], respectively, where x ranges from 0 to 4.

As each element in a jagged array is itself an array reference, it will be implicitly initialized to null. This is why we cannot specify an array initializer for a jagged array, but have to first create each of the sub-arrays:

    int[][] jaggy = new int[3][];    jaggy[0] = new int[] {2, 8, 8, 9, 4};    jaggy[1] = new int[] {2, 3, 0, 7, 9, 6};    jaggy[2] = new int[] {1, 8, 1, 1, 1, 9, 5, 9}; 

Note that in this situation we cannot use the simplified initialization syntax, which is only permitted at the point of declaration.

Using Arrays

Individual array elements are accessed using the [] notation in a way similar to that used for array declaration. For example, we can access elements in our initialized arrays like this:

    int numa = oneDee[2];    // numa now contains 2    int numb = twoDee[0, 1];    // numb now contains 3    int numc = jaggy[1][3];    // numc now contains 7 

All developers should note that arrays in C# are by default zero based. Visual Basic developers should note the use of square brackets instead of parentheses to access array elements in C#.

As arrays inherit all of the functionality defined in System.Array, we have access to some useful properties and methods to query and manipulate the array and its elements. For some of these methods, the element types must support particular interfaces. We'll cover interfaces at the end of the chapter. Here are a few of the public members we can use from System.Array:

Member

Description

Rank

This read-only property returns the number of dimensions in the array.

GetLength()

Returns the number of elements in a specified dimension.

Sort()

Sorts the elements in one array, two arrays, or part of one array. Note that the array element type must implement IComparer or IComparable.

BinarySearch()

Uses a binary search algorithm to search a sorted array for a particular element. As with Sort(), the array element type must implement IComparer or IComparable.

IndexOf()

Returns the index of the first matching occurrence of an element in a one-dimensional array.

In addition to this, when we access array elements in our code, the CLR ensures that the array index is valid at run time. This means that we cannot accidentally or deliberately overstep the boundaries of an array and so gain access to memory outside the allowable range. This is not only useful for tracking bugs but also plugs a potential security hole.

Casting Arrays

With a few restrictions, C# and the .NET runtime allow us to cast arrays between reference types. For this to work both array types must have the same number of dimensions and an implicit or explicit conversion between the element types must exist. The CLR doesn't allow implicit casting of arrays containing value types but System.Array defines a Copy() method that allows us to achieve the same effect.

Casting an array to another type is similar to the conversion between any two types. If there is a valid conversion between two reference types, then the same conversion should be valid for arrays of those reference types. This is known as array covariance and because of this the CLR checks at run time whether any assignment to an element of a reference array is made from a valid type or not. For example, while it is legal to cast a string array to an object array, the third line of code in the following example would throw an exception:

     string[] someStrings = new string[4];      // ok     object[] someObjects = someStrings;        // ok     someObjects[2] = 42;                      // throws exception! 

It is more useful if we consider how to cast between arrays of value types. Perhaps you have an int array containing screen coordinates, on which you now wish to perform some algorithm requiring floating-point precision. You can use the Copy() method to quickly and efficiently perform this conversion on the whole array:

     int[] points = {13, 145, 87, 209};     double[] work = new double[points.Length];     Array.Copy(points, work, points.Length); 

You'll see a few more array handling examples in the next section.

Strings

The last section of our tour through standard .NET reference types deals with strings. Support for string handling in .NET is integrated tightly with the CLR and is very comprehensive. Hence, C# regards strings as primitive types, although they are actually reference types, and they are defined by the System.String class. In this section we'll see how to declare and use strings and just touch on some of the additional features. For more details, consult the C# Text Manipulation Handbook, Apress, ISBN 1-86100-823-6.

Declaring Strings

The fact that strings are considered as primitive types shows itself in a number of ways. For example, we initialize a string using a literal value:

    string s = "Hello World";                 // Ok    string s = new String("Hello world");     // Error! 

You can initialize a string using a constructor, but only by passing in chars or char arrays (or memory pointers to byte arrays if writing unsafe, non-CLS-compliant code). C# defines the string keyword to indicate its primitive status and this is often a cause for confusion among new C# developers. In fact, string and System.String are synonymous and may be used interchangeably in your code, although it is always better to stick with one standard.

One great relief to C++ developers will be the C# syntax for specifying an @-quoted string, sometimes called a verbatim string. Simply prefixing the literal string value with the @ symbol will ensure that any escape sequences in the string are not processed, thus avoiding the need for doubling up escape characters. The following two statements declare identical strings; see which you think is clearer:

    string s1 = "C:\\Winnt\\System32\\Write.exe";    string s2 = @"C:\Winnt\System32\Write.exe"; 

C# allows the use of the same escape sequences familiar to C and C++ developers. For example, we can use \t to encode a tab character, \n for a newline, and \r for a carriage return. However, there is a slightly more robust way of defining a newline character; as the System.Environment class defines a static NewLine property that is platform independent. Under Windows, this property contains "\r\n" and is used by other .NET Framework classes, such as by the Console.WriteLine() method.

Strings are Immutable

One of the most important things to be aware of in .NET is that strings are immutable. This is the same as for the java.lang.String class in Java, and it means that a string never changes once it has been initialized. It cannot shrink, grow, or have any characters altered.

Whenever you perform any manipulation on a string, a new instance is returned and the original string left intact. If we write the following statements, the test is performed on a temporary copy of our string, which is subsequently removed by the garbage collector:

    string s = @"C:\Winnt\System32\Write.exe";    if (s.ToLower().EndsWith("dll"))    {        // Do something    } 

Mutability is covered in more detail in Chapter 2.

Using Strings

We'll finish off our brief look at strings with an example that includes arrays. First we define an array of strings using an array initializer:

    using System;    public class StringClass    {      public static void Main()      {        // Initialize at declaration        string[] chapters = {"Managed heap", "Array handling",                             "Reference types", "Year 2000"}; 

Strings and arrays are reference types, so what we have here is a stack variable, chapters, containing a reference to an array on the managed heap. That array contains references to strings elsewhere on the managed heap. If we attempt to change any of these array elements, this will result in the original element being replaced with a reference to a new string instance. Next we'll define a second string array, but this time we'll initialize individual elements in code:

        // Initialize by element        string[] sections = new string[8];        sections[0] = "Value types";        sections[1] = "Language features";        sections[2] = "Implementing interfaces"; 

We can concatenate strings using the ‘+’ operator:

         // Concatenate strings         sections[3] = "User-defined " + sections[0]; 

Again, this doesn't change the string objects on which it operates; it creates a completely new string object, and it is a reference to this object that we place in the third element of sections. Now, we'll use the CopyTo() method defined in System.Array to copy strings between our two arrays. This works fast since strings are reference types, and so only the references need to be copied. The CLR doesn't need to duplicate the actual strings themselves. Because strings are immutable, it doesn't matter if we perform any operations on the strings in the original array – the actual strings won't change. The effect of this is to make strings appear to behave in exactly the same way as value types, even though they are reference types. This behavior can be very useful, and is the reason for coding reference types immutably, which we'll examine in the next chapter.

         // Copy elements between arrays         chapters.CopyTo(sections, 4); 

Using the System.Array.Sort() method, we can sort either the whole array or, as in this example, just a subset of its elements. The String class implements the IComparable interface, which is necessary for it to be sorted by System.Array. In fact there are a range of comparison methods in the String class, which can cater for case-sensitivity and take into account the calling thread's CurrentCulture to ensure correct comparison of characters in different languages. For example, in German, the strings ‘Strasse’ and ‘Stra e’ might need to be considered equivalent.

         // Partial string sort         Array.Sort(sections, 0, 4); 

Finally, as System.Array implements IEnumerable, we can use the C# foreach keyword to iterate through the elements in the array and display them to the console:

         // Enumerate array         foreach (string s in sections)         {           Console.WriteLine(s);         }      }    } 

Save this file as string_array.cs, and compile it. The application produces the following output:

      C:\> string_array      Implementing interfaces      Language features      User-defined Value types      Value types      Managed heap      Array handling      Reference types      Year 2000 




C# Class Design Handbook(c) Coding Effective Classes
C# Class Design Handbook: Coding Effective Classes
ISBN: 1590592573
EAN: 2147483647
Year: N/A
Pages: 90

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net