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 choice—your 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.
3 4
8
Attributes
Most programming languages are designed with a given set of abilities in mind. For example, when you set out to design a compiler, you think about how an application written in the new language will be structured, how code will call other code, how functionality will be packaged, and many other issues that will make the language a productive
medium for developing software. Most of what a compiler designer comes up with is static. For example, in C#, you define a class by placing the keyword class before the class name. You then signify derivation by inserting a colon after the class name followed by the name of the base class. This is an example of a decision that, once made by the
language designer, can't be changed.
Now, the people who write compilers are darn good at what they do. However, even they can't anticipate all the future developments in our industry and how those developments will alter how programmers want to express their types in a given language. For example, how do you create the relationship between a class in C++ and a documentation URL for that class? Or how do you associate the specific members of a C++ class with the XML fields for your company's new business-to-business solution? Because C++ was designed many years before the advent of the Internet and protocols such as XML, it's not easy to perform either of these tasks.
Until now, the solutions to problems like these involved storing extra information in a separate file (DEF, IDL, and so on) that was then loosely associated with the type or member in question. Because the compiler has no knowledge of the separate file or the code-generated relationship between your class and the file, this approach is usually called a
"disconnected solution." The main problem is that the class is no longer "self-describing"—that is, a user can no longer look at the class definition by itself and know everything about that class. One advantage of a self-describing
component is that the compiler and run time can ensure that the rules associated with the component are adhered to.
Additionally, a self-describing component is easier to maintain because the developer can see all the information related to the component in one place.
This has been the way of the world for many decades of compiler evolution. The language designers try to determine what you'll need the language to do, they design the compiler with those capabilities, and, for better or worse, those are the capabilities you have until another compiler comes along. That is, until now. C# offers a different paradigm, which stems from the introduction of a feature called attributes.