CHAPTER 16

Handling binary data and using indexers

After completing this chapter, you will be able to:

  • Store and display integer data using binary and hexadecimal representations.

  • Perform bitwise operations in binary data.

  • Encapsulate logical array-like access to an object by using indexers.

  • Control read access to indexers by declaring get accessors.

  • Control write access to indexers by declaring set accessors.

  • Create interfaces that declare indexers.

  • Implement indexers in structures and classes that inherit from interfaces.

Chapter 15, “Implementing properties to access fields,” describes how to implement and use properties as a means of providing controlled access to the fields in a class. Properties are useful for mirroring fields that contain a single value. However, indexers are invaluable if you want to provide access to items that contain multiple values, and to do so by using a natural and familiar syntax.

What is an indexer?

You can think of an indexer as a smart array, in much the same way that you can think of a property as a smart field. Whereas a property encapsulates a single value in a class, an indexer encapsulates a set of values. The syntax that you use for an indexer is the same as the syntax that you use for an array.

The best way to understand indexers is to work through an example. First, you’ll consider a problem and examine a solution that doesn’t use indexers. Then you’ll work through the same problem and look at a better solution that does use indexers. The problem concerns integers, or more precisely, the int type. The example uses C# integers to store and query data stored as binary data, so, it helps to have an understanding of how you can use the integer types in C# to store and manipulate binary values. We will discuss this first.

Storing binary values

You normally use an int to hold an integer value. Internally, an int stores its value as a sequence of 32 bits, where each bit can be either 0 or 1. Most of the time, you don’t care about this internal binary representation; you just use an int type as a container that holds an integer value. Sometimes, however, programmers use the int type for other purposes—some programs use an int as a set of binary flags and manipulate the individual bits within an int. If you are an old C hack like I am, what follows should have a very familiar feel.

Images

Note Some older programs used int types to save memory. Such programs typically date from when the size of computer memory was measured in kilobytes rather than the gigabytes available these days, and memory was at an absolute premium. A single int holds 32 bits, each of which can be 1 or 0. In some cases, programmers assigned 1 to indicate the value true and 0 to indicate false and then employed an int as a set of Boolean values.

To make life a little easier for handling data that you want to treat as a collection of binary values,  C# enables you to specify integer constants using binary notation. You indicate that a constant should be treated as a binary representation by prefixing it with 0b0. For example, the following code assigns the binary value 1111 (15 in decimal) to a variable:

uint binData = 0b01111;

Note that this is a 4-bit value, but an integer occupies 32 bits; any bits not specified are initialized to zero. You should also observe that when you specify an integer as a binary value, it is good practice to store the result as an unsigned int (uint). In fact, if you provide a full 32-bit binary value, the C# compiler will insist that you use a uint.

To help cope with long strings of bits, you can also insert the “_” character as a separator between blocks of digits, like this:

uint moreBinData = 0b0_11110000_01011010_11001100_00001111;

In this example, the “_” separator is used to mark the byte boundaries (32 bits is 4 bytes). You can use the “_” separator anywhere within a binary constant (not just on byte boundaries); it is ignored by the C# compiler and is provided simply to help improve the readability of your code.

If you find binary strings a little lengthy, you can opt to specify values using hexadecimal (base 16) notation by using the 0x0 prefix. The following two statements assign the same values shown in the previous example to another pair of variables. Again, you can use the “_” character as a separator to make the values easier to read:

uint hexData = 0x0_0F;
uint moreHexData = 0x0_F0_5A_CC_0F;

Displaying binary values

If you need to display the binary representation of an integer, you can use the Convert.ToString method. Convert.ToString is a heavily overloaded method that can generate a string representation of a range of data values held in different types. If you are converting integer data, you can additionally specify a numeric base (2, 8, 10, or 16), and the method will convert the integer to that base using an algorithm a little like the exercises you have seen in some earlier chapters. The following example prints out the binary value of the moreHexData variable:

uint moreHexData = 0x0_F0_5A_CC_0F;
Console.WriteLine($"{Convert.ToString(moreHexData, 2)}");
// displays 11110000010110101100110000001111

Manipulating binary values

C# provides a set of operators that you can use to access and manipulate the individual bits in an uint. These operators are as follows:

  • The NOT (~) operator This is a unary operator that performs a bitwise complement. For example, if you take the 8-bit value 0b0_11001100 (204 decimal) and apply the ~ operator to it, you obtain the result 0b0_00110011 (51 decimal); all the 1s in the original value become 0s, and all the 0s become 1s.

    Images

    Note The examples shown here are purely illustrative and are accurate only to 8 bits. In C#, the int type is 32 bits, so if you try any of these examples in a C# application, you will get a 32-bit result that might be different from those shown in this list. For example, in 32 bits,  204 is 0b0_00000000_00000000_00000000_11001100, so in C#, ~204 is 0b0_11111111_11111111_ 11111111_00110011 (which is actually the int representation of –205 in C#).

  • The left-shift (<<) operator This is a binary operator that performs a left shift. The 8-bit expression 204 << 2 returns the value 48. (In binary, 204 decimal is 0b0_11001100, and shifting it left by two places yields 0b0_00110000, or 48 decimal.) The far-left bits are discarded, and zeros are introduced from the right. There is a corresponding right-shift operator (>>).

  • The OR (|) operator This is a binary operator that performs a bitwise OR operation, returning a value containing a 1 in each position in which either of the operands has a 1. For example, the 8-bit expression 204 | 24 has the value 220 (204 is 0b0_11001100, 24 is 0b0_00011000, and 220 is 0b0_11011100).

  • The AND (&) operator This operator performs a bitwise AND operation. AND is similar to the bitwise OR operator, but it returns a value containing a 1 in each position where both of the operands have a 1. So, the 8-bit expression 204 & 24 is 8 (204 is 0b0_11001100, 24 is 0b0_00011000, and 8 is 0b0_00001000).

  • The XOR (^) operator This operator performs a bitwise exclusive OR operation, returning a 1 in each bit where there is a 1 in one operand or the other but not both. (Two 1s yield a 0; this is the “exclusive” part of the operator.) So the 8-bit expression 204 ^ 24 is 212 (0b0_11001100 ^ 0b0_00011000 is 0b0_11010100).

You can use these operators together to determine the values of the individual bits in an int. As an example, the following expression uses the left-shift (<<) and bitwise AND (&) operators to determine whether the sixth bit from the right of the byte variable named bits is set to 0 or to 1:

(bits & (1 << 5)) != 0

Images

Note The bitwise operators count the positions of bits from right to left, and the bits are numbered starting at 0. So, bit 0 is the rightmost bit, and the bit at position 5 is the bit six places from the right.

Suppose that the bits variable contains the decimal value 42. In binary, this is 0b0_00101010. The decimal value 1 is 0b0_00000001 in binary, and the expression 1 << 5 has the value 0b0_00100000; the sixth bit is 1. In binary, the expression bits & (1 << 5) is 0b0_00101010 & 0b0_00100000, and the value of this expression is 0b0_00100000, which is nonzero. If the variable bits contains the value 65, or 0b0_01000001, the value of the expression is 0b0_01000001 & 0b0_00100000, which yields the result 0b0_00000000, or zero.

This is a fairly complicated example, but it’s trivial in comparison to the following expression, which uses the compound assignment operator &= to set the bit at position 6 to 0:

bits &= ~(1 << 5)

Similarly, if you want to set the bit at position 6 to 1, you can use the bitwise OR (|) operator. The following complicated expression is based on the compound assignment operator |=:

bits |= (1 << 5)

The trouble with these examples is that although they work, they are fiendishly difficult to understand. They’re complicated, and the solution is a very low-level one: it fails to create an abstraction of the problem that it solves, and it is consequently very difficult to maintain code that performs these kinds of operations.

Solving the same problems using indexers

Let’s pull back from the preceding low-level solution for a moment and remember what the problem is. You’d like to use an int not as an int but as an array of bits. Therefore, the best way to solve this problem is to use an int as if it were an array of bits; in other words, what you’d like to be able to write in order to access the bit six places from the right in the bits variable is an expression such as the following (remember that arrays start with index 0):

bits[5]

And, to set the bit four places from the right to true, you’d like to be able to write this:

bits[3] = true

Images

Note To seasoned C developers, the Boolean value true is synonymous with the binary value 1, and the Boolean value false is synonymous with the binary value 0. Consequently, the expression bits[3] = true means “Set the bit four places from the right of the bits variable to 1.”

Unfortunately, you can’t use the square bracket notation on an int; it works only on an array or on a type that behaves like an array. So the solution to the problem is to create a new type that acts like, feels like, and is used like an array of bool variables but is implemented by using an int. You can achieve this feat by defining an indexer. Let’s call this new type IntBits. IntBits will contain an int value (initialized in its constructor), but the idea is that you’ll use IntBits as an array of bool variables.

Images

Tip The IntBits type is small and lightweight, so it makes sense to create it as a structure rather than as a class.

struct IntBits
{
    private int bits;

    // Simple constructor, implemented as an expression-bodied method
    public IntBits(int initialBitValue) => bits = initialBitValue;
    
    // indexer to be written here
}

To define the indexer, you use a notation that is a cross between a property and an array. You introduce the indexer with the this keyword, specify the type of the value returned by the indexer, and also specify the type of the value to use as the index into the indexer between square brackets. The indexer for the IntBits struct uses an integer as its index type and returns a Boolean value. It looks like this:

struct IntBits
{
    ...
    public bool this [ int index ]
    {
      get => (bits & (1 << index)) != 0;
      
      set
      {
        if (value) // turn the bit on if value is true; otherwise, turn it off bits |=  (1 << index);
        else
          bits &= ~(1 << index);
      }
    }
}

Notice the following points:

  • An indexer is not a method; there are no parentheses containing a parameter, but there are square brackets that specify an index. This index is used to specify which element is being accessed.

  • All indexers use the this keyword. A class or structure can define at most one indexer (although you can overload it and have several implementations), and it is always named this.

  • Indexers contain get and set accessors just like properties. In this example, the get and set accessors contain the complicated bitwise expressions previously discussed.

  • The index specified in the indexer declaration is populated with the index value specified when the indexer is called. The get and set accessor methods can read this argument to determine which element should be accessed.

Images

Note You should perform a range check on the index value in the indexer to prevent any unexpected exceptions from occurring in your indexer code.

It is also good practice to provide a way to display the data in this structure. You can do this by overriding the ToString method and converting the value held in the structure to a string containing its binary representation, like this:

struct IntBits
{
    ...
    public override string ToString()
    {
        return (Convert.ToString(bits, 2);
    }
}

After you have created the indexer, you can use a variable of type IntBits instead of an int and apply the square bracket notation, as shown in the next example:

int adapted = 0b0_01111110;
IntBits bits = new IntBits(adapted);
bool peek = bits[6]; // retrieve bool at index 6; should be true (1)
bits[0] = true; // set the bit at index 0 to true (1)
bits[3] = false; // set the bit at index 3 to false (0)
Console.WriteLine($""); // displays 1110111 (0b0_01110111)

This syntax is certainly much easier to understand. It directly and succinctly captures the essence of the problem.

Understanding indexer accessors

When you read an indexer, the compiler automatically translates your array-like code into a call to the get accessor of that indexer. Consider the following example:

bool peek = bits[6];

This statement is converted to a call to the get accessor for bits, and the index argument is set to 6.

Similarly, if you write to an indexer, the compiler automatically translates your array-like code into a call to the set accessor of that indexer, setting the index argument to the value enclosed in the square brackets, such as illustrated here:

bits[3] = true;

This statement is converted to a call to the set accessor for bits where index is 3. As with ordinary properties, the data you are writing to the indexer (in this case, true) is made available inside the set accessor by using the value keyword. The type of value is the same as the type of the indexer itself (in this case, bool).

It’s also possible to use an indexer in a combined read/write context. In this case, both the get and set accessors are used. Look at the following statement, which uses the XOR operator (^) to invert the value of the bit at index 6 in the bits variable:

bits[6] ^= true;

This code is automatically translated into the following:

bits[6] = bits[6] ^ true;

This code works because the indexer declares both a get and a set accessor.

Images

Note You can declare an indexer that contains only a get accessor (a read-only indexer) or only a set accessor (a write-only indexer).

Comparing indexers and arrays

When you use an indexer, the syntax is deliberately very array-like. However, there are some important differences between indexers and arrays:

  • Indexers can use nonnumeric subscripts, such as a string (as shown in the following example), whereas arrays can use only integer subscripts.

    public int this [ string name ] { ... } // OK

  • Indexers can be overloaded (just like methods), whereas arrays cannot.

    public Name this [ PhoneNumber number ] { ... }
    public PhoneNumber this [ Name name ] { ... }

  • Indexers cannot be used as ref or out parameters, whereas array elements can.

    IntBits bits; // bits contains an indexer
    Method(ref bits[1]); // compile-time error

Properties, arrays, and indexers

It is possible for a property to return an array, but remember that arrays are reference types, so exposing an array as a property creates the possibility of accidentally overwriting a lot of data. Look at the following structure that exposes an array property named Data:

struct Wrapper
{
    private int[] data;
    ...
    public int[] Data
    {
      get => this.data;
      set => this.data = value;
    }
}

Now consider the following code that uses this property:

Wrapper wrap = new Wrapper();
...
int[] myData = wrap.Data;
myData[0]++;
myData[1]++;

This looks pretty innocuous. However, because arrays are reference types, the variable myData refers to the same object as the private data variable in the Wrapper structure. Any changes you make to elements in myData are made to the data array; the expression myData[0]++ has the very same effect as data[0]++. If this is not your intention, you should use the Clone method in the get and set accessors of the Data property to return a copy of the data array, or make a copy of the value being set, as shown in the code that follows. (Chapter 8, “Understanding values and references,” discusses the Clone method for copying arrays.) Notice that the Clone method returns an object, which you must cast to an integer array.

struct Wrapper
{
    private int[] data;
    ...
    public int[] Data
    {
      get { return this.data.Clone() as int[]; }
      set { this.data = value.Clone() as int[]; }
    }
}

However, this approach can become very messy and expensive in terms of memory use. Indexers provide a natural solution to this problem—don’t expose the entire array as a property; just make its individual elements available through an indexer:

struct Wrapper
{
    private int[] data;
    ...
    public int this [int i]
    {
        get => this.data[i];
        set => this.data[i] = value;
    }
}

The following code uses the indexer in a similar manner to the property shown earlier:

Wrapper wrap = new Wrapper();
...
int[] myData = new int[2];
myData[0] = wrap[0];
myData[1] = wrap[1];
myData[0]++;
myData[1]++;

This time, incrementing the values in the myData array has no effect on the original array in the Wrapper object. If you really want to modify the data in the Wrapper object, you must write statements such as this:

wrap[0]++;

This is much clearer and safer!

Indexers in interfaces

You can declare indexers in an interface. To do this, specify the get keyword, the set keyword, or both, but replace the body of the get or set accessor with a semicolon. Any class or structure that implements the interface must implement the indexer accessors declared in the interface, as demonstrated here:

interface IRawInt
{
    bool this [ int index ] { get; set; }
}
struct RawInt : IRawInt
{
    ...
    public bool this [ int index ]
    {
        get { ... }
        set { ... }
    }
    ...
}

If you implement the interface indexer in a class, you can declare the indexer implementations as virtual. This allows further derived classes to override the get and set accessors, such as in the following:

class RawInt : IRawInt
{
        ...
        public virtual bool this [ int index ]
        {
          get { ... }
          set { ... }
        }
        ...
}

You can also choose to implement an indexer by using the explicit interface implementation syntax covered in Chapter 13, “Creating interfaces and defining abstract classes.” An explicit implementation of an indexer is nonpublic and nonvirtual (and so cannot be overridden), as shown in this example:

struct RawInt : IRawInt
{
    ...
    bool IRawInt.this [ int index ]
    {
        get { ... }
        set { ... }
    }
    ...
}

Using indexers in a Windows application

In the following exercise, you will examine a simple phone book application and complete its implementation. You will write two indexers in the PhoneBook class: one that accepts a Name parameter and returns a PhoneNumber, and another that accepts a PhoneNumber parameter and returns a Name. (The Name and PhoneNumber structures have already been written.) You will also need to call these indexers from the correct places in the program.

Familiarize yourself with the application

  1. Start Microsoft Visual Studio 2017 if it is not already running.

  2. Open the Indexers solution, which is located in the Microsoft PressVCSBSChapter 16Indexers folder in your Documents folder.

    With this graphical application, a user can search for the telephone number for a contact, and also find the name of a contact that matches a given telephone number.

  3. On the Debug menu, click Start Debugging.

    The project builds and runs. A form appears, displaying two empty text boxes labeled Name and Phone Number. The form initially displays two buttons: one to find a phone number when given a name, and one to find a name when given a phone number. Expanding the command bar at the bottom of the form reveals an additional Add button that will add a name/phone number pair to a list of names and phone numbers held by the application. All buttons (including the Add button in the command bar) currently do nothing. The application looks like this:

    The Indexers application is running. The user has expended the command bar at the bottom of the form. None of the buttons are functional yet.

    Your task is to complete the application so that the buttons work.

  4. Return to Visual Studio 2017 and stop debugging.

  5. Display the Name.cs file for the Indexers project in the Code and Text Editor window. Examine the Name structure. Its purpose is to act as a holder for names.

    The name is provided as a string to the constructor. The name can be retrieved by using the read-only string property named Text. (The Equals and GetHashCode methods are used for comparing Names when searching through an array of Name values—you can ignore them for now.)

  6. Display the PhoneNumber.cs file in the Code and Text Editor window, and examine the PhoneNumber structure. It is similar to the Name structure.

  7. Display the PhoneBook.cs file in the Code and Text Editor window and examine the PhoneBook class.

    This class contains two private arrays: an array of Name values called names, and an array of PhoneNumber values called phoneNumbers. The PhoneBook class also contains an Add method that adds a phone number and name to the phone book. This method is called when the user clicks the Add button on the form. The enlargeIfFull method is called by Add to check whether the arrays are full when the user adds another entry. This method creates two new, bigger arrays, copies the contents of the existing arrays to them, and then discards the old arrays.

    The Add method is deliberately kept simple and does not check whether a name or phone number has already been added to the phone book.

    The PhoneBook class does not currently provide any functionality with which a user can find a name or telephone number; you will add two indexers to provide this facility in the next exercise.

Write the indexers

  1. In the PhoneBook.cs file, delete the comment // TODO: write 1st indexer here and replace it with a public read-only indexer for the PhoneBook class, as shown in bold in the code that follows. The indexer should return a Name and take a PhoneNumber item as its index. Leave the body of the get accessor blank.

    The indexer should look like this:

    sealed class PhoneBook
    {
        ...
        public Name this[PhoneNumber number]
        {
            get
            {
            }
        }
        ...
    }

  2. Implement the get accessor as shown in bold in the code that follows.

    The purpose of the accessor is to find the name that matches the specified phone number. To do this, you need to call the static IndexOf method of the Array class. The IndexOf method performs a search through an array, returning the index of the first item in the array that matches the specified value. The first argument to IndexOf is the array to search through (phoneNumbers). The second argument to IndexOf is the item for which you are searching. IndexOf returns the integer index of the element if it finds it; otherwise, IndexOf returns –1. If the indexer finds the phone number, it should return the corresponding name. Otherwise, it should return an empty Name value. (Note that Name is a structure, so the default constructor sets its private name field to null.)

    sealed class PhoneBook
    {
        ...
        public Name this [PhoneNumber number]
        {
            get
            {
                int i = Array.IndexOf(this.phoneNumbers, number);
                if (i != -1)
                {
                    return this.names[i];
                }
                else
                {
                    return new Name();
                }
            }
        }
        ...
    }

  3. Remove the comment // TODO: write 2nd indexer here and replace it with a second public read-only indexer for the PhoneBook class that returns a PhoneNumber and accepts a single Name parameter. Implement this indexer in the same way as the first one, as shown in bold in the code that follows. (Again, note that PhoneNumber is a structure and therefore always has a default constructor.)

    The second indexer should look like this:

    sealed class PhoneBook
    {
        ...
        public PhoneNumber this [Name name]
        {
            get
            {
                int i = Array.IndexOf(this.names, name);
                if (i != -1)
                {
                    return this.phoneNumbers[i];
                }
                else
                {
                    return new PhoneNumber();
                }
            }
        }
        ...
    }

    Notice that these overloaded indexers can coexist because the values that they index are of different types, which means that their signatures are different. If the Name and PhoneNumber structures were replaced by simple strings (which they wrap), the overloads would have the same signature, and the class would not compile.

  4. On the Build menu, click Build Solution, correct any syntax errors, and then rebuild the solution if necessary.

Call the indexers

  1. Display the MainPage.xaml.cs file in the Code and Text Editor window and then locate the findByNameClick method.

    This method is called when the Find By Name button is clicked. This method is currently empty. Replace the // TODO: comment with the code shown in bold in the example that follows. This code performs these tasks:

    a. Reads the value of the Text property from the name text box on the form. This is a string containing the contact name that the user has typed in.

    b. If the string is not empty, the code searches for the phone number corresponding to that name in the PhoneBook by using the indexer. (Notice that the MainPage class contains a private PhoneBook field named phoneBook.) It constructs a Name object from the string, and passes it as the parameter to the PhoneBook indexer.

    c. If the Text property of the PhoneNumber structure returned by the indexer is not null or empty, the code writes the value of this property to the phoneNumber text box on the form; otherwise, it displays the text “Not Found.”

    The completed findByNameClick method should look like this:

    private void findByNameClick(object sender, RoutedEventArgs e)
    {
        string text = name.Text;
        if (!String.IsNullOrEmpty(text))
        {
            Name personsName = new Name(text);
            PhoneNumber personsPhoneNumber = this.phoneBook[personsName];
            phoneNumber.Text =
                String.IsNullOrEmpty(personsPhoneNumber.Text) ?
                "Not Found" : personsPhoneNumber.Text;
        }
    }

    Other than the statement that accesses the indexer, there are two further points of interest in this code:

    • The static String method IsNullOrEmpty is used to determine whether a string is empty or contains a null value. This is the preferred method for testing whether a string contains a value. It returns true if the string contains a null value or it is an empty string; otherwise, it returns false.

    • The ? : operator used by the statement that populates the Text property of the phone- Number text box on the form. Remember that this operator acts like an inline if…else statement for an expression. In the preceding code, if the expression String.IsNullOrEmpty (personsPhoneNumber.Text) is true, no matching entry was found in the phone book and the text “Not Found” is displayed on the form; otherwise, the value held in the Text property of the personsPhoneNumber variable is displayed.

  2. Locate the findByPhoneNumberClick method in the MainPage.xaml.cs file. It is below the findByNameClick method.

    The findByPhoneNumberClick method is called when the Find By Phone Number button is clicked. This method is currently empty apart from a // TODO: comment. You need to implement it as follows (the completed code is shown in bold in the example that follows):

    a. Read the value of the Text property from the phoneNumber box on the form. This is a string containing the phone number that the user has typed.

    b. If the string is not empty, use the indexer to search for the name corresponding to that phone number in the PhoneBook.

    c. Write the Text property of the Name structure returned by the indexer to the name box on the form.

    The completed method should look like this:

    private void findByPhoneNumberClick(object sender, RoutedEventArgs e)
    {
        string text = phoneNumber.Text;
        if (!String.IsNullOrEmpty(text))
        {
            PhoneNumber personsPhoneNumber = new PhoneNumber(text);
            Name personsName = this.phoneBook[personsPhoneNumber];
            name.Text = String.IsNullOrEmpty(personsName.Text) ?
              "Not Found" : personsName.Text;
        }
    }

  3. On the Build menu, click Build Solution, and then correct any errors that occur.

Test the application

  1. On the Debug menu, click Start Debugging.

  2. Type your name and phone number in the appropriate boxes, and then expand the command bar and click Add. (You can expand the command bar by clicking the ellipsis.)

    When you click the Add button, the Add method stores the information in the phone book and clears the text boxes so that they are ready to perform a search.

  3. Repeat step 2 several times with some different names and phone numbers so that the phone book contains a selection of entries. Note that the application performs no checking of the names and telephone numbers that you enter, and you can input the same name and telephone number more than once. For the purposes of this demonstration, to avoid confusion, be sure that you provide different names and telephone numbers.

  4. Type a name that you used in step 3 into the Name box, and then click Find By Name.

    The phone number you added for this contact in step 3 is retrieved from the phone book and is displayed in the Phone Number text box.

  5. Type a phone number for a different contact in the Phone Number box, and then click Find By Phone Number.

    The contact name is retrieved from the phone book and is displayed in the Name box.

  6. Type a name that you did not enter in the phone book into the Name box, and then click Find By Name.

    This time, the Phone Number box displays the message “Not Found.”

  7. Close the form, and return to Visual Studio 2017.

Summary

In this chapter, you saw how to use indexers to provide array-like access to data in a class. You learned how to create indexers that can take an index and return the corresponding value by using logic defined by the get accessor, and you saw how to use the set accessor with an index to populate a value in an indexer.

  • If you want to continue to the next chapter, keep Visual Studio 2017 running and turn to  Chapter 17, “Introducing generics.”

  • If you want to exit Visual Studio 2017 now, on the File menu, click Exit. If you see a Save dialog box, click Yes and save the project.

Quick reference

To

Do this

Specify an integer value using binary or hexadecimal notation

Use the 0b0 (for binary values) or 0x0 (for hexadecimal values) prefixes. Include “_” separators to make values easier to read. For example:

uint moreBinData = 0b0_11110000_01011010_11001100_ 00001111;
uint moreHexData = 0x0_F0_5A_CC_0F;

Display an integer value as its binary or hexadecimal representation

Use the Convert.ToString method, and specify 2 (for binary) or 16 (for hexadecimal) as the number base. For example:

uint moreHexData = 0x0_F0_5A_CC_0F;
Console.WriteLine($"{Convert.ToString(moreHexData, 2)}");
// displays 11110000010110101100110000001111

Create an indexer for a class or structure

Declare the type of the indexer, followed by the keyword this, and then the indexer arguments in square brackets. The body of the indexer can contain a get and/or set accessor. For example:

struct RawInt
{
  ...
  public bool this [ int index ]
  {
      get { ... }
      set { ... }
  }
  ...
}

Define an indexer in an interface

Define an indexer with the get and/or set keywords. For example:

interface IRawInt
{
    bool this [ int index ] { get; set; }
}

Implement an interface indexer in a class or structure

In the class or structure that implements the interface, define the indexer and implement the accessors. For example:

struct RawInt : IRawInt
{
    ...
    public bool this [ int index ]
    {
      get { ... }
      set { ... }
    }
    ...
}

Implement an indexer defined by an interface by using explicit interface implementation in a class or structure

In the class or structure that implements the interface, specify the interface but do not specify the indexer accessibility. For example:

struct RawInt : IRawInt
{
    ...
    bool IRawInt.this [ int index ]
    {
      get { ... }
      set { ... }
    }
    ...
}

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.155.100