Chapter 4: Properties and Operators


In this chapter, we will look at the role of properties and operators in C# class design. We'll see plenty of examples along the way to illustrate how properties can improve the encapsulation, usability, and robustness of our types. We will also look at Indexers, which are a special kind of property. We'll see how operator overloading can be used to simplify application development by allowing your own types to be used with arithmetic or Boolean operators, just like primitive types. Explicit and implicit conversion operators will be covered in Chapter 7.

By the end of this chapter, you should understand how to create meaningful properties and when and how to implement operators for your classes and structures. You should also understand the connection between properties and operators. Before continuing with this chapter, it is recommended that you first read the previous chapter on Methods. Many of the techniques you will learn in the Methods chapter directly apply to this chapter.

Properties in C#

Properties represent the attributes of a class unlike methods (which represent the actions of a class). For example, consider a Person class that has attributes such as Name and Salary. The Person class also has actions such as Update and CalculateBonus. We must decide how to represent the Name and Salary attributes in our code during implementation. One option would be to define Name and Salary as private fields in the Person class; however, this will prevent the information from being accessed elsewhere in the program. Declaring Name and Salary as public fields is not a good idea because it breaks the encapsulation of the class. It exposes the data directly so that it can be read and modified by any other part of the program; so much for data hiding! Properties solve this problem by providing indirect access to private variables through get and set accessors.

Property accessors are just methods that permit conditional abstract access to the underlying data. Property get and set accessors are sometimes called property procedures in other languages such as Visual Basic .NET.

Important

Properties represent the attributes of a class. They provide indirect access to private fields through get and set accessors.

From the perspective of a programmer using a class, a property can be accessed like a public field in a class. Users of the class can access the property name directly, as if the class really contains a public field with that name. The only difference is that we can make a property read-only or write-only; this is not possible with public fields.

Properties enable us to expose data to the client code, while at the same time preserving the encapsulation of how that data is represented within the class. For example, we can write validation code in the set accessor to ensure the user doesn't assign an illegal value to the property. Likewise, we can perform computations in the get accessor to recalculate the value of the property on request. We will discuss accessors later when we look at the MSIL code emitted by the C# compiler for properties; for now, understand that an accessor is an entry into the class like a method.

C# Property Syntax

Properties, like methods are implemented within classes. The public and private access modifiers discussed in Chapter 3 also apply to properties and the same rules apply. Properties can also be instance and static class members; these differences are discussed in a short while.

Let's start by defining how properties are implemented in C#. This example is actually a scalar property, but the principles of the get and set accessors are the same as an indexer:

     public class Person     {       private string name;       public string Name       {         get { return name; }         set { name = value; }       }     } 

In this example, we can see a private field called name. This field can be accessed through the get and set accessors of the Name property. We can omit either the get or set accessor for a property, but not both:

  • If we omit the set accessor, the property becomes read-only. This can be useful for properties that might need to be recalculated each time they are used, such as the days a person has worked at a company.

  • If we omit the get accessor, the property becomes write-only; this can be useful for a password property on a security object for example. However, since making the property readable is rarely a bad idea (you never know when someone might need to query it), this situation will not be common.

Java Note

Java properties are also defined as get and set methods but they must also be accessed as get and set methods too.

The CLS in the .NET Framework supports two different kinds of properties:

  • Scalar Properties

    A scalar property represents a single class attribute. A property can be a primitive value such as an int or reference type such as a string, or a more complex type such as a DateTime, a Color, or a BankAccount class. Unlike with Visual Basic .NET, C# scalar properties cannot be parameterized. However, both C# and Visual Basic .NET offer a more elegant solution in the form of Indexers.

  • Indexers (indexed properties)

    An indexer is a construct that allows array-like syntax to be used on a class. The client code uses array syntax to access a specific value in a private collection or array.

We'll examine both kinds of properties in detail during this chapter. During these discussions, remember that properties are a standard feature in the .NET Framework. This means client code written in any CLS-compliant language can use any property defined in our C# classes providing the property is of a CLS-compliant type.

Scalar Properties

Let's start with scalar properties. The following example was used on the previous page and shows a simple read/write property called Name, in a class called Person. We've compressed the syntax a little, so you can see how properties provide a neat way to write accessor code. We'll explain how properties can be used, after the code listing. The source code for this example is called simple_scalar_property.cs:

     public class Person     {       private string name;       public string Name       {         get { return name; }         set { name = value; }       }     } 

