Inside C#

Following is chapter 7 from Inside C#, a book published by Microsoft Press.


7 - Properties, Arrays, and Indexers

So far, I've described the basic types supported by C# and how to declare and use them in your classes and applications. This chapter will break the pattern of presenting one major feature of the language per chapter. In this chapter, you'll learn about properties, arrays, and indexers because these language features share a common bond. They enable you, the C# class developer, to extend the basic class/field/method structure of a class to expose a more intuitive and natural interface to your class's members.

Properties as Smart Fields

It's always a good goal to design classes that not only hide the implementation of the class's methods, but that also disallow any direct member access to the class's fields. By providing accessor methods whose job it is to retrieve and set the values of these fields, you can be assured that a field is treated correctlythat is, according to the rules of your specific problem domainand that any additional processing that's needed is performed.

As an example, let's say that you have an address class with a ZIP code field and a city field. When the client modifies the Address.ZipCode field, you want to validate the ZIP code against a database and automatically set the Address.City field based on that ZIP code. If the client had direct access to a public Address.ZipCode member, you'd have no easy way of doing either of these things because changing a member variable directly doesn't require a method. Therefore, instead of granting direct access to the Address.ZipCode field, a better design would be to define the Address.ZipCode and Address.City fields as protected and provide an accessor methods for getting and setting the Address.ZipCode field. This way, you can attach code to the change that performs any additional work that needs to be done.

This ZIP code example would be programmed in C# as follows. Notice that the actual ZipCode field is defined as protected and, therefore, not accessible from the client and that the accessor methods, GetZipCode and SetZipCode, are defined as public.

    class Address
    {
        protected string ZipCode;
        protected string City;
        public string GetZipCode()
        {
            return this.ZipCode;
        }
        public void SetZipCode(string ZipCode)
        {
            // Validate ZipCode against some datastore.
            this.ZipCode = ZipCode;
            // Update this.City based on validated ZIP code.
        }
    }

The client would then access the Address.ZipCode value like this:

    Address addr = new Address();
    addr.SetZipCode(55555);
    string zip = addr.GetZipCode();

Defining and Using Properties

Using accessor methods works well and is a technique used by programmers of several object-oriented languages, including C++ and Java. However, C# provides an even richer mechanismpropertiesthat has the same capabilities as accessor methods and is much more elegant on the client side. Through properties, a programmer can write a client that can access a class's fields as though they are public fields without knowing whether an accessor method exists.

A C# property consists of a field declaration and accessor methods used to modify the field's value. These accessor methods are called getter and setter methods. Getter methods are used to retrieve the field's value, and setter methods are used to modify the field's value. Here's the earlier example rewritten using C# properties:

    class Address
    {
        protected string city;
        protected string zipCode;

        public string ZipCode
        {
            get
            {
                return zipCode;
            }
            set
            {
                // Validate value against some datastore.
                zipCode = value;
                // Update city based on validated zipCode.
            }
        }
    }

