173
■ ■ ■
CHAPTER 7
Features of a .NET Class
Y
ou’ve been using properties throughout the text, and you looked at an example of an event
in Chapter 2. This chapter will go into a bit more detail on properties and events, and will also
discuss some features of operators unique to C++/CLI, including static operators and how conver-
sion operators work in C++/CLI versus classic C++. You’ll also learn about casts and conversions.
Properties
As you saw in Chapter 2, in terms of object-oriented programming, properties capture the
“has-a” relationship for an object. Properties seem a lot like fields to the consumer of a class.
They represent values that can be retrieved and/or written to. You can use them inside the
class as well as outside the class (if they are public). There is a special syntax for using them that
makes them look like fields, but operations on these “fields” invoke the accessor (get and set)
methods that you’ve defined. Properties fully encapsulate the underlying data, whether it’s a
single field or something more complex, meaning that you are free to change the underlying
field’s representation without affecting the users of the class. Say we want to declare some
typical properties we might find in a periodic table of the elements. Listing 7-1 shows how.
Listing 7-1. Declaring Properties
// declaring_properties.cpp
using namespace System;
value class ElementType
{
public:
property unsigned int AtomicNumber;
property double AtomicWeight;
property String^ Name;
property String^ Symbol;
};
Hogenson_705-2C07.fm Page 173 Wednesday, October 18, 2006 4:50 PM
174
CHAPTER 7
■
FEATURES OF A .NET CLASS
int main()
{
ElementType oxygen;
oxygen.AtomicNumber = 8;
oxygen.AtomicWeight = 15.9994;
oxygen.Name = "Oxygen";
oxygen.Symbol = "O";
Console::WriteLine("Element: {0} Symbol: {1}", oxygen.Name, oxygen.Symbol);
Console::WriteLine("Atomic Number: {0} Atomic Weight: {1}",
oxygen.AtomicNumber, oxygen.AtomicWeight);
}
The output of Listing 7-1 is as follows:
Element: Oxygen Symbol: O
Atomic Number: 8 Atomic Weight: 15.9994
As you can see, the property is invoked by using its name in a member access expression.
You do not call get and set explicitly; they are called for you whenever code specifies a construct
that either retrieves the value (for example, using the property in an expression or as a function
parameter) or sets the value (when the property is used as an lvalue).
Expressions involving properties may not be chained. That is to say, a property cannot be
an lvalue and an rvalue at the same time. So, code like this does not work:
a = oxygen.AtomicNumber = 8; // error
In this example, we use the shorthand syntax for declaring properties that map directly
onto a field and have trivial get and set methods. A field is created automatically for such a
property, as well as the default get and set methods. Such a field is not intended to be accessed
in any way other than through the property. If you use this syntax, you can change it later to the
full form of the syntax (for example, to provide an alternative implementation of the property’s
underlying data, or add some custom code to the get and set methods) without changing the
property’s interface to outside users of the type. In Listing 7-2, we change the AtomicWeight
property from a simple double value to a computed value based on the isotopic abundances
and number of isotopes. Once the value is computed, the stored result is used. The set method
just sets the value as usual, and would perhaps be used if looking up the information from a
periodic table.
Listing 7-2. Computing a Property Value
// periodic_table.cpp
using namespace System;
using namespace System::Collections::Generic;
Hogenson_705-2C07.fm Page 174 Wednesday, October 18, 2006 4:50 PM
CHAPTER 7
■
FEATURES OF A .NET CLASS
175
value class Isotope
{
public:
property double Mass;
property unsigned int AtomicNumber;
};
value class ElementType
{
List<Isotope>^ isotopes;
List<double>^ isotope_abundance;
double atomicWeight;
public:
property unsigned int AtomicNumber;
property String^ Name;
property String^ Symbol;
property double AtomicWeight
{
double get()
{
// Check to see if atomic weight has been calculated yet.
if (atomicWeight == 0.0)
{
if (isotopes->Count == 0)
return 0.0;
for (int i = 0; i < isotopes->Count; i++)
{
atomicWeight += isotopes[i].Mass * isotope_abundance[i];
}
}
return atomicWeight;
}
void set(double value)
{
// used if you look up atomic weight instead of calculating it
atomicWeight = value;
}
}
// other properties same as before
};
You can see how creating a trivial property isn’t like exposing a field directly to users of
a class. If you expose a field directly, you run into problems later if the implementation of the
field changes. With a trivial property, you can always later define the get and set methods
yourself and change the backing store for the property to suit your needs, while preserving the
Hogenson_705-2C07.fm Page 175 Wednesday, October 18, 2006 4:50 PM
176
CHAPTER 7
■
FEATURES OF A .NET CLASS
interface the property presents to other consumers. When defining get and set explicitly, the
set method must return void and the get method must return the type of the property. The
parameter list for get must be void and the parameter list for set must be the type of the property.
Properties need not map onto a field’s value. For example, you could eliminate the
atomicWeight field from the class and simply compute the value whenever get is called. The set
method would then have to be eliminated. This is fine, though, since if only a get method is
defined, the property can be retrieved but not set.
As these methods get more complicated, you’ll want to move them out of the class decla-
ration. When defining property get and set methods out of the body of the class, use the class
name and property name as qualifiers, as in Listing 7-3.
Listing 7-3. Defining Property Accessors Outside of a Class
value class ElementType
{
public:
property double AtomicWeight
{
double get();
}
};
double ElementType::AtomicWeight::get()
{
// same implementation as before
}
In fact, this notation is how the property accessor is referred to when you need to refer
to the method name, such as when you assign a delegate to a get or set method; you use the
name of the property in the qualified name, as shown in Listing 7-4.
Listing 7-4. Using a Delegate with a Property Accessor
// property_accessor_delegate.cpp
using namespace System;
delegate double ValueAccessor();
value class ElementType
{
public:
property double AtomicWeight;
};
Hogenson_705-2C07.fm Page 176 Wednesday, October 18, 2006 4:50 PM
CHAPTER 7
■
FEATURES OF A .NET CLASS
177
int main()
{
ElementType oxygen;
oxygen.AtomicWeight = 15.9994;
ValueAccessor^ get_method = gcnew ValueAccessor(oxygen,
&ElementType::AtomicWeight::get);
Console::WriteLine("{0}", get_method->Invoke());
}
Say we’d like to also have some static properties in our Element class. In fact, we’d like to
make a periodic table class with a static array property. There is nothing special about a static
property; all the rules for static methods and fields apply. Static properties are intended to be
used for properties of a type, not properties of a particular instance. Listing 7-5 is a first attempt
at this.
Listing 7-5. Trying to Define a Static Property
// property_static.cpp
value class ElementType
{
public:
// Periodic Table of the Elements
static property array<ElementType>^ PeriodicTable;
static ElementType()
{
PeriodicTable = gcnew array<ElementType>(120);
// Initialize each element and its properties.
}
};
That’s great, but if we later want to change the implementation from an array to a List or
Hashtable, we might need to rewrite the code that uses the property. A better way to implement
collection-like properties is to use vector properties, also called indexed properties.
Using Indexed Properties
A special type of property is allowed in C++/CLI that enables properties to act like arrays. You
can also use indexed properties to provide array indexing on objects, the equivalent of defining
the array indirection operator (operator[]) for your type.
To make a property support the indexing syntax, use the square brackets in the property
declaration. Inside the square brackets, put the type you will use as the index. You can index on
any type. Listing 7-6 shows a simple indexed property named ordinal. Note the type of the
index appears inside square brackets, and the index is used as the first parameter of both the
get and set methods.
Hogenson_705-2C07.fm Page 177 Wednesday, October 18, 2006 4:50 PM
178
CHAPTER 7
■
FEATURES OF A .NET CLASS
Listing 7-6. Using an Indexed Property
// properties_indexed1.cpp
using namespace System;
ref class Numbers
{
array<String^>^ ordinals;
public:
Numbers()
{
ordinals = gcnew array<String^> { "zero", "one", "two", "three" };
}
property String^ ordinal[unsigned int]
{
String^ get(unsigned int index)
{
return ordinals[index];
}
void set(unsigned int index, String^ value)
{
ordinals[index] = value;
}
}
};
int main()
{
Numbers^ nums = gcnew Numbers();
// Access the property values using the indexer
// with an unsigned int as the index.
Console::WriteLine( nums->ordinal[0] );
}
Here is the output of Listing 7-6:
zero
You can also define a default indexed property by naming the property default, which
enables the index to be used directly on the instance of the object (see Listing 7-7). Whether
you are accessing a default indexed property using a handle or a variable declared with stack
semantics, you can use the array indirection operator directly.
Hogenson_705-2C07.fm Page 178 Wednesday, October 18, 2006 4:50 PM
CHAPTER 7
■
FEATURES OF A .NET CLASS
179
Listing 7-7. Using a Default Property
// properties_indexed2.cpp
using namespace System;
ref class Numbers
{
array<String^>^ ordinals;
public:
Numbers()
{
ordinals = gcnew array<String^> { "zero", "one", "two", "three" };
}
property String^ default[unsigned int]
{
String^ get(unsigned int index)
{
return ordinals[index];
}
void set(unsigned int index, String^ value)
{
ordinals[index] = value;
}
}
};
int main()
{
Numbers nums;
// Access property using array indexing operators on the
// instance directly.
Console::WriteLine( nums[0] );
// If using a handle, you can still use array syntax.
Numbers^ nums2 = gcnew Numbers();
Console::WriteLine( nums2[1] );
// You can also use the name "default" and access like a
// named property.
Console::WriteLine( nums.default[2] );
Console::WriteLine( nums2->default[3] );
}
Hogenson_705-2C07.fm Page 179 Wednesday, October 18, 2006 4:50 PM
180
CHAPTER 7
■
FEATURES OF A .NET CLASS
The output of Listing 7-7 is as follows:
zero
one
two
three
Listing 7-8 shows a class with an indexed property whose backing store is a collection class.
The indexed property on the class PeriodicTable invokes the default indexed property on a .NET
Framework collection class, Hashtable (here accessed through the interface IDictionary). The
ElementType class now overrides the ToString method on Object to allow custom output.
Chapter 8 discusses the override keyword.
Listing 7-8. Backing a Property with a Collection
// periodic_table.cpp
using namespace System;
using namespace System::Collections;
value class ElementType
{
public:
property unsigned int AtomicNumber;
property double AtomicWeight;
property String^ Name;
property String^ Symbol;
// You cannot use initializer list syntax to initialize properties.
ElementType(String^ name, String^ symbol,
double a, double n)
{
AtomicNumber = n;
AtomicWeight = a;
Name = name;
Symbol = symbol;
}
// Override the ToString method (you'll learn more about the override
// keyword in the next chapter).
virtual String^ ToString() override
{
return String::Format(
"Element {0} Symbol {1} Atomic Number {2} Atomic Weight {3}",
Name, Symbol, AtomicNumber, AtomicWeight);
}
};
Hogenson_705-2C07.fm Page 180 Wednesday, October 18, 2006 4:50 PM
CHAPTER 7
■
FEATURES OF A .NET CLASS
181
ref class PeriodicTable
{
private:
Hashtable^ table;
public:
PeriodicTable()
{
table = gcnew Hashtable();
ElementType element("Hydrogen", "H", 1.0079, 1);
// Add to the Hashtable using the key and value.
table->Add(element.Name, element);
// Add the other elements...
}
property ElementType default[String^]
{
ElementType get(String^ key)
{
return safe_cast<ElementType>( table[key] );
}
}
};
int main()
{
PeriodicTable^ table = gcnew PeriodicTable();
// Get the element using the indexed property and print it.
Console::WriteLine( table["Hydrogen"] );
}
The output of Listing 7-8 is shown here:
Element Hydrogen Symbol H Atomic Number 1 Atomic Weight 1.0079
Now suppose we want to implement a table of the isotopes, as envisioned in Chapter 2.
Isotopes are different versions of the same element, so there is a many-to-one relationship
between isotopes and elements. Isotopes are distinguished by a number, the isotope number,
which is equal to the number of protons plus the number of neutrons. The number of protons
determines the type of element, and the different isotopes of an element just vary by the number
Hogenson_705-2C07.fm Page 181 Wednesday, October 18, 2006 4:50 PM
182
CHAPTER 7
■
FEATURES OF A .NET CLASS
of neutrons. In Listing 7-9, a hashtable is used to store the various isotopes. The key is based on
the element type and the isotope number, which uniquely identifies the isotope. For example,
for carbon-14, the key is “C14”. Since you can have more than one index variable, separated by
commas, in an indexed property, we could look up an isotope by the name of the element and
the isotope number, as the ElementIsotope property in Listing 7-9 shows. The key is computed
by appending the element symbol and the isotope number, which are the arguments of the
indexed property.
Listing 7-9. Using Multiple Indexes
// isotope_table.cpp
using namespace System;
using namespace System::Collections::Generic;
value class Isotope
{
public:
property unsigned int IsotopeNumber;
property unsigned int AtomicNumber;
};
ref class IsotopeTable
{
private:
Dictionary<String^, Isotope>^ isotopeTable;
public:
IsotopeTable()
{
isotopeTable = gcnew Dictionary<String^, Isotope>();
// Add the elements and their isotopes...
// Additional code for the elements is assumed.
for each (ElementType element in PeriodicTable::Elements)
{
// Add each isotope to the isotopes table.
for each (Isotope isotope in element.Isotopes)
{
isotopeTable->Add(element.Name + isotope.IsotopeNumber, isotope);
}
}
}
Hogenson_705-2C07.fm Page 182 Wednesday, October 18, 2006 4:50 PM
CHAPTER 7
■
FEATURES OF A .NET CLASS
183
// Pass in the element symbol and isotope number, e.g., "C" and 14 for
// carbon-14.
property Isotope ElementIsotope[String^, int ]
{
Isotope get(String^ key, int isotopeNumber)
{
key = key + isotopeNumber.ToString();
return isotopeTable[key];
}
}
};
For many of these examples, we omit the set accessor to make the property read-only. You
can do the opposite for a write-only property (see Listing 7-10). You can also use access control
to set individual access to the set and get methods. Recalling the Atom class from Chapter 2,
and the derived class RadioactiveAtom, it makes sense to use the access control specifier protected
to limit setting the AtomicNumber property to the class and its derived classes. That way the
radioactive atom can change the atomic number to process a decay event, but consumers of
the atom class can’t otherwise change the atomic number.
Listing 7-10. Defining a Write-Only Property
ref class Atom
{
unsigned int atomic_number;
public:
property unsigned int IsotopeNumber;
property unsigned int AtomicNumber
{
// Anyone can get the atomic number.
public: unsigned int get()
{
return atomic_number;
}
// Only derived classes (such as RadioactiveAtom)
// can change the atomic number.
protected: void set(unsigned int n)
{
atomic_number = n;
}
}
};
ref class RadioactiveAtom : Atom
{
// other stuff
Hogenson_705-2C07.fm Page 183 Wednesday, October 18, 2006 4:50 PM
184
CHAPTER 7
■
FEATURES OF A .NET CLASS
public:
void AlphaDecay()
{
AtomicNumber -= 2;
IsotopeNumber -= 4;
}
};
AlphaDecay is a function representing a decay of the atom, releasing two protons and two
neutrons in the form of an alpha particle. This changes the atomic number and isotope number,
which are updated. As you recall, the decay events in a radioactive atom were modeled using
delegates and events. The delegate or event was used to call the designated decay method. The
next section covers delegates and events in more detail.
Delegates and Events
Delegates can be viewed as the function pointers of the managed world. As a C++ programmer,
you probably often use typedef to hide some of the complexity of the syntax for declaring and
using function pointers. A delegate is an object that designates a function to call on a specific
object (if the function is an instance method) or class (if the function is a static method), or a
global function. The delegate is not the function itself; it simply represents the address of a
function to call, along with a specific object whose method is to be called, if applicable.
Delegates are strongly typed, in that the parameter types and return type are part of the
type of a delegate. A delegate variable may only be assigned to a function that matches the
delegate signature. Delegates may not be used to designate a family of overloaded functions.
They may only be used to designate specific function prototypes with specific arguments.
You saw in Chapter 2 how to declare and use a simple delegate. Delegates are actually
instances of the .NET Framework class System::MulticastDelegate. The name “multicast”
implies that many functions may be called when a delegate is invoked. This is, in fact, the case.
The delegate keeps an internal list of functions in an invocation list, and all the functions on
that list are invoked every time the Invoke method is called. You use the += operator to add
functions to the invocation list, and the -= operator to remove them. You can also use the ()
operator to call the Invoke method implicitly, as in Listing 7-11.
Listing 7-11. Using a Delegate
// delegate_operators.cpp
using namespace System;
delegate void MyDelegate();
ref class R
{
public:
Hogenson_705-2C07.fm Page 184 Wednesday, October 18, 2006 4:50 PM
CHAPTER 7
■
FEATURES OF A .NET CLASS
185
void f() { Console::WriteLine("R::f"); }
void g() { Console::WriteLine("R::g"); }
};
int main()
{
MyDelegate^ d;
R^ r = gcnew R();
d += gcnew MyDelegate(r, &R::f);
d += gcnew MyDelegate(r, &R::g);
d->Invoke();
d -= gcnew MyDelegate(r, &R::g);
// Use operator() instead of calling Invoke.
d();
}
The output of Listing 7-11 is as follows:
R::f
R::g
R::f
Don’t worry that when you use the -= operator, you are passing a newly created delegate
to the -= operator. This seems counterintuitive, since you’re actually deleting something, not
creating it anew. The -= operator compares the invocation list of the right-side delegate to the
invocation list of the delegate from which you are removing it, and removes the matching function
(or functions) from the list.
Let’s say the functions we’re invoking have return values.
delegate String^ MyDelegate();
You’ll find that the line
d += gcnew MyDelegate(r, &R::f);
triggers a compiler warning:
warning C4358: '+=': return type of combined delegates is not 'void';
returned value is undefined
The issue is that if there are multiple functions called, each of which returns a different
value, how do we know which function’s return value gets returned from the delegate? And
what happens to the return values for the others? In the CLR, the actual return value is the
Hogenson_705-2C07.fm Page 185 Wednesday, October 18, 2006 4:50 PM
186
CHAPTER 7
■
FEATURES OF A .NET CLASS
return value of the last delegate called. However, it would not be wise to rely on which function
is the last one called, as this is implementation dependent. The Invoke function is too simplistic
to deal with this situation. What we need to do is get the invocation list and walk through it,
calling each target function and examining the return value separately, as in Listing 7-12. In
order to avoid the warning, we can use the Combine and Remove methods instead of the operators.
Listing 7-12. Walking Through an Invocation List
// delegate_invocation_list.cpp
using namespace System;
delegate String^ MyDelegate();
ref class R
{
public:
String^ f() { return "R::f"; }
String^ g() { return "R::g"; }
String^ h() { return "R::h"; }
};
int main()
{
MyDelegate^ d;
R^ r = gcnew R();
d = gcnew MyDelegate(r, &R::f);
// Cast the return value to this particular delegate type.
// Note: the C-style cast evaluates to a safe_cast.
d = (MyDelegate^) d->Combine(d, gcnew MyDelegate(r, &R::g));
d = (MyDelegate^) d->Combine(d, gcnew MyDelegate(r, &R::h));
String^ s = d->Invoke();
Console::WriteLine("Return value was {0}", s);
d = (MyDelegate^) d->Remove(d, gcnew MyDelegate(r, &R::g));
s = d->Invoke();
Console::WriteLine("Return value was {0}", s);
for each (MyDelegate^ del in d->GetInvocationList())
{
s = del->Invoke();
Console::WriteLine("Return value was {0}", s);
}
}
Hogenson_705-2C07.fm Page 186 Wednesday, October 18, 2006 4:50 PM
CHAPTER 7
■
FEATURES OF A .NET CLASS
187
Here is the output for Listing 7-12:
Return value was R::h
Return value was R::h
Return value was R::f
Return value was R::h
The output shows us that, in reality, the last function added is the one whose value is
returned. But since this is implementation-defined, we should heed the warning and always
use a manual walk of the invocation list with these delegates.
Using GetInvocationList is also useful if exceptions might be thrown by the functions
called through the delegate. If one delegate function throws an exception, other target functions
may never execute. Walking through the invocation list manually enables you to wrap each
invocation in a try/catch block, giving you more control over the functions that are invoked.
Listing 7-13 demonstrates this technique.
Listing 7-13. Manually Walking Through an Invocation List
// delegate_with_exceptions.cpp
using namespace System;
delegate String^ MyDelegate();
ref class R
{
public:
String^ f() { throw gcnew Exception(); return "R::f"; }
String^ g() { return "R::g"; }
String^ h() { return "R::h"; }
};
int main()
{
MyDelegate^ d;
R^ r = gcnew R();
d = gcnew MyDelegate(r, &R::f);
d = safe_cast<MyDelegate^>(d->Combine(d, gcnew MyDelegate(r, &R::g)));
d = safe_cast<MyDelegate^>(d->Combine(d, gcnew MyDelegate(r, &R::h)));
for each (MyDelegate^ del in d->GetInvocationList())
{
Hogenson_705-2C07.fm Page 187 Wednesday, October 18, 2006 4:50 PM