Note the following points in this example:

  • The Person class has a field called name, which holds the person's name. This field is declared as private, to prevent direct access by client code. As we've said several times already, one of the most important aims of object-oriented development is to preserve the encapsulation of the class.

  • The Person class has a property called Name, to get and set the person's name. The property acts as a wrapper for the name field. The Name property is declared as public, so that it can be used in client code. Most properties tend to be public, because the essence of properties is to provide a convenient public interface to a class. Nevertheless, situations do arise where a private, protected, or internal property might be required. For example, we can define a private property that can only be accessed by the other members in the same class. The Name property is also of type string; the get accessor must also return a value of type string. The underlying private field is usually of the same type too, as in this example.

  • The set accessor includes a hidden parameter called value. The value parameter contains the value that was passed from the client code.

Important

When you define a property, specify the type of the property by including the type between the access modifier and the name of the property. In this example, the Name property is of type string, but it can be any other publicly available type.

It is important to devise a consistent naming scheme that differentiates between fields and properties in a class. This will make the code more self-documenting, which will make it easier to write and (hopefully) reduce the bug count. Also, our code will be easier to maintain because our intentions are clearer for the maintenance programmer.

Defining a naming scheme for properties and fields is simplified in C# because C# is case-sensitive. In other words, name is not the same as Name. It is often necessary, particularly when considering properties, to represent the data in a number of ways; primarily fields for internal use and public properties for external consumption. As a developer, you need to use both meaningful field and property names that imply the same purpose. Using the same word for both the public property and its underlying type is a good idea because the connection is obvious.

The approach we've taken in this chapter is to use camel Case (xxXxx) for fields (for example, name or dateOfBirth) and Pascal Case (XxXxx) for properties (for example, Name or DateOfBirth). Some developers prefer to use a leading underscore for fields (for example, _Name). Properties should also be named using nouns.

It really doesn't matter what naming convention you choose, as long as you use it consistently within and across classes.

The following code snippet shows how to use the Name property defined in the Person class:

     public class SimpleScalarProperty     {       static void Main()       {         Person person = new Person();         person.Name = "James";         Console.WriteLine(person.Name);       }     } 

Note the following in the Main() method:

  • The client code has no direct access to the name field; all access to this data must be made indirectly through the Name property. This means the Person class developer retains control over how the data is implemented privately inside the class. This is called encapsulation.

  • The client code uses field-like syntax to access the property. This is more convenient than method-call syntax. For example, if Person defined a pair of methods called GetName() and SetName() (similar to Java), the client code would have to use this rather more cumbersome syntax to get and set the person's name:

         person.SetName("James")     Console.WriteLine(person.GetName()) 

Now that we've seen how to define and use a simple property, it's time to roll up our sleeves and discuss the design and implementation issues that enable us to use properties correctly in our classes, starting with the MSIL code emitted by the C# compiler.

Compiling Scalar Properties into MSIL

In Chapter 3, we discussed the role of MSIL code. We looked at how different kinds of methods compiled into MSIL code. When properties are compiled, the MSIL code emitted from the C# compiler also produces the MSIL .method routines for properties. This is because properties are specialized types of methods, and this is how they are compiled into MSIL code.

When we define a read/write property in a class, the C# compiler generates a pair of methods for the MSIL code. For example, if our property is called Name, the compiler generates methods called get_Name() and set_Name() in the MSIL code. Whenever these properties are used in client code, the compiler implicitly generates code to invoke the get_Name() and set_Name() MSIL methods.

We can use the MSIL Disassembler tool ildasm to illustrate this behavior.

click to expand

Notice the following members of the Person class:

Member name

Description

m_name

This is a private string field, to hold the person's name

get_Name

This method is generated by the compiler, to get the Name property as a string

set_Name

This method is generated by the compiler, to set the Name property as a string

Name

This is the Name property itself, which contains the get and set methods

The following screenshot illustrates how the Name property is implemented in MSIL; this is obtained by double-clicking on the Name property in the above window:

click to expand

Notice that the Name property contains two MSIL statements that identify the get and set methods for this property. The following is the MSIL generated for the Main() method:

click to expand

The statements marked in the above screenshot set and get the Name property by setting the compiler-generated code to invoke the set_Name(), and get_Name() methods respectively.

By understanding how our code is compiled into MSIL, we can use language features effectively and appropriately in our code. For example, we have just seen that properties are implemented as methods in MSIL code. Whenever client code uses a property, the compiler uses a method call.

Some .NET Framework languages are much more explicit about the way properties map onto methods. A good example is Managed Extensions for C++; to create a read/write property called Name, we would explicitly define separate property functions called get_Name() and set_Name(). However, these differences at the source-code level are leveled when we compile our code into MSIL. MSIL code always looks the same, regardless of which programming language we use to write the source code.

It's worth thinking here for a moment about what happens to the MSIL code at run time. The JIT compiler compiles this code, which performs some optimizations based on what it knows about the code that's been loaded in. This means that while the indirection introduced by properties generates more lines of MSIL instructions, the JIT compiler is able to optimize some of the operations and generate tight, fast code. In fact, in many cases, the JIT compiler can make property access faster than field access, which sounds impossible. However, method calls can be optimized efficiently, and access to member data in the same instance can be faster than remote access to member data in other instances. This makes a property an excellent technique to quickly access the state of an object.

Read/Write, Read-Only, and Write-Only Properties

The Name property in the previous example was a read/write property. We can also define read-only properties and write-only properties. Read-only properties are quite useful, because they enable us to expose data without allowing the client code to modify the data. For example, we might want to provide a read-only property to get a person's date of birth. Similarly, we could use a write-only property to set a security password.

The following example illustrates read/write properties, read-only properties, and write-only properties. The Person class has the following properties:

  • A Name property that gets and sets a person's name. The person's name is allowed to change, for example if the person gets married or decides they don't like the name they were born with.

  • A DOB property, to get the person's date of birth. The person's date of birth must be set in the constructor, and cannot change thereafter (a person's date of birth cannot change after the person has been born).

  • An EmailAlias property, to set the person's e-mail alias. The e-mail alias represents the first part of an employee's e-mail address, before the domain name. The e-mail alias can be modified, but it can never be retrieved on its own (instead, the client program retrieves the full e-mail address).

  • An EmailAddress property, to get the person's full e-mail address. The e-mail address is computed every time it is requested, by appending the company's domain name to a person's e-mail alias.

The source code for this example is located in the file readable_and_writable.cs:

     using System;     public class Person     {       private string name;       private DateTime dob;       private string emailAlias;       public Person(string name, DateTime dOB)       {         name = name;         dob = dOB;       }       public string Name       {         get { return name; }         set { name = value;}       }       public DateTime DOB       {         get { return dob;}       }       public string EmailAlias       {         set { emailAlias = value; }       }       public string EmailAddress       {         get { return emailAlias + "@MyCompany.com"; }       }     }     class ReadableAndWritable     {       static void Main()       {         Person person = new Person("Steve", new DateTime(1972, 8, 22));         person.EmailAlias = "Steve.S";         Console.WriteLine("Name - " + person.Name);         Console.WriteLine("DOB - " + person.DOB.ToShortDateString());         Console.WriteLine("Email Address - " + person.EmailAddress);       }     } 

When we run the program, we get the following output in the console window:

 C:\Class Design\Ch 04>readable_and_writable.exe Name - Steve DOB - 22 August 1972 Email Address - Steve.S@MyCompany.com 

We can use the MSIL Disassembler tool to investigate how read-only and write-only properties are compiled into MSIL code.

Run the MSIL Disassembler on readable_and_writable.exe and you should see the following:

click to expand

Notice the following items in the MSIL Disassembler window:

  • The get_Name() and set_Name() methods get and set the Name property

  • The get_DOB() method gets the DOB property

  • The set_EmailAlias() method sets the EmailAlias property

  • The get_EmailAddress() method gets the EmailAddress property

  • The C# compiler did not emit a set_DOB() method; this is because the DOB property is read-only

  • The C# compiler did not emit a set_EmailAddress() method; this is because the e-mail address is derived from EmailAlias, and is a read-only property

Static Properties

A static property represents a piece of type-wide information that we want to expose as part of the class, rather than as part of a particular instance. In the same way as static methods discussed in the previous chapter, client code accesses static properties through the class, rather than through an instance of the class.

The following example shows how to define a static property named Domain, to represent the domain name used as part of a person's e-mail address. The assumption here is that all people in our application have the same domain name. Therefore, the domain name is not specific to an instance of the Person class, but the Person class as a whole.

The source code for this example is located in the file static_properties.cs:

     using System;     public class Person     {       private string name;       private DateTime dob;       private string emailAlias;       private static string domain;       public Person(string Name, DateTime dOB)       {         name = Name;         dob = dOB;       }       public string Name       {         get { return name; }         set { name = value; }       }       public DateTime DOB       {         get { return dob; }       }       public string EmailAlias       {         set { emailAlias = value; }       }       public static string Domain       {         get { return domain; }         set { domain = value; }       }       public string EmailAddress       {         get { return emailAlias + "@" + domain; }       }     }     class ReadableAndWritable     {       static void Main()       {         Person person = new Person("Steve", new DateTime(1972, 8, 22));         person.EmailAlias = "Steve.S";         Person.Domain = "AnotherCompany.com";         Console.WriteLine("Name - " + person.Name);         Console.WriteLine("DOB - " + person.DOB.ToShortDateString());         Console.WriteLine("EMail Address - " + person.EmailAddress);       }     } 

Note the following points with this example:

  • The Domain property is static

  • The EmailAlias property uses the domain name to generate the full e-mail address for the person

  • The Main subroutine sets the Domain property by using the class name (Person) rather than an instance (such as person)

Guidelines for Writing Get Procedures

When we define a read-write or read-only property, we need to consider how to write the get accessor.

The get accessor shouldn't make any changes that affect the state of the object in any way. This is because calling a get accessor is logically comparable to accessing a public field. If we find ourselves updating state in our get accessor, we should implement it as a method instead. Methods may, and often do, have side effects. We suggest two distinct scenarios where get accessors are useful:

  • To get the value of an existing field in the class. In this scenario, the get function simply acts as a wrapper for a private field that is already present in the class. The get function returns the value directly.

  • To calculate a derived value in the class. In this scenario, the get accessor uses data in the class to compute the new value. In UML, for object-oriented analysis and design, this is known as a derived attribute.

Guidelines for Writing Set Procedures

When we define a read/write or write-only property, we need to consider how to write the set accessor. Perhaps the most important decision is how to handle illegal values. One way to do this is to throw an exception to indicate to the client code that an illegal value is unacceptable. Since values are passed to property set accessors in the same way as arguments are passed to parameterized methods, we can throw a System.ArgumentException like an ArgumentOutOfRangeException or an AgrumentNullException, either because the value is out of range, or it is null.

Important

Structured Exception Handling is discussed in Chapter 3. Exceptions can be handled and thrown inside properties just like methods.

Complete Example of Scalar Properties

The following example illustrates all of the issues we've discussed in this section on scalar properties. In this example, we define a class named FootballTeam to represent a football team. Among other members, the class has properties for the soccer team's name, the color of their jerseys, and the number of points the team has earned so far (3 points for a win, 1 point for a draw, and 0 points for a defeat).

Read through the code to understand how the properties are defined in the class. The System.Drawing class is used for the Color class, whereas System.IO is used for file handling classes. The source code for this example is located in scalar_properties_complete.cs:

     using System;     using System.Drawing;     using System.IO;     class FootballTeam     {       private string name;       private Color jerseyColor;       private short wins, draws, defeats;       private bool logging;       public FootballTeam(string teamName, Color teamColor)       {         name = teamName;         jerseyColor = teamColor;       }       public string Name       {         get { return name; }       }       public int Points       {         get { return ((wins * 3) + (draws * 1)); }       }       public Color JerseyColor       {         get { return jerseyColor; }         set {           if(value.Equals(Color.Black))           {             throw(new ArgumentException(               "Teams cannot have Black jerseys"));           }           else           {             jerseyColor = value;           }         }       }       public bool Logging { set { logging = value; } }       private FileStream LogStream       {         get {           try           {             return (new FileStream(name + ".log", FileMode.Append,                                    FileAccess.Write));           }           catch (System.IO.IOException)           {             return (new FileStream("Default.log", FileMode.Append,                                     FileAccess.Write));           }         }       }       public void PlayGame(string opponent, short goalsFor,                  short goalsAgainst)       {         if (goalsFor > goalsAgainst)           wins++;         else if (goalsFor == goalsAgainst)           draws++;         else           defeats++;         if (logging)         {           StreamWriter sw = new StreamWriter(LogStream);           sw.WriteLine("{0} {1}-{2} {3}", name, goalsFor,                goalsAgainst, opponent);           sw.Flush();           sw.Close();         }       }     } 

Note the following design points in the FootballTeam class:

  • The constructor explicitly initializes name and jerseyColor. The other fields are initialized implicitly; wins, draws, and defeats are implicitly initialized to 0, and logging is implicitly initialized to False. It's good practice to rely on implicit initialization where it is suitable; in our example, the implicit initialization for wins, draws, defeats, and logging is fine.

  • The Name property gets the name of the soccer team. There is no set accessor for Name, because a soccer team cannot change its name once it has been created (unlike US football teams, where the name of a team can change when the franchise is sold to another city).

  • The Points property calculates and returns the number of points earned by the team so far. This property is never stored in the object, but is always recalculated on demand. If the Points property is accessed frequently, an alternative strategy would be to have a points field that is updated every time a game is played. However, if the Points property is seldom used, the overhead of keeping the value up to date might not be worth the effort; this design assumption led us to recalculate the Points value only when requested.

  • The JerseyColor property gets and sets the color of the team's jerseys. The set accessor includes validation, to prevent teams from having black jerseys (since only referees can wear black). An ArgumentException is thrown in this case.

  • The Logging property is a rare example of a write-only property. This property enables client code to enable or disable logging of results to a file. If this property is True, we write the result of every game the team plays to a log file. There is no get accessor for the Logging property as the client program should never need to query whether logging is enabled or disabled.

  • The LogStream property is a Private property. This means it can only be used within the FootballTeam class. The purpose of the property is to create and return a FileStream object, which can be used to write results to a log file. The accessor encapsulates logic that decides which file to use for logging. This is a good example of a property being more appropriate than a field.

  • The PlayGame() method increments wins, draws, or defeats. The subroutine also tests the Logging property to see if logging is enabled; if it is, the subroutine uses the LogStream property to get a FileStream object to use to log the result to file.

We can use the FootballTeam properties (and other members) as shown in the following client code:

     class Season     {       static void Main()       {         FootballTeam myTeam = new FootballTeam("Wolves", Color.Gold);         myTeam.Logging = true;         myTeam.PlayGame("Portsmouth", 2, 1);         myTeam.PlayGame("Manchester United", 2, 3);         myTeam.PlayGame("West Bromwich Albion", 4, 0);         myTeam.PlayGame("Stoke City", 3, 2);         myTeam.PlayGame("West Ham", 1, 1);         Console.WriteLine(myTeam.Name + "                           (" + myTeam.JerseyColor.Name + ") - " +                           myTeam.Points.ToString());       }     } 

Note the following points about the Main() method:

  • We use the Name read-only property to display the name of the soccer team

  • We use the JerseyColor read/write property to change the color of the team's jerseys, and then display the new jersey color

  • We use the Logging write-only property to enable file logging

  • We use the Points read-only property to calculate the number of points earned by the team

When you run the application, it should display the following output in the console window:

 C:\Class Design\Ch 04>scalar_properties_complete.exe Wolves (Gold) - 10 

The application also logs the football results to a file named Wolves.log. The file contains the following:

         Wolves 2-1 Portsmouth         Wolves 2-3 Manchester United         Wolves 4-0 West Bromwich Albion         Wolves 3-2 Stoke City         Wolves 1-1 Newcastle United 

Indexers

Now that we've seen how to use scalar properties, let's take a look at indexers.

C# does not permit parameterized scalar properties. In other languages that do support parameterized scalar properties, they tend to be used in conjunction with private arrays or collections of data. For instance, if we have a Person class with a property called Child, the Person class must support the possibility of several children. The Child parameter would be used to reference each element of an array.

This technique is not allowed in C#; however, C# does support indexers, which are a more elegant technique for solving this problem.

C++ Note:

C# Indexers are called Indexed Properties in C++.

We've already considered a Person class with a Child property, but what if we need to deal with people instead of a single Person? Here we have a requirement for an object hierarchy of Person objects each with Child objects. Indexers allow types such as a Person to be treated like an array:

  • Indexers are declared like a scalar property

  • Indexers provide indirect access to the underlying private fields using get and set accessors

  • Indexer set accessors have a hidden and implicit parameter called value

  • Indexers can be any value or reference type

Unlike scalar properties, however, indexers have the following characteristics:

  • Indexers allow a class to be treated like an array using the [] operators

  • Indexers are parameterized and accept arguments

  • Indexers almost always provide indirect access to hidden collections or arrays of types

  • Indexer properties must be declared as this

  • Indexers add a hidden property called Item to a class

  • Indexers behave like a default property because the client code does not need to refer to the name of the indexer

  • Classes can only have one indexer because a class can only have one default type member

When a class exposes an indexer, it is sometimes referred to as a container class because it acts as a container for a collection of other types. Those types in turn can also be containers for other types creating a hierarchy of object types. A good example of this within the .NET Framework is a DataSet object, which is a container of Table objects. Table objects in turn are collections for Row and Column objects.

When indexers are used to create an object hierarchy like this, the object relationships then become similar in principal to database one-to-many or parent-child relationships.

Defining a Single Indexer in a Class

Recall that indexers provide indirect access to private variables, just like scalar properties. Unlike scalar properties though, this private data is almost always some kind of collection such as an array of classes or structures. This is because we normally want to index whole entities rather than a single attribute of an entity.

For example, if we create an indexer property in a Person class to represent the person's Name, we could write code like this:

     Person[1] = "James";     Person[2] = "Steve"; 

This would provide an array-like syntax but limits us for other class attributes such as Email, Address, and so on. We cannot add additional indexers because indexers are default properties and only one default property is allowed in a class.

To use an indexer, we need to index the Person entity as a whole and not just the person's name. To do this, we need a containing class an indexer of the type Person. This supports the sensible requirement of being able to index a Person and not just one attribute of the Person.

The following example (indexers_simple.cs) demonstrates this idea:

     using System;     public class Person     {       private string name;       public Person(string Name)       {         name = Name;       }       public string Name       {         get { return name; }       }     }     public class Authors     {       private Person[] persons = new Person[5];       public Person this [int index]       {         get { return persons[index]; }         set { persons[index] = value; }       }     }     public class SingleIndexer     {       static void Main()       {         Authors authors = new Authors();         authors[0] = new Person("James");         authors[1] = new Person("Roger");         authors[2] = new Person("Ben");         authors[3] = new Person("Richard");         authors[4] = new Person("Teun");         for(int i=0; i<5; i++)           Console.WriteLine(authors[i].Name);       }     } 

When you compile and run this program, the following list will be displayed in the console windows:

 C:\Class Design\Ch 04>indexers_simple.exe James Roger Ben Richard Teun 

Let's now consider the main points of interest with our first indexer:

  • The Person entity is represented as a class with properties and respective private fields to store and access the attributes of the Person.

  • The Authors class is the container class for Person. It is called a container class because it contains a hidden private array of type Person and because it contains the indexer. The indexer is also of type Person.

  • The indexer property is parameterized and accepts an argument of type int. Like scalar properties, the indexer also provides indirect access to a private field. However, the private field is an array of Person objects. The int parameter represents the ordinal value in the array.

  • The indexer is created with the this keyword, which implicitly creates a default property called Item. The indexer is accessed in the C# client code without specifying the name of the property – Item, because it is the default property.

  • The SingleIndexer client code class has a very intuitive coding style. We are using the container class (Authors) with the array syntax but each element is of type Person.

  • The management of the array of Person classes is hidden from the client code. This is called encapsulation or data hiding because the array of persons can only be accessed through the Authors class indexer.

The fact concerning the default property called Item is important when the C# class containing the indexer is accessed through non-C# client code. Because Item is a default property, it can never be referred to in the C# client program. Though other languages such as Visual Basic .NET, using our C# Person class can explicitly refer to the Item property. Compare the following example in Visual Basic .NET with the C# client code above:

 indexers_simple.vb:     Module Module1       Sub Main()       Dim i As Integer       Dim authors As New Authors()       authors.Item(0) = New Person("James")       authors.Item(1) = New Person("Roger")       authors.Item(2) = New Person("Ben")       authors.Item(3) = New Person("Richard")       authors.Item(4) = New Person("Teun")       For i = 0 To 4         Console.WriteLine(authors.Item(i).Name)       Next i     End Sub End Module 

Here the Visual Basic .NET client code can make explicit reference to the Item indexer property. If your C# classes are likely to be consumed by Visual Basic.NET client code, the default property name for an indexer can be overridden. Change the Authors class as shown below. (The complete code listing can be found in indexers_indexer_name.cs.)

     public class Authors     {       Person[] persons = new Person[5];       [System.Runtime.CompilerServices.IndexerName("Person")]       public Person this [int index]       {         get { return persons[index]; }         set { persons[index] = value; }       }     }   } 

Applying the above attribute allows Visual Basic.NET client code to refer to a Person property instead of an Item property:

         authors.Person(0) = New Person("James")         authors.Person(1) = New Person("Roger")         authors.Person(2) = New Person("Ben")         authors.Person(3) = New Person("Richard")         authors.Person(4) = New Person("Teun") 

Let's recap the main observations so far:

  • We have seen how to define a single indexer on a class to create a parent-child relationship similar to a one-to-many database relationship.

  • The term container class has been introduced. A container class is the parent in the parent-child relationship.

  • The container class has a private array of objects of the same type as the indexer; in our example the container or parent class was the Authors class, and the child was a Person class. The private array within Authors was also of type Person.

  • Child classes can themselves contain indexers making them parent classes for other child classes. This is similar to an ADO.NET DataSet object, which contains Table, which in turn contains Columns and Rows.

  • An indexer is actually a default property called Item that is hidden in C# client code. C# client code does not refer to the Item property when using the array [] operators.

  • Visual Basic .NET clients can refer to the Item property.

  • The default indexer property name of Item can be changed using an attribute – [System.Runtime.CompilerServices.IndexerName ("DefaultPropertyName")]. This can make Visual Basic.NET client code more legible.

  • Indexers differ from scalar properties in that they are parameterized, the parameter though, is used as an identifier for the child class – in the examples so far, the ordinal position in the private array. Indexers can accept different and multiple arguments as we will shortly see.