Notice that I created a field called Address.zipCode and a property called Address.ZipCode. This can confuse some people at first because they think Address.ZipCode is the field and wonder why it needs to be defined twice. But it's not the field. It's the property, which is simply a generic means of defining accessors to a class member so that the more intuitive object.field syntax can be used. If in this example I were to omit the Address.zipCode field and change the statement in the setter from zipCode = value to ZipCode = value, I'd cause the setter method to be called infinitely. Also notice that the setter doesn't take any arguments. The value being passed is automatically placed in a variable named value that is accessible inside the setter method. (You'll soon see in MSIL how this bit of magic happens.)

Now that we've written the Address.ZipCode property, let's look at the changes needed for the client code:

    Address addr = new Address();
    addr.ZipCode = 55555";
    string zip = addr.ZipCode;

As you can see, how a client accesses the fields is intuitive: no more guessing or looking through documentation (aka the source code) to determine whether a field is public and, if not, what the name of the accessor method is.

What the Compiler Is Really Doing

So, how does the compiler allow us to call a method in the standard object.field syntax? Also, where does that value variable come from? To answer these questions, we need to look at the MSIL being produced by the compiler. Let's consider the property's getter method first.

In our ongoing example, we have the following getter method defined:

class Address
{
    protected string city;
    protected string zipCode;
    public string ZipCode
    {
        get
        {
            return zipCode;
        }
        '
    }
    '
}

If you look at the resulting MSIL from this method, you'll see that the compiler has created an accessor method called get_ZipCode, as seen here:

.method public hidebysig specialname instance string
        get_ZipCode() cil managed
{
  // Code size       11 (0xb)
  .maxstack  1
  .locals ([0] string _Vb_t_$00000003$00000000)
  IL_0000:  ldarg.0
  IL_0001:  ldfld      string Address::zipCode
  IL_0006:  stloc.0
  IL_0007:  br.s       IL_0009
  IL_0009:  ldloc.0
  IL_000a:  ret
} // end of method Address::get_ZipCode

You can tell the name of the accessor method because the compiler prefixes the property name with get_ (for a getter method) or set_ (for a setter method). As a result, the following code resolves to a call to get_ZipCode:

String str = addr.ZipCode; // this calls Address::get_ZipCode

Therefore, you might be tempted to try the following explicit call to the accessor method yourself:

String str = addr.get_ZipCode; // **ERROR Won't compile

However, in this case, the code will not compile because it's illegal to explicitly call an internal MSIL method.

The answer to our questionhow can a compiler allow us to use the standard object.field syntax and have a method be called?is that the compiler actually generates the appropriate getter and setter methods for us when it parses the C# property syntax. Therefore, in the case of the Address.ZipCode property, the compiler emits MSIL that contains the get_ZipCode and set_ZipCode methods.

Now let's look at the generated setter method. In the Address class you saw the following:

    public string ZipCode
    {
        '
        set
        {
            // Validate value against some datastore.
            zipCode = value;
           // Update city based on validated zipCode.
        }
    }

Notice that nothing in this code declares a variable named value yet we're able to use this variable to store the caller's passed value and to set the protected zipCode member field. When the C# compiler generates the MSIL for a setter method, it injects this variable as an argument in a method called set_ZipCode.

In the generated MSIL, this method takes as an argument a string variable:

.method public hidebysig specialname instance void
        set_ZipCode(string value') cil managed
{
  // Code size       8 (0x8)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  stfld      string Address::zipCode
  IL_0007:  ret
} // end of method Address::set_ZipCode

Even though you can't see this method in the C# source code, when you set the ZipCode property with something like addr.ZipCode("12345"), it resolves to an MSIL call to Address::set_ZipCode("12345"). As with the get_ZipCode method, attempting to call this method directly in C# generates an error.

Read-Only Properties

In the example we've been using, the Address.ZipCode property is considered read/write because both a getter and a setter method are defined. Of course, sometimes you won't want the client to be able to set the value of a given field, in which case you'll make the field read-only. You do this by omitting the setter method. To illustrate a read-only property, let's prevent the client from setting the Address.city field, leaving the Address.ZipCode property as the only code path tasked with changing this field's value:

    class Address
    {
        protected string city;
        public string City
        {  
            get
            {
                return city;
            }
        }

        protected string zipCode;
        public string ZipCode
        {
            get
            {
                return zipCode;
            }
            set
            {
                // Validate value against some datastore.
                zipCode = value;
                // Update city based on validated zipCode.
            }
        }
    }

Inheriting Properties

Like methods, a property can be decorated with the virtual, override, or abstract modifiers I covered in Chapter 6, Methods. This enables a derived class to inherit and override properties just as it could any other member from the base class. The key issue here is that you can specify these modifiers only at the property level. In other words, in cases where you have both a getter method and a setter method, if you override one, you must override both.

Advanced Use of Properties

So far, I've talked about properties being useful for the following reasons:

7        They provide a level of abstraction to clients.

7        They provide a generic means of accessing class members by using the object.field syntax.

7        They enable a class to guarantee that any additional processing can be done when a particular field is modified or accessed.

The third item points us to another helpful use for properties: the implementation of something called lazy initialization. This is an optimization technique whereby some of a class's members are not initialized until they are needed.

Lazy initialization is beneficial when you have a class that contains seldom-referenced members whose initialization takes up a lot of time or chews up a lot of resources. Examples of this would be situations where the data has to be read from a database or across a congested network. Because you know these members are not referenced often and their initialization is expensive, you can delay their initialization until their getter methods are called. To illustrate this, let's say you have an inventory application that sales representatives run on their laptop computers to place customer orders and that they occasionally use to check inventory levels. Using properties, you could allow the relevant class to be instantiated without the inventory records having to be read, as shown in the code below. Then, if a sales rep did want to access the inventory count for the item, the getter method would access the remote database at that time.

    class Sku
    {
        protected double onHand;

        public string OnHand
        {
            get
            {
                // Read from central db and set onHand value.
                return onHand;
            }
        }
    }

As you've seen throughout this chapter so far, properties enable you to provide accessor methods to fields and a generic and intuitive interface for the client. Because of this, properties are sometimes referred to as smart fields. Now, let's take this a step further and look at how arrays are defined and used in C#. We'll also see how properties are used with arrays in the form of indexers.

Arrays

So far, most of the examples in this book have shown how to define variables in finite, predetermined numbers. However, in most practical applications, you don't know the exact number of objects you need until run time. For example, if you're developing an editor and you want to keep track of the controls that are added to a dialog box, the exact number of controls the editor will display can't be known until run time. You can, however, use an array to store and keep track of a dynamically allocated grouping of objectsthe controls on the editor, in this case.

In C#, arrays are objects that have the System.Array class defined as their base class. Therefore, although the syntax of defining an array looks similar to that in C++ or Java, you're actually instantiating a .NET class, which means that every declared array has all the same members inherited from System.Array. In this section, I'll cover how to declare and instantiate arrays, how to work with the different array types, and how to iterate through the elements of an array. I'll also look at some of the more commonly used properties and methods of the System.Array class.

Declaring Arrays

Declaring an array in C# is done by placing empty square brackets between the type and the variable name, like this:

int[] numbers;

Note that this syntax differs slightly from C++, in which the brackets are specified after the variable name. Because arrays are class-based, many of the same rules that apply to declaring a class also pertain to arrays. For example, when you declare an array, you're not actually creating that array. Just as you do with a class, you must instantiate the array before it exists in terms of having its elements allocated. In the following example, I'm declaring and instantiating an array at the same time:

// Declares and instantiates a single-
// dimensional array of 6 integers.
int[] numbers = new int[6];

However, when declaring the array as a member of a class, you need to declare and instantiate the array in two distinct steps because you can't instantiate an object until run time:

class YourClass
{
    '
    int[] numbers;
    '

    void SomeInitMethod()
    {
        '
        numbers = new int[6];
        '
    }
}

Single-Dimensional Array Example

Here's a simple example of declaring a single-dimensional array as a class member, instantiating and filling the array in the constructor, and then using a for loop to iterate through the array, printing out each element:

using System;

class SingleDimArrayApp
{
    protected int[] numbers;

    SingleDimArrayApp()
    {
        numbers = new int[6];
        for (int i = 0; i < 6; i++)
        {
            numbers[i] = i * i;
        }
    }

    protected void PrintArray()
    {
        for (int i = 0; i < numbers.Length; i++)
        {
            Console.WriteLine(numbers[{0}]={1}, i, numbers[i]);
        }
    }
   

    public static void Main()
    {
        SingleDimArrayApp app = new SingleDimArrayApp();
        app.PrintArray();
    }
}

Running this example produces the following output:

numbers[0]=0
numbers[1]=1
numbers[2]=4
numbers[3]=9
numbers[4]=16
numbers[5]=25

In this example, the SingleDimArray.PrintArray method uses the System.Array Length property to determine the number of elements in the array. It's not obvious here, since we have only a single-dimensional array, but the Length property actually returns the number of all the elements in all the dimensions of an array. Therefore, in the case of a two dimensional array of 5 by 4, the Length property would return 9. In the next section, I'll look at multidimensional arrays and how to determine the upper bound of a specific array dimension.

Multidimensional Arrays

In addition to single-dimensional arrays, C# supports the declaration of multidimensional arrays where each dimension of the array is separated by a comma. Here I'm declaring a three-dimensional array of doubles:

double[,,] numbers;

To quickly determine the number of dimensions in a declared C# array, count the number of commas and add one to that total.

In the following example, I have a two-dimensional array of sales figures that represent this year's year-to-date figures and last year's totals for the same time frame. Take special note of the syntax used to instantiate the array (in the MultiDimArrayApp constructor).

using System;

class MultiDimArrayApp
{
    protected int currentMonth;
    protected double[,] sales;

    MultiDimArrayApp()

    {
        currentMonth=10;

        sales = new double[2, currentMonth];
        for (int i = 0; i < sales.GetLength(0); i++)
        {
            for (int j=0; j < 10; j++)
            {
                sales[i,j] = (i * 100) + j;
            }
        }
    }

    protected void PrintSales()
    {
        for (int i = 0; i < sales.GetLength(0); i++)
        {
            for (int j=0; j < sales.GetLength(1); j++)
            {
                Console.WriteLine([{0}][{1}]={2}", i, j, sales[i,j]);
            }
        }
    }
 
    public static void Main()
    {
        MultiDimArrayApp app = new MultiDimArrayApp();
        app.PrintSales();
    }
}

Running the MultiDimArrayApp example results in this output:

[0][0]=0
[0][1]=1
[0][2]=2
[0][3]=3
[0][4]=4
[0][5]=5
[0][6]=6
[0][7]=7
[0][8]=8
[0][9]=9
[1][0]=100
[1][1]=101
[1][2]=102
[1][3]=103
[1][4]=104
[1][5]=105
[1][6]=106
[1][7]=107
[1][8]=108
[1][9]=109

Remember that in the single-dimensional array example I said the Length property will return the total number of items in the array, so in this example that return value would be 20. In the MultiDimArray.PrintSales method I used the Array.GetLength method to determine the length or upper bound of each dimension of the array. I was then able to use each specific value in the PrintSales method.

Querying for Rank

Now that we've seen how easy it is to dynamically iterate through a single- dimensional or multidimensional array, you might be wondering how to determine the number of dimensions in an array programmatically. The number of dimensions in an array is called an array's rank, and rank is retrieved using the Array.Rank property. Here's an example of doing just that on several arrays:

using System;

class RankArrayApp
{
    int[] singleD;
    int[,] doubleD;
    int[,,] tripleD;
   
    protected RankArrayApp()
    {
        singleD = new int[6];
        doubleD = new int[6,7];
        tripleD = new int[6,7,8];
    }
   
    protected void PrintRanks()
    {
        Console.WriteLine(singleD Rank = {0}, singleD.Rank);
        Console.WriteLine(doubleD Rank = {0}, doubleD.Rank);
        Console.WriteLine(tripleD Rank = {0}, tripleD.Rank);
    }

    public static void Main()
    {
        RankArrayApp app = new RankArrayApp();
        app.PrintRanks();
    }
}

As expected, the RankArrayApp application outputs the following:

singleD Rank = 1
doubleD Rank = 2
tripleD Rank = 3

Jagged Arrays

The last thing we'll look at with regard to arrays is the jagged array. A jagged array is simply an array of arrays. Here's an example of defining an array containing integer arrays:

int[][] jaggedArray;

You might use a jagged array if you were developing an editor. In this editor, you might want to store the object representing each user-created control in an array. Let's say you had an array of buttons and combo boxes (to keep the example small and manageable). You might have three buttons and two combo boxes both stored in their respective arrays. Declaring a jagged array enables you to have a parent array for those arrays so that you can easily programmatically iterate through the controls when you need to, as shown here:

using System;

class Control
{
    virtual public void SayHi()
    {
        Console.WriteLine(base control class);
    }
}

class Button : Control
{
    override public void SayHi()
    {
        Console.WriteLine(button control);
    }
}

class Combo : Control
{
    override public void SayHi()
    {
        Console.WriteLine(combobox control);
    }
}

class JaggedArrayApp
{
    public static void Main()
    {
    Control[][] controls;
    controls = new Control[2][];

    controls[0] = new Control[3];

    for (int i = 0; i < controls[0].Length; i++)
    {
        controls[0][i] = new Button();
    }

    controls[1] = new Control[2];
    for (int i = 0; i < controls[1].Length; i++)
    {
        controls[1][i] = new Combo();
    }

    for (int i = 0; i < controls.Length;i++)
    {
        for (int j=0;j< controls[i].Length;j++)
        {
            Control control = controls[i][j];
            control.SayHi();
        }
    }       

    string str = Console.ReadLine();
    }
}

As you can see, I've defined a base class (Control) and two derived classes (Button and Combo) and I've declared the jagged array as an array of arrays that contain Controls objects. That way I can store the specific types in the array and, through the magic of polymorphism, know that when it's time to extract the objects from the array (through an upcasted object) I'll get the behavior I expect.

Treating Objects Like Arrays by Using Indexers

In the Arrays section, you learned how to declare and instantiate arrays, how to work with the different array types, and how to iterate through the elements of an array. You also learned how to take advantage of some of the more commonly used properties and methods of the array types underlying the System.Array class. Let's continue working with arrays by looking at how a C#-specific feature called indexers enables you to programmatically treat objects as though they were arrays. And why would you want to treat an object like an array? Like most features of a programming language, the benefit of indexers comes down to making your applications more intuitive to write. In the first section of this chapter, Properties as Smart Fields, you saw how C# properties give you the ability to reference class fields by using the standard class.field syntax, yet they ultimately resolve to getter and setter methods. This abstraction frees the programmer writing a client for the class from having to determine whether getter/setter methods exist for the field and from having to know the exact format of these methods. Similarly, indexers enable a class's client to index into an object as though the object itself is an array.

Consider the following example. You have a list box class that needs to expose some way in which a user of that class can insert strings. If you're familiar with the Win32 SDK, you know that to insert a string into a list box window, you send it a LB_ADDSTRING or LB_INSERTSTRING message. When this mechanism appeared in the late 1980s, we thought we really were object-oriented programmers. After all, weren't we sending messages to an object just like those fancy object-oriented analysis and design books told us to? However, as object-oriented and object-based languages such as C++ and Object Pascal began to proliferate, we learned that objects could be used to create more intuitive programming interfaces for such tasks. Using C++ and MFC (Microsoft Foundation Classes), we were given an entire lattice of classes that enabled us to treat windows (such as list boxes) as objects with these classes exposing member functions that basically provided a thin wrapper for the sending and receiving of messages to and from the underlying Microsoft Windows control. In the case of the CListBox class (that is, the MFC wrapper for the Windows list box control), we were given an AddString and an InsertString member function for the tasks previously accomplished by sending the LB_ADDSTRING and LB_INSERTSTRING messages.

However, to help develop the best and most intuitive language, the C# language design team looked at this and wondered, Why not have the ability to treat an object that is at its heart an array, like an array? When you consider a list box, isn't it just an array of strings with the additional functionality of displaying and sorting? From this idea was born the concept of indexers.

Defining Indexers

Because properties are sometimes referred to as smart fields and indexers are called smart arrays, it makes sense that properties and indexers would share the same syntax. Indeed, defining indexers is much like defining properties, with two major differences: First, the indexer takes an index argument. Second, because the class itself is being used as an array, the this keyword is used as thename of the indexer. You'll see a more complete example shortly, but take a look at an example indexer:

class MyClass
{
    public object this [int idx]
    {
        get
        {
            // Return desired data.
        }
        set
        {
            // Set desired data.
        }
    }
    ...
}

I haven't shown you a full example to illustrate the syntax of indexers because the actual internal implementation of how you define your data and how you get and set that data isn't relevant to indexers. Keep in mind that regardless of how you store your data internally (that is, as an array, a collection, and so on), indexers are simply a means for the programmer instantiating the class to write code such as this:

MyClass cls = new MyClass();
cls[0] = someObject;
Console.WriteLine({0}", cls[0]);

What you do within the indexer is your own business, just as long as the class's client gets the expected results from accessing the object as an array.

Indexer Example

Let's look at some places where indexers make the most sense. I'll start with the list box example I've already used. As mentioned, from a conceptual standpoint, a list box is simply a list, or an array of strings to be displayed. In the following example, I've declared a class called MyListBox that contains an indexer to set and retrieve strings through an ArrayList object. (The ArrayList class is a .NET Framework class used to store a collection of objects.)

using System;
using System.Collections;

class MyListBox

{
    protected ArrayList data = new ArrayList();

    public object this[int idx]

    {
        get
        {
            if (idx > -1 && idx < data.Count)
            {
                return (data[idx]);
            }
            else
            {
                // Possibly throw an exception here.
                return null;
            }
        }
        set
        {
            if (idx > -1 && idx < data.Count)
            {
                data[idx] = value;
            }
            else if (idx == data.Count)
            {
                data.Add(value);
            }
            else
            {
                // Possibly throw an exception here.
            }
        }
    }
}

class Indexers1App
{
    public static void Main()
    {
        MyListBox lbx = new MyListBox();
        lbx[0] = foo";
        lbx[1] = bar";
        lbx[2] = baz";
        Console.WriteLine({0} {1} {2}",
                           lbx[0], lbx[1], lbx[2]);
    }
}Notice in this example that I check for out-of-bounds errors in the indexing of the data. This is not technically tied to indexers because, as I mentioned, indexers pertain only to how the class's client can use the object as an array and have nothing to do with the internal representation of the data. However, when learning a new language feature, it helps to see practical usage of a feature rather than only its syntax. So, in both the indexer's getter and setter methods, I validate the index value being passed with the data being stored in the class's ArrayList member. I personally would probably choose to throw exceptions in the cases where the index value being passed can't be resolved. However, that's a personal choiceyour error handling might differ. The point is that you need to indicate failure to the client in cases where an invalid index has been passed.

Design Guidelines

Indexers are yet another example of the C# design team adding a subtle yet powerful feature to the language to help us become more productive in our development endeavors. However, like any feature of any language, indexers have their place. They should be used only where it would be intuitive to treat an object like an array. Let's take as an example the case of invoicing. It's reasonable that an invoicing application has an Invoice class that defines a member array of InvoiceDetail objects. In such a case, it would be perfectly intuitive for the user to access these detail lines with the following syntax:

InvoiceDetail detail = invoice[2]; // Retrieves the 3rd detail line.

However, it wouldn't be intuitive to take that a step further and try to turn all of the InvoiceDetail members into an array that would be accessed via an indexer. As you can see here, the first line is much more readily understood than the second:

TermCode terms = invoice.Terms; // Property accessor to Terms member.
TermCode terms = invoice[3];    // A solution in search of a problem.

In this case, the maxim holds true that just because you can do something doesn't mean you should necessarily do it. Or, in more concrete terms, think of how implementing any new feature is going to affect your class's clients, and let that thinking guide you when you're deciding whether implementing the feature will make the use of your class easier.

Summary

C# properties consist of field declaration and accessor methods. Properties enable smart access to class fields so that a programmer writing a client for the class doesn't have to try to determine whether (and how) an accessor method for the field was created. Arrays in C# are declared by placing an empty square bracket between the type and the variable name, a syntax slightly different than the one used in C++. C# arrays can be single-dimensional, multidimensional, or jagged. Objects in C# can be treated like arrays through the use of indexers. Indexers allow programmers to easily work with and track many objects of the same type.


To purchase a copy of this book, click HERE.



Comments

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds