Chapter 13. Properties

After completing this chapter, you will be able to:

  • Describe what properties are.

  • Explain how properties are supported by C++/CLI.

  • Implement properties.

Properties have been available in some programming languages—such as Microsoft Visual Basic—for some time, but the Microsoft .NET Framework has added support for them into Microsoft Intermediate Language (MSIL) so that they can be easily implemented in any .NET programming language. You’ll see in this chapter that properties can often lead to a more natural style of programming without sacrificing robustness or violating the principles of object-oriented programming.

What are properties?

It is a long-accepted principle of object-oriented programming that it’s a bad idea to give users direct access to the data members that make up your classes. There are two main reasons for this:

  • If users directly access data members, they’re required to know about the implementation of the class, and that might limit your ability to modify the implementation later.

  • Users of your classes might accidentally—or deliberately—corrupt the data in objects by using inappropriate values, possibly leading to application failures or other undesirable results.

As a result, it’s recommended that you hide data members, making them private and giving indirect access to them by using member functions. In traditional C++, indirect access has often been implemented by using get and set members. Thus, a data member named date might be accessed using a pair of member functions named set_date and get_date. This method works fine, but client code always has to call the get and set functions directly.

Properties in the .NET Framework give you a way to implement a virtual data member for a class. You implement the get and set parts of the property, and the compiler converts them into calls to the get or set method as appropriate.

MyClass ^pmc = gcnew MyClass();
pmc->Name = "fred";         // calls the setter
s = pmc->Name;              // calls the getter

It appears to the user that MyClass has a real data member called Name, and the property can be used in exactly the same way as a real data member.

Anyone who programmed in Visual Basic would find the idea of implementing properties using the get, set, and let methods familiar. In the .NET Framework, properties can be created and used in any .NET language, so you can create a class in Visual Basic and still use its properties in a C++ application, and vice versa.

The two kinds of properties

C++/CLI supports two kinds of properties: scalar and indexed.

A scalar property gives access to a single value by using getter and setter code. For example, a Name property would implement getter and setter code to give access to the underlying name data. It’s important to note that a property doesn’t have to represent a simple data member of the managed class; a property can represent derived values. For example, if a class has a date-of-birth member, it would be possible to implement a property that calculates the age. Properties can also represent far more complex values, which might involve using data from other sources, such as searching databases or accessing URLs.

An indexed property makes it possible for a property to be accessed as if it were an array, using the traditional C++ square bracket notation.

Note

If you’ve ever come across the overloaded [ ] operator in traditional C++, you’ll find that indexed properties provide similar functionality, but you don’t have to code the operator overload yourself.

Indexed properties are also implemented by using getter and setter code, and the compiler automatically generates the required code so that clients can use the square bracket notation.

The next sections in this chapter demonstrate how to implement both scalar and indexed properties.

Implementing scalar properties

As mentioned in the previous section, a scalar property is one that gives you access to a single data member by using getter and setter code. The following exercise shows you how to implement scalar properties. In this example, we’ll use a simple Person class containing name and age members.

  1. Start Microsoft Visual Studio 2012 and create a new CLR Console Application project named Properties.

  2. Add the following class definition after the using namespace System; line and before the main function:

    ref class Person
    {
        String ^name;
        int age;
    public:
        // Person class constructor
        Person()
        {
            Name = "";
            Age = 0;
        }
    
        // The Name property
        property String ^Name
        {
            String ^get() { return name; }
            void set(String ^n) { name = n; }
        }
    
        // The Age property
        property int Age
        {
            int get() { return age; }
            void set(int val) { age = val; }
        }
    };

    The class has two private data members that hold the name and age of the person. Properties are introduced by the property keyword, which is followed by a type and then the property name. It is convention to begin property names with a capital letter.

    The getter and setter are declared inside the property and look a lot like nested functions. The getter is always called get and has a return type that matches the property type. The setter is called set, takes an argument of property type, and has a return type of void.

    You can use the property from C++ code as if it were a real data member of the class. Note how the properties are used in the constructor in preference to using the data members directly; you will see why this is a good idea shortly.

    Note

    The property names in this example are the same as the names of the underlying data members, but capitalized. It is a widespread convention in C# code that function and property names are capitalized. Therefore, to fit into the .NET world, it is a good idea if your property names are capitalized, as well.

  3. Add the following code to main to test the property:

    int main(array<String ^> ^args)
    {
        // Create a Person object
        Person ^p = gcnew Person();
    
        // Set the name and age using properties
        p->Name = "fred";
        p->Age = 77;
    
        // Access the properties
        Console::WriteLine("Age of {0} is {1}", p->Name, p->Age);
        return 0;
    }

    After a Person object has been created and initialized, the name and age members can be accessed through the Name and Age virtual data members that have been generated by the compiler.

  4. Build and run the application.

Errors in properties

What happens if a property get or set method encounters an error? Consider the following code:

// Set the name and age using properties
p->Name = "spiro";
p->Age = -31;

How can the Age property communicate that it isn’t happy with a negative value? This situation is a good one in which to use exceptions, which are discussed in Chapter 11. You could modify the setter function to check its argument like this:

void set(int val)
{
    if (val < 0)
        throw gcnew ArgumentException("Negative ages aren't allowed");
    age = val;
}

If anyone tries to set the age to a negative value, an ArgumentException will be thrown to alert the caller that there is a problem.

Auto-implemented properties

Many properties simply assign to and return a data member, as shown in the following:

String ^name;

property String ^Name
{
    String ^get { return name; }
    void set(String ^n) { name = n; }
}

When that is the case, you can get the compiler to implement the getter and setter, and it will generate a backing variable to store the data. You don’t see this variable, but you access it indirectly through the property getter and setter.

This means that you can implement the Name property very simply, as demonstrated here:

property String ^Name;

In the next short exercise, you can declare and use an auto-implemented property in your Person class.

  1. Modify your Person class, providing an automatic implementation for the Name property and removing the data member.

  2. Build and run the application, which should work exactly the same as before.

    Because you used the property in the constructor rather than assigning to the data member, changing to an auto-implemented property still works.

Whenever you use auto-implemented properties, you must use the property within your class when assigning to or reading the value because you don’t know the name of the backing variable that the compiler creates.

Read-only and write-only properties

You don’t always have to provide get and set methods for a property. If you don’t provide a set method, you end up with a read-only property. If you omit the get method, you’ll have a write-only property (which is possible, but a lot less common than the read-only variety).

The following exercise shows how to implement a read-only property, and it also illustrates how to create a derived property. You’ll change the Person class from the previous exercise so that it includes a date of birth rather than an age. The derived Age property will then calculate the person’s age from the date of birth; it’s obviously a derived property because you can’t change someone’s age without changing his or her date of birth, as well. It’s also obviously a read-only property because it’s always calculated and cannot be set by users of the class.

  1. Either start a new CLR Console Application project or modify the one from the previous exercise.

  2. Type or edit the definition of the Person class so that it looks like the following code. Place it after the using namespace System; line and before the main method.

    ref class Person
    {
        int dd, mm, yyyy;
    
    public:
        // Person class constructor
        Person(String ^n, int d, int m, int y)
        {
            Name = n;
            dd = d; mm = m; yyyy = y;
        }
    
        // Auto implementation of the Name property
        property String ^Name;
    
        // The read-only Age property
        property int Age
        {
            int get() {
                DateTime now = DateTime::Now;
                return now.Year - yyyy;
            }
        }
    };

    The class now has three integer data members to hold the date of birth, initialized in the constructor.

    The Age property now has only a get method, which retrieves a DateTime object representing the current date and time and then calculates the age from the difference between the current year and the stored year.

  3. Use the Name and Age properties as you did in the previous example.

    int main(array<String ^> ^args)
    {
        // Create a Person object
        Person ^p = gcnew Person("fred", 4,9,1955);
    
        // Access the Name and Age properties
        Console::WriteLine("Age of {0} is {1}", p->Name, p->Age);
        return 0;
    }

    You can’t set the Age property because you haven’t provided a setter. This will result in a compiler error if you try to assign to the Age property.

  4. Build and run the application.

Properties, inheritance, and interfaces

Properties are first-class members of types, on the same level as member functions and data members. This means that you can use them in inheritance and in interfaces. Properties can be virtual and even pure virtual, and it isn’t necessary for both the get and set methods to have the same virtual specifier.

This exercise shows you how to use a virtual property when inheriting from a base class.

  1. Create a new CLR Console Application project named PropertyInheritance.

  2. Immediately after the using namespace System; line, define an abstract class called Shape.

    public ref class Shape abstract
    {
    public:
        virtual property double Area;
    };

    This class defines a property called Area that is virtual and which can be overridden by subclasses.

  3. Add the definition for a Circle class, which inherits from Shape and which also implements the Area property.

    public ref class Circle : Shape
    {
        double radius;
    public:
        Circle(double r)
        {
            radius = r;
        }
    
        virtual property double Area
        {
            double get() override {
                return Math::PI * radius * radius;
            }
        }
    };

    The constructor for Circle takes a value for the radius, which is used in the Area property to calculate the area of the circle. Note the placement of the modifiers on the Area property declaration: It is declared as virtual, and the get is declared as an override.

  4. Add a simple function to take a Shape and print out its area.

    void printArea(Shape ^s)
    {
        Console::WriteLine("Area is {0}", s->Area);
    }
  5. Create a Circle in main and pass it to the printArea function.

    Circle ^c = gcnew Circle(4.0);
    printArea(c);
  6. Build and run the application.

    You will see that even though the printArea function has a Shape as its argument type, it will use the Circle implementation of Area at run time.

Implementing indexed properties

Now that you know how to implement a scalar property, let’s move on to consider indexed properties, which are also known as indexers. These are useful for classes that have data members that are collections of items, and where you might want to access one of the items in the collection.

The Bank example

Consider as an example a Bank class that maintains a collection of Accounts. If you’re not using properties, you’d tend to see code such as the following being used to access members of the Bank class:

// Get a reference to one of the Accounts held by the Bank
Account ^acc = theBank->getAccount(1234567);

An indexed property makes it possible for you access the Account members by using array notation, such as is demonstrated here:

// Get a reference to one of the accounts held by the Bank
Account ^acc = theBank->Account[1234567];

You can implement get and set methods for indexed properties so that you can use them on both sides of the equal sign (=). The following code fragment uses two properties, with the first indexed property giving access to an account, and the second giving access to an overdraft limit:

// Set the overdraft limit for one of the accounts
theBank->Account[1234567]->OverDraft = 250.0;

Implementing the Bank class

The longer exercise that follows walks you through implementing the Bank and Account classes, and it also shows you how to create and use both scalar and indexed properties.

  1. Start Visual Studio 2012 and create a new CLR Console Application project named Banker.

  2. Add a new C++ header file named Bank.h to the project. When the file opens in the editor, edit the class declaration so that looks like this:

    #pragma once
    
    ref class Bank
    {
    public:
        Bank();
    };
  3. Add an implementation file called Bank.cpp to the project. When it opens in the editor, edit the code so that it looks like this:

    #include "stdafx.h"
    using namespace System;
    
    #include "Bank.h"
    
    Bank::Bank()
    {
        Console::WriteLine("Bank: constructor");
    }
  4. To ensure that everything is correct, open the Banker.cpp file and add code to the main function to create a Bank object.

    int main(array<String ^> ^args)
    {
        Console::WriteLine("Bank Example");
    
        // Create a Bank object
        Bank ^theBank = gcnew Bank();
    
        return 0;
    }
  5. You must also include Bank.h from the Banker.cpp file so that the compiler will know where to locate the declaration of the Bank class. Therefore, add the following code to Banker.cpp after the #include “stdafx.h” line:

    #include "Bank.h"
  6. Compile and run the application. You should see the constructor message being printed on the console.

Adding the Account class

The next stage involves creating the Account class in very much the same way.

  1. Add a header file named Account.h to the project. Edit the header file so that it looks like this:

    #pragma once
    using namespace System;
    
    ref class Account
    {
    public:
        Account();
    };
  2. Add an implementation file named Account.cpp that looks like this:

    #include "stdafx.h"
    using namespace System;
    
    #include "Account.h"
    
    Account::Account()
    {
        Console::WriteLine("Account: constructor");
    }
  3. Add some structure to the Account class. Accounts will have an account number, a balance, and an overdraft limit, so add three private members to the Account class definition in Account.h, as shown in the following:

    private:
        long accNumber;   // the account number
        double balance;   // the current balance
        double limit;     // the overdraft limit
  4. Open Account.cpp. Edit the constructor definition and implementation as follows so that three values are passed in and used to initialize these three variables:

    Account::Account(long num, double bal, double lim)
    {
        Console::WriteLine("Account: constructor");
        // Basic sanity check
        if (num < 0 || lim < 0)
            throw gcnew ArgumentException("Bad arguments to constructor");
    
        // Initialize values
        accNumber = num;
        balance = bal;
        limit = lim;
    }

    Remember that you will need to modify the declaration of the constructor in the Account.h header file, as well.

    The basic sanity check simply checks that the account number and overdraft limit aren’t negative. If they are, it throws an ArgumentException.

Creating Account class properties

After the Account class has been constructed, you can add properties to give access to the three data members. All three members are scalar, so the properties are easy to implement.

  1. Add a public property to Account.h to allow read-only access to the account number, as shown here:

    property long AccountNumber
    {
        long get() { return accNumber; }
    }

    You can add the function definition inline in the class definition. Remember to put it in the public section.

  2. You also need to add a read-only property for the balance member, because in real life, you don’t want people simply modifying the balances in their accounts from code.

    property double Balance
    {
        double get() { return balance; }
    }
  3. Add a read/write property for the overdraft limit because it’s quite possible that the limit might be changed from time to time.

    property double OverdraftLimit
    {
        double get() { return limit; }
        void set(double value) {
            if (value < 0)
                throw gcnew ArgumentException("Limit can't be negative");
    
            limit = value;
        }
    }

    If you choose to implement these properties inline in the class definition, you’ll need to add a using namespace System; line or fully qualify the name of ArgumentException before the code will compile.

  4. Test out your implementation by adding some code to the main function in Banker.cpp to create a new Account object and access its properties. Include the Account.h file, and then add code to create an Account object, as demonstrated here:

    // Create an Account object
    Account ^theAccount = gcnew Account(123456, 0.0, 0.0);
  5. Build and run the application and check the output.

Adding accounts to the Bank class

The purpose of the Bank class is to hold Accounts, so the next step is to modify the Bank class to hold a collection of Account objects. Rather than design something from scratch, you’ll use the System::Collections::Generic::List class (which is introduced in Chapter 12) to hold the Accounts.

Implementing the Add and Remove methods

The Add and Remove methods provide a way to manipulate the collection of Accounts held by the Bank class.

  1. Open the Bank.h header file. Add the following two lines of code immediately after the #pragma once line at the top of the file:

    using namespace System::Collections::Generic;
    #include "Account.h"

    The using declaration will make it easier to use a List in the Bank class, and you’ll need to reference the Account class later.

  2. Add a List variable to the Bank class, ensuring that it’s private.

    List<Account^> ^accounts;

    Because List is a generic collection, you need to specify what it is going to hold. In this case, the List is going to hold Account handles.

  3. Add the code for the public AddAccount method inline in the header file as follows:

    bool AddAccount(Account ^acc)
    {
        // check if the account is already in the list
        if (accounts->Contains(acc))
            return false;
        else
            accounts->Add(acc);
        return true;
    }

    AddAccount takes a handle to an Account object and then uses the List::Contains method to check whether the account already exists in the collection. If it doesn’t, the Account is added to the collection.

  4. Add code for the RemoveAccount function, which works in a very similar way.

    bool RemoveAccount(Account ^acc)
    {
        // check if the account is already in the list
        if (accounts->Contains(acc))
        {
            accounts->Remove(acc);
            return true;
        }
        else
            return false;
    }

    RemoveAccount checks whether an Account is in the list and, if present, removes it. It isn’t necessary to call Contains because RemoveAccount will silently do nothing if you try to remove an item that isn’t in the list. However, users might be interested in knowing that the account they’re trying to remove isn’t in the collection already.

  5. Add the following line of code to the Bank constructor to create the List member:

    accounts = gcnew List<Account^>();
  6. Build the application to ensure that there are no errors.

Implementing an indexed property to retrieve accounts

You can now manipulate the collection of Accounts, adding and removing items. If you want to look up a particular account, you’ll probably want to do so by the account number, and an indexed property provides a good way to access accounts by account number.

Indexed properties work in a very similar way to scalar properties, but you show the compiler that you are defining an indexed property by including the index type in square brackets after the property name.

property double Balance[long]

This informs the compiler that we are defining an indexed property called Balance that will use a long as its index type. When you define the indexed property, you include the index as the first parameter to the getter and setter.

property double Balance[long]
{
    double get(long idx) { ... }
    void set(long idx, double value) { ... }
}

Within the getter and setter, you can use the index to find the appropriate value. You can use the indexer like this:

// Get the balance for account 12345
double bal = myBank->Balance[12345];

In this exercise you will implement an indexed property to retrieve Account objects. Because you only need to retrieve Account handles and not set them, you’ll implement a read-only indexed property.

  1. Open the Bank.h header file.

  2. Add the following code to implement the property:

    // Indexed property to return an account
    property Account ^default[long]
    {
        Account ^get(long num)
        {
            for each(Account ^acc in accounts)
            {
                if (acc->AccountNumber == num)
                    return acc;
            }
            throw gcnew ArgumentOutOfRangeException("No such account");
        }
    }

    When you find an account whose number matches the one passed in, its handle is returned. If no such account is found, an exception is thrown because trying to access a nonexistent account is equivalent to reading off the end of an array: It’s a serious error that should be signaled to the caller.

  3. Test out the Bank class by adding some code to the main function in Banker.cpp. You’ll need to start by ensuring that the Bank.h and Account.h header files are included. Next add some code so that your main function is similar to the following:

    int main(array<String ^> ^args)
    {
        Console::WriteLine("Bank example");
    
        // Create a bank
        Bank ^theBank = gcnew Bank();
    
        // Create some accounts
        Account ^accountOne = gcnew Account(123456, 100.0, 0.0);
        Account ^accountTwo = gcnew Account(234567, 1000.0, 100.0);
        Account ^accountThree = gcnew Account(345678, 10000.0, 1000.0);
    
        // Add them to the Bank
        theBank->AddAccount(accountOne);
        theBank->AddAccount(accountTwo);
        theBank->AddAccount(accountThree);
    
        // Use the indexed property to access an account
        Account ^pa = theBank[234567];
        Console::WriteLine("Account Number is {0}", pa->AccountNumber);
    
        return 0;
    }

    After creating a Bank and a number of Account objects, you add the Account objects to the Bank collection by calling Add. You can then use the indexed property to access an account by number and use that pointer to display the balance. Test the property by passing in an account number that doesn’t exist and check that an exception is thrown.

  4. Build and run the application and then check the output.

Quick reference

To

Do This

Create a property for a C++ class.

Use the property keyword and implement get and/or set methods. For example:

property int Weight
{
    int get() { ... }
    void set(int w) { ... }
}

Implement a simple property that requires no logic in its get or set methods.

Use an auto-implemented property. For example:

property String ^Name;

Implement a read-only property.

Implement only the get method.

Implement a write-only property.

Implement only the set method.

Implement an indexed property.

Implement a property that specifies an index type in square brackets, and whose get and set methods take an index value that is used to determine which value to get or set. For example:

property Amount ^Pay[Person]
{
    Amount ^get(Person^) { ... }
}
..................Content has been hidden....................

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