Compiling Indexers into MSIL

To gain a deeper view of how indexers work, compile the C# example above and examine the executable output using ildasm. From the command prompt, type the following:

     C:\Class Design\Ch 04>ildasm indexer_single_vb.dll 

The following screen should be displayed after you have expanded the Authors and Person nodes:

click to expand

The MSIL code emitted by the C# compiler confirms our earlier observations:

  • We can see the indexer defined in the Authors class. Its definition is the same as a scalar property, as it has both get and set accessors.

  • Notice that the indexer is called Person and not Item. This is because the default name of the indexer has been overridden.

  • Indexers are in fact default properties; the Authors class contains a DefaultMemberAttribute that allows the indexer (default property called Person) to be referenced implicitly through C# client code.

  • In Visual Basic .NET, the default Person property can be explicitly referenced.

Using Indexers with ArrayLists

The previous example worked well enough as a demonstration of indexers, but lacked much functionality for a production-quality component. The array in our example had a fixed length of five. The boundaries of the array are stored internally within the Authors class and there is no functionality to add or remove Person classes from the Authors class.

We need to make our Authors class more useful by adding :

  • A method to Add Person classes

  • A method to Remove Person classes

  • A method to Count the Person classes

Important

When considering the use of indexers in your class, familiarization with the Systems.Collections namespace is a good idea. This namespace provides many useful variations on the array and collection theme. One of the most useful is the ArrayList, which will be used predominantly through the rest of this topic.

