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.
Special .NET Type Members | |
Jeffrey Richter | |
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.
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).
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.
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:
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:
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:
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.
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 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. 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:
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.
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. 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. |
|
Jeffrey Richter (https://www.wintellect.com/) is the author of Programming Applications for Microsoft Windows (Microsoft Press, 1999), and is a co-founder of Wintellect (https://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