.NET: Special .NET Type Members

We were unable to locate this content in de-de.

Here is the same content in en-us.

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MSDN Magazine
Special .NET Type Members
Jeffrey Richter
I
n the October and December 2000 .NET columns, I examined the fundamentals of types. This month I'll take a look at some of the special members that a type can define. These members encourage good object-oriented design while greatly simplifying the syntax required to manipulate a type and its object instances.

Type Constructors

      You are already familiar with constructors, which are responsible for setting an object instance to its initial state. In addition to instance constructors, the Microsoft® .NET common language runtime (CLR) also supports type constructors (also known as static constructors, class constructors, or type initializers). A type constructor can be applied to interfaces, classes, and value types. It allows the type to perform any initialization required before any members declared within the type are accessed. Type constructors accept no parameters and always have a return type of void. A type constructor only has access to a type's static fields and its usual purpose is to initialize those fields. A type's constructor is guaranteed to run before any instance of the type is created and before any static field or method of the type is referenced.
      Many languages (including C#) automatically generate type constructors for any types you define. However, some languages will require you to implement the type constructors explicitly.
      To understand type constructors, examine the following type (defined in C#):
class AType {
   static int x = 5;
}
When this code is built, the compiler automatically generates a type constructor for AType. This constructor is responsible for initializing the static field x to the value 5. If you're using ILDasm, you can easily spot type constructor methods because they have a name of .cctor (for class constructor).
      In C#, you can implement type constructors yourself by defining a static constructor method in your type. The use of the static keyword makes the constructor a type constructor rather than an instance constructor. Here is a very simple example:
class AType {
   static int x;

   static AType() {
      x = 5;
   }
}
This type definition is identical to the previous one. Note that a type constructor must never attempt to create any instances of its own type, and the constructor must not reference any of the type's nonstatic members.
      Finally, if you give the C# compiler the following code, it generates a single type constructor method.
class AType {
   static int x = 5;

   static AType() {
      x = 10;
   }
}
This constructor first initializes x to 5 and then initializes x to 10. In other words, the resulting type constructor generated by the compiler first contains the static field initialization code, which is then followed by the code in your type constructor method.

Properties

      Many types define attributes that can be retrieved or altered. Quite frequently, these attributes are implemented as field members of the type. For example, here is a type definition that contains two fields:
class Employee {
   public String Name;
   public Int32 Age;
}
      If you were to create an instance of this type, then you could easily get or set any of the following attributes with code like this:
Employee e = new Employee();
e.Name = "Jeffrey Richter"; // Set the Name attribute
e.Age = 36;                 // Set the Age attribute

Console.WriteLine(e.Name);  // Displays "Jeffrey Richter" 
      Working with attributes in this way is very common. However, in my opinion the previous code should never be implemented as shown. One of the covenants of object-oriented design and programming is data abstraction. Data abstraction means that your type's fields should never be publicly exposed because it's too easy to write code that improperly uses the fields, corrupting the state of the object. For example, it's all too easy for someone to corrupt an Employee object with code like this:
e.Age = -5; // How could someone be -5 years old?
      So, when designing a type, I strongly suggest that all your fields be private, or at least protectedâ€"never public. Then, to allow a user of your type to get or set an attribute, expose methods specifically for this purpose. Methods that wrap access to a field are typically called accessor methods. They can optionally perform sanity checking and ensure that the object's state is never corrupted. For example, I'd rewrite the Employee class shown previously to resemble the code in Figure 1. This is a simple example, but you can see the enormous benefit of abstracting the data fields. You can also see how easy it is to make properties read-only or write-only by just not implementing certain accessor methods.
      Abstracting the data as shown in Figure 1 has two disadvantages. First, there is more code to write since you now have to implement additional functions. Second, users of the type now must call methods rather than simply referring to a single field name:
e.SetAge(36);    // Updates the age
e.SetAge(-5);    // Throws an exception 
I think you'll all agree that these disadvantages are quite minor. Nevertheless, the runtime offers a mechanism called properties, which somewhat alleviates the first disadvantage and removes the second disadvantage entirely.
      The class shown in Figure 2 uses properties and is functionally identical to the class in Figure 1. As you can see, properties simplify the code slightly, but more importantly, they allow the caller to write their code as follows:
e.Age = 36;    // Updates the age
e.Age = -5;    // Throws an exception
      The return value from a get property accessor and the parameter passed to a set property accessor are of the same type. Set properties have a void return type and get properties have no parameters. Properties may be static, virtual, abstract, internal, private, protected, or public. In addition, properties may be defined in an interface, which I'll discuss later.
      I should also point out that properties do not have to be associated with a field. For example, the System.IO.FileStream type defines a Length property that returns the number of bytes in the stream. This length is not maintained in a field; instead, when the length property get method is called, it calls another function that asks the underlying operating system to return the number of bytes in the open file's stream.
      When you create a property, the compiler actually emits special get_PropName and/or set_PropName accessor methods (where PropName is the name of the property). Most compilers will understand these special methods and will allow developers to access the methods using the special property syntax. However, a compiler that complies with the Common Language Specification (CLS) is not required to fully support properties; the compiler is only required to support the calling of the special accessor methods.
      In addition, compilers that do fully support properties may require slightly different syntax for defining and using properties. For example, C++ with managed extensions requires the use of the __property keyword.

Index Properties

      Some types, like System.Collections.SortedList, expose a logical list of elements. To make accessing the elements in this type easy, the type can define an index property (also referred to as an indexer). An example of an index property is shown in Figure 3. Using this type's indexer is incredibly simple:
BitArray ba = new BitArray(14);
for (int x = 0; x < 14; x++) {
   // Turn all even numbered bits on
   ba[x] = (x % 2 == 0);
   Console.WriteLine("Bit " + x + " is " + (ba[x] ? "On" : "Off"));
}
      In the BitArray example in Figure 3, the indexer takes one Int32 parameter: bitPosition. While all indexers must have at least one parameter, they may have two or more parameters. These parameters (as well as the return type) may be of any type. It is quite common to create an indexer that takes a String as a parameter to look up values in an associative array. A type can offer multiple, overloaded indexers as long as their prototypes differ.
      Like set properties, a set indexer accessor method contains a hidden parameter, value, which indicates the new desired value when the accessor is called. The BitArray set accessor shows this value parameter being used.
      A well-designed indexer should have both get and set accessors. Even though you can implement only a get accessor (for read-only semantics) or only a set accessor (for write-only semantics), it is recommended that indexers always implement both accessors. The reason is simply that a user of an index wouldn't expect only half of the behavior. For example, the user would not expect to see a compiler error when writing the following two lines:
String s = SomeObj[5];  // Compiles OK if get accessor exists
SomeObj[5] = s;         // Compiler error if set accessor doesn't exist
      Indexers always act on a specific instance of a type and cannot be declared static. However, indexers may be marked as public, private, protected, or internal.
      When you create an index property, the compiler actually emits special get_Item and/or set_Item accessor methods. Most compilers will understand these special methods and will allow developers to access the methods using the special index property syntax. However, a CLS-compliant compiler is not required to fully support index properties; the compiler is only required to support the calling of the special accessor methods.
      Also, compilers that completely support index properties may require a somewhat different syntax for defining and using these properties. C++ with managed extensions, for example, requires you to use the __property keyword.

Conclusion

      The concepts discussed in this column are extremely important to all programmers developing for .NET. The special type members I mentioned make components first-class citizens in the common language runtime. That is, modern components are designed to support properties.
      In my next column, I'll introduce delegates and event members, as they are an integral part of component-based application development and design.
Jeffrey Richter (http://www.wintellect.com/) is the author of Programming Applications for Microsoft Windows (Microsoft Press, 1999), and is a co-founder of Wintellect (http://www.Wintellect.com), a software education, debugging, and consulting firm. He specializes in programming/design for .NET and Win32®. Jeff is currently writing a Microsoft .NET Framework programming book and offers .NET technology seminars.

From the February 2001 issue of MSDN Magazine

Page view tracker