We will now extend the previous example by including our wish list of functionality detailed above.

Modify the Authors class from the indexers_simple.cs file as follows. The complete listing can be found in indexers_simple_arraylist.cs:

    public class Authors    {      private ArrayList persons = new ArrayList();      public Person this [int index]      {        get { return ((Person)persons[index]); }        set { persons[index] = value; }      }      public void Add(Person person)      { persons.Add(person); }      public void Remove(Person person)      { persons.Remove(person); }      public int Count { get { return persons.Count; } }    } 

Our Authors class is now much more useful. Instead of using a simple array of Person classes, we are now using an ArrayList of Person classes. The ArrayList object can be found in the System.Collections namespace. This ArrayList object inherently supports all of the functionality we need to make our Authors class work properly, so we are actually providing a strongly typed wrapper to the ArrayList object through an indexer. Our Authors class is strongly typed because an ArrayList object stores elements of type object. The Authors class is designed to accept and return values of type Person, which is why the indexer get accessor explicitly casts the ArrayList element as a Person.

Important

Because ArrayList elements are of type Object, elements are implicitly boxed.

To test the new Authors class, add the following client code:

     public class SingleIndexer     {       static void Main()       {         Authors authors = new Authors();         authors.Add(new Person("James"));         authors.Add(new Person("Roger"));         authors.Add(new Person("Ben"));         authors.Add(new Person("Richard"));         authors.Add(new Person("Teun"));         for(int i=0; i<authors.Count; i++)           Console.WriteLine(authors[i].Name);       }     } 

The program will still produce the same results as the examples used earlier in this topic, even though we have significantly changed the implementation of the Authors class. The differences to note are:

  • The Authors class contains a private ArrayList object, which is used to store each Person object.

  • The Authors class Add() method is now used add a new Person to the Authors class instead of the indexer set accessor. The indexer set accessor still functions as a mechanism for updating Person objects through the Authors class.

  • The indexer is being used within a for loop to provide the sequential output of the Name property of the Person class to the console window.

In this section we have used the ArrayList class to enhance the Authors class by providing the familiar Add(), Remove(), and Count() methods found in many of the .NET Framework classes. The ArrayList class itself provides this functionality so it makes sense to provide a strongly typed wrapper around the ArrayList as well as keeping the indexer property to support sequential read and update access.

Overloading Indexers

Sometimes it makes sense to index a collection on a value other than its ordinal value. The indexer used so far in this chapter is based on the ordinal value of a Person class within the ArrayList. However, we might want to index the collection based on the person's name for example. An indexer can be overloaded just like a method. The same rules for method overloading (discussed in Chapter 3) apply to overloading indexers, that is, multiple indexers may have the same name, as long as they have unique signatures.

To make this example possible, we need to expand the Person class to store some additional data. Our overloaded indexer will permit Person objects to be accessed by Name. An Age property will also be added to the Person object to demonstrate that the client code is working and the new indexer is finding the correct Person. The complete code listing can be found in overloading_indexer.cs:

    using System;    using System.Collections;    public class Person    {      string name;      short age;      public Person(string name, short age)      {        this.name = name;        this.age = age;      }      public string Name      {        get { return name; }        set { name = value; }      }      public short Age      {        get { return age; }        set { age = value; }      }    }    public class Authors    {      private ArrayList persons = new ArrayList();      public Person this [int index]      { get { return ((Person)persons[index]); } } 

We now provide an overload of the indexer that permits the client code to index based on the Name of the author (of type Person) as well as the ordinal value in the ArrayList.

     public Person this [string name]     {       get       {         foreach(Person person in persons)         {           if (person.Name == name)             return person;         }         return (Person)persons[0];       }     } 

The main difference between the new (overloaded) indexer and the first indexer is that the get accessor has to search for the correct Person class. This is done with a foreach loop. Note that this example only works because each person has a unique name. If the ArrayList contained more than one Person object with the same name, only the first Person would ever be found. Using the original indexer, which accesses the ArrayList by the ordinal value of the elements, would still permit access to other person objects with the same name.

Important

The ArrayList class also supports the methods BinarySearch and Contains which might provide an efficient search mechanism in the get accessors of your indexers.

     public void Add(Person person)     { persons.Add(person); }     public void Remove(Person person)     { persons.Remove(person); }     public int Count { get { return persons.Count; } }   }   public class OverloadedIndexer   {     static void Main()     {       Authors authors = new Authors();       authors.Add(new Person("James", 31));       authors.Add(new Person("Roger", 21));       authors.Add(new Person("Ben", 21));       authors.Add(new Person("Richard", 21));       authors.Add(new Person("Teun", 21));       Person author = authors["James"];       Console.WriteLine(author.Name + " - " + author.Age.ToString());       author.Age = 21;       author = authors["James"];       Console.WriteLine(author.Name + " - " + author.Age.ToString());     }   } 

We have omitted the set accessors for both indexers, making the indexers read-only, because we would have to write some fairly awkward code, similar to the get accessor to search and then update the Person class. A much simpler solution is to provide set accessors within the Person class itself. Because the get accessors of both indexers actually return a reference to a Person type, we can channel all updates to an existing Person through a reference to the Person type.

Important

When the internal array or collection contains lots of data, a more efficient search routine may be necessary so as not to degrade performance. Sequentially looping and comparing many objects within an ArrayList could be very inefficient if there are many elements. Consider using the BinarySearch or Contains methods instead.

Indexer Summary

Indexers are used to make a class behave like an array by using the [] operator syntax. Indexer properties are actually a hidden default property called Item. This Item property is never referred to in C# code because it is the default type member. Because a class can only have one default type-member, there can only be one indexer in a class. However, an indexer can be overloaded to index a class on different internal data.

Although indexers can be used to index simple types such as strings or numbers, indexers are most effective when indexing more complex types such as custom classes like our Person class. To index a custom class, create a container class that exposes an indexer of the same type as the custom class that will be indexed. The container class should contain a private internal array or collection to store the indexed type, and have public methods to add, remove, and count the indexed type. The private ArrayList object already contains Add(), Remove(), and Count() members, so the container class then becomes a wrapper for the private ArrayList but with an indexer property to allow the client code to treat the custom class like an array.

We can provide overloaded indexers to index the custom class on one of its properties instead of its ordinal value within the ArrayList, by including specialized code to search for the required object in the ArrayList. To enable the search routines inside the indexer get accessor to deal with duplicate objects, a unique identifier can be used. The indexer is then used in a very similar way to relational database access.

Indexers actually work by overloading the [] operators to make a class behave like an array.




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