This chapter discusses three interrelated and powerful features: runtime type identification, reflection, and attributes. Runtime type ID is the mechanism that lets you identify a type during the execution of a program. Reflection is the feature that enables you to obtain information about a type. Using this information, you can construct and use objects at runtime. This feature is very powerful because it lets a program add functionality dynamically, during execution. An attribute describes a characteristic of some element of a C# program. For example, you can specify attributes for classes, methods, and fields, among others. Attributes can be interrogated at runtime, and the attribute information obtained. Attributes use both runtime type identification and reflection.
Runtime type identification (RTTI) allows the type of an object to be determined during program execution. RTTI is useful for many reasons. For example, you can discover precisely what type of object is being referred to by a base-class reference. Another use of RTTI is to test in advance whether a cast will succeed, preventing an invalid cast exception. Runtime type identification is also a key component of reflection.
C# includes three keywords that support runtime type identification: is, as, and typeof. Each is examined in turn.
You can determine if an object is of a certain type by using the is operator. Its general form is shown here:
expr is type
Here, expr is an expression that describes an object whose type is being tested against type. If the type of expr is the same as, or compatible with, type, then the outcome of this operation is true. Otherwise, it is false. Thus, if the outcome is true, expr is some form of type. As it applies to is, one type is compatible with another if both are the same type, or if a reference, boxing, or unboxing conversion exists.
Here is an example that uses is:
// Demonstrate is.
using System;
class A {}
class B : A {}
class UseIs {
static void Main() {
A a = new A();
B b = new B();
if(a is A) Console.WriteLine("a is an A");
if(b is A)
Console.WriteLine("b is an A because it is derived from A");
if(a is B)
Console.WriteLine("This won’t display -- a not derived from B");
if(b is B) Console.WriteLine("b is a B");
if(a is object) Console.WriteLine("a is an object");
}
}
The output is shown here:
a is an A
b is an A because it is derived from A
b is a B
a is an object
Most of the is expressions are self-explanatory, but two may need a little discussion. First, notice this statement:
if(b is A)
Console.WriteLine("b is an A because it is derived from A");
The if succeeds because b is an object of type B, which is derived from type A. Thus, b is an A. However, the reverse is not true. When this line is executed,
if(a is B)
Console.WriteLine("This won’t display -- a not derived from B");
the if does not succeed, because a is of type A, which is not derived from B. Thus, a is not a B.
Sometimes you will want to try a conversion at runtime, but not throw an exception if the conversion fails (which is the case when a cast is used). To do this, use the as operator, which has this general form:
expr as type
Here, expr is the expression being converted to type. If the conversion succeeds, then a reference to type is returned. Otherwise, a null reference is returned. The as operator can be used to perform only reference, boxing, unboxing, or identity conversions.
The as operator offers a streamlined alternative to is in some cases. For example, consider the following program that uses is to prevent an invalid cast from occurring:
// Use is to avoid an invalid cast.
using System;
class A {}
class B : A {}
class CheckCast {
static void Main() {
A a = new A();
B b = new B();
// Check to see if a can be cast to B.
if(a is B) // if so, do the cast
b = (B) a;
else // if not, skip the cast
b = null;
if(b==null)
Console.WriteLine("The cast in b = (B) a is NOT allowed.");
else
Console.WriteLine("The cast in b = (B) a is allowed");
}
}
This program displays the following output:
The cast in b = (B) a is NOT allowed.
As the output shows, since a is not a B, the cast of a to B is invalid and is prevented by the if statement. However, this approach requires two steps. First, the validity of the cast must be confirmed. Second, the cast must be made. These two steps can be combined into one through the use of as, as the following program shows:
// Demonstrate as.
using System;
class A {}
class B : A {}
class CheckCast {
static void Main() {
A a = new A();
B b = new B();
b = a as B; // cast, if possible
if(b==null)
Console.WriteLine("The cast in b = (B) a is NOT allowed.");
else
Console.WriteLine("The cast in b = (B) a is allowed");
}
}
Here is the output, which is the same as before:
The cast in b = (B) a is NOT allowed.
In this version, the as statement checks the validity of the cast and then, if valid, performs the cast, all in one statement.
Although useful in their own ways, the as and is operators simply test the compatibility of two types. Often, you will need to obtain information about a type. To do this, C# supplies the typeof operator. It retrieves a System.Type object for a given type. Using this object, you can determine the type’s characteristics.
The typeof operator has this general form:
typeof(type)
Here, type is the type being obtained. The Type object returned encapsulates the information associated with type.
Once you have obtained a Type object for a given type, you can obtain information about it through the use of various properties, fields, and methods defined by Type. Type is a large class with many members, and a discussion is deferred until the next section, where reflection is examined. However, to briefly demonstrate Type, the following program uses three of its properties: FullName, IsClass, and IsAbstract. To obtain the full name of the type, use FullName. IsClass returns true if the type is a class. IsAbstract returns true if a class is abstract.
// Demonstrate typeof.
using System;
using System.IO;
class UseTypeof {
static void Main() {
Type t = typeof(StreamReader);
Console.WriteLine(t.FullName);
if(t.IsClass) Console.WriteLine("Is a class.");
if(t.IsAbstract) Console.WriteLine("Is abstract.");
else Console.WriteLine("Is concrete.");
}
}
This program outputs the following:
System.IO.StreamReader
Is a class.
Is concrete.
This program obtains a Type object that describes StreamReader. It then displays the full name, and determines if it is a class and whether it is abstract.
Reflection is the feature that enables you to obtain information about a type. The term reflection comes from the way the process works: A Type object mirrors the underlying type that it represents. To obtain information, you ask the Type object questions, and it returns (reflects) the information associated with the type back to you. Reflection is a powerful mechanism because it allows you to learn and use the capabilities of types that are known only at runtime.
Many of the classes that support reflection are part of the .NET Reflection API, which is in the System.Reflection namespace. Thus, you will normally include the following in programs that use reflection:
using System.Reflection;
System.Type is at the core of the reflection subsystem because it encapsulates a type. It contains many properties and methods that you will use to obtain information about a type at runtime. Type is derived from an abstract class called System.Reflection.MemberInfo.
MemberInfo defines the following read-only properties:
Notice that the return type of MemberType is MemberTypes. MemberTypes is an enumeration that defines values that indicate the various member types. Among others, these include
MemberTypes.Constructor
MemberTypes.Method
MemberTypes.Field
MemberTypes.Event
MemberTypes.Property
Thus, the type of a member can be determined by checking MemberType. For example, if MemberType equals MemberTypes.Method, then that member is a method.
MemberInfo includes two abstract methods: GetCustomAttributes( ) and IsDefined( ). These both relate to attributes. The first obtains a list of the custom attributes associated with the invoking object. The second determines if an attribute is defined for the invoking object. The .NET Framework Version 4.0 adds a method called GetCustomAttributesData( ), which returns information about custom attributes. (Attributes are described later in this chapter.)
To the methods and properties defined by MemberInfo, Type adds a great many of its own. For example, here are several commonly used methods defined by Type:
Here are several commonly used, read-only properties defined by Type:
Using Type’s methods and properties, it is possible to obtain detailed information about a type at runtime. This is an extremely powerful feature, because once you have obtained information about a type, you can invoke its constructors, call its methods, and use its properties. Thus, reflection enables you to use code that was not available at compile time.
The Reflection API is quite large, and it is not possible to cover the entire topic here. (Complete coverage of reflection could easily fill an entire book!) However, because the Reflection API is logically designed, once you understand how to use a part of it, the rest just falls into place. With this thought in mind, the following sections demonstrate four key reflection techniques: obtaining information about methods, invoking methods, constructing objects, and loading types from assemblies.
Once you have a Type object, you can obtain a list of the public methods supported by the type by using GetMethods( ). One form is shown here:
MethodInfo[ ] GetMethods( )
It returns an array of MethodInfo objects that describe the methods supported by the invoking type. MethodInfo is in the System.Reflection namespace.
MethodInfo is derived from the abstract class MethodBase, which inherits MemberInfo. Thus, the properties and methods defined by all three of these classes are available for your use. For example, to obtain the name of a method, use the Name property. Two members that are of particular interest at this time are ReturnType and GetParameters( ).
The return type of a method is found in the read-only ReturnType property, which is an object of Type.
The method GetParameters( ) returns a list of the parameters associated with a method. It has this general form:
ParameterInfo[ ] GetParameters( );
The parameter information is held in a ParameterInfo object. ParameterInfo defines properties and methods that describe the parameter. Two properties that are of particular value are Name, which is a string that contains the name of the parameter, and ParameterType, which describes the parameter’s type. The parameter’s type is encapsulated within a Type object.
Here is a program that uses reflection to obtain the methods supported by a class called MyClass. For each method, it displays the return type and name of the method, and the name and type of any parameters that each method may have.
// Analyze methods using reflection.
using System;
using System.Reflection;
class MyClass {
int x;
int y;
public MyClass(int i, int j) {
x = i;
y = j;
}
public int Sum() {
return x+y;
}
public bool IsBetween(int i) {
if(x < i && i > y) return true;
else return false;
}
public void Set(int a, int b) {
x = a;
y = b;
}
public void Set(double a, double b) {
x = (int) a;
y = (int) b;
}
public void Show() {
Console.WriteLine(" x: {0}, y: {1}", x, y);
}
}
class ReflectDemo {
static void Main() {
Type t = typeof(MyClass); // get a Type object representing MyClass
Console.WriteLine("Analyzing methods in " + t.Name);
Console.WriteLine();
Console.WriteLine("Methods supported: ");
MethodInfo[] mi = t.GetMethods();
// Display methods supported by MyClass.
foreach(MethodInfo m in mi) {
// Display return type and name.
Console.Write(" " + m.ReturnType.Name +
" " + m.Name + "(");
// Display parameters.
ParameterInfo[] pi = m.GetParameters();
for(int i=0; i < pi.Length; i++) {
Console.Write(pi[i].ParameterType.Name +
" " + pi[i].Name);
if(i+1 < pi.Length) Console.Write(", ");
}
Console.WriteLine(")");
Console.WriteLine();
}
}
}
The output is shown here:
Analyzing methods in MyClass
Methods supported:
Int32 Sum()
Boolean IsBetween(Int32 i)
Void Set(Int32 a, Int32 b)
Void Set(Double a, Double b)
Void Show()
Type GetType()
String ToString()
Boolean Equals(Object obj)
Int32 GetHashCode()
Notice that in addition to the methods defined by MyClass, the public, non-static methods defined by object are also displayed. This is because all types in C# inherit object. Also notice that the .NET structure names are used for the type names. Observe that Set( ) is displayed twice. This is because Set( ) is overloaded. One version takes int arguments. The other takes double arguments.
Let’s look at this program closely. First, notice that MyClass defines a public constructor and a number of public methods, including the overloaded Set( ) method.
Inside Main( ), a Type object representing MyClass is obtained using this line of code:
Type t = typeof(MyClass); // get a Type object representing MyClass
Recall that typeof returns a Type object that represents the specified type, which in this case is MyClass.
Using t and the Reflection API, the program then displays information about the methods supported by MyClass. First, a list of the methods is obtained by the following statement:
MethodInfo[] mi = t.GetMethods();
Next, a foreach loop is established that cycles through mi. With each pass, the return type, name, and parameters for each method are displayed by the following code:
// Display return type and name.
Console.Write(" " + m.ReturnType.Name +
" " + m.Name + "(");
// Display parameters.
ParameterInfo[] pi = m.GetParameters();
for(int i=0; i < pi.Length; i++) {
Console.Write(pi[i].ParameterType.Name +
" " + pi[i].Name);
if(i+1 < pi.Length) Console.Write(", ");
}
In this sequence, the parameters associated with each method are obtained by calling GetParameters( ) and stored in the pi array. Then a for loop cycles through the pi array, displaying the type and name of each parameter. The key point is that this information is obtained dynamically at runtime without relying on prior knowledge of MyClass.
A second form of GetMethods( ) lets you specify various flags that filter the methods that are retrieved. It has this general form:
MethodInfo[ ] GetMethods(BindingFlags bindingAttr)
This version obtains only those methods that match the criteria you specify. BindingFlags is an enumeration. Here are several commonly used values:
You can OR together two or more flags. In fact, minimally you must include either Instance or Static with Public or NonPublic. Failure to do so will result in no methods being retrieved.
One of the main uses of the BindingFlags form of GetMethods( ) is to enable you to obtain a list of the methods defined by a class without also retrieving the inherited methods. This is especially useful for preventing the methods defined by object from being obtained. For example, try substituting this call to GetMethods( ) into the preceding program:
// Now, only methods declared by MyClass are obtained.
MethodInfo[] mi = t.GetMethods(BindingFlags.DeclaredOnly |
BindingFlags.Instance |
BindingFlags.Public) ;
After making this change, the program produces the following output:
Analyzing methods in MyClass
Methods supported:
Int32 Sum()
Boolean IsBetween(Int32 i)
Void Set(Int32 a, Int32 b)
Void Set(Double a, Double b)
Void Show()
As you can see, only those methods explicitly defined by MyClass are displayed.
Once you know what methods a type supports, you can call one or more of them. To do this, you will use the Invoke( ) method that is contained in MethodInfo. One of its forms is shown here:
object Invoke(object obj, object[ ] parameters)
Here, obj is a reference to the object on which the method is invoked. (For static methods, you can pass null to obj.) Any arguments that need to be passed to the method are specified in the array parameters. If no arguments are needed, parameters must be null. Also, parameters must contain exactly the same number of elements as there are arguments. Therefore, if two arguments are needed, then parameters must be two elements long. It can’t, for example, be three or four elements long. The value returned by the invoked method is returned by Invoke( ).
To call a method, simply call Invoke( ) on an instance of MethodInfo that was obtained by calling GetMethods( ). The following program demonstrates the procedure:
// Invoke methods using reflection.
using System;
using System.Reflection;
class MyClass {
int x;
int y;
public MyClass(int i, int j) {
x = i;
y = j;
}
public int Sum() {
return x+y;
}
public bool IsBetween(int i) {
if((x < i) && (i < y)) return true;
else return false;
}
public void Set(int a, int b) {
Console.Write("Inside Set(int, int). ");
x = a;
y = b;
Show();
}
// Overload set.
public void Set(double a, double b) {
Console.Write("Inside Set(double, double). ");
x = (int) a;
y = (int) b;
Show();
}
public void Show() {
Console.WriteLine("Values are x: {0}, y: {1}", x, y);
}
}
class InvokeMethDemo {
static void Main() {
Type t = typeof(MyClass);
MyClass reflectOb = new MyClass(10, 20);
int val;
Console.WriteLine("Invoking methods in " + t.Name);
Console.WriteLine();
MethodInfo[] mi = t.GetMethods();
// Invoke each method.
foreach(MethodInfo m in mi) {
// Get the parameters.
ParameterInfo[] pi = m.GetParameters();
if(m.Name.Equals("Set", StringComparison.Ordinal) &&
pi[0].ParameterType == typeof(int)) {
object[] args = new object[2];
args[0] = 9;
args[1] = 18;
m.Invoke(reflectOb, args);
}
else if(m.Name.Equals("Set", StringComparison.Ordinal) &&
pi[0].ParameterType == typeof(double)) {
object[] args = new object[2];
args[0] = 1.12;
args[1] = 23.4;
m.Invoke(reflectOb, args);
}
else if(m.Name.Equals("Sum", StringComparison.Ordinal)) {
val = (int) m.Invoke(reflectOb, null);
Console.WriteLine("sum is " + val);
}
else if(m.Name.Equals("IsBetween", StringComparison.Ordinal)) {
object[] args = new object[1];
args[0] = 14;
if((bool) m.Invoke(reflectOb, args))
Console.WriteLine("14 is between x and y");
}
else if(m.Name.Equals("Show", StringComparison.Ordinal)) {
m.Invoke(reflectOb, null);
}
}
}
}
The output is shown here:
Invoking methods in MyClass
sum is 30
14 is between x and y
Inside Set(int, int). Values are x: 9, y: 18
Inside Set(double, double). Values are x: 1, y: 23
Values are x: 1, y: 23
Look closely at how the methods are invoked. First, a list of methods is obtained. Then, inside the foreach loop, parameter information is retrieved. Next, using a series of if/else statements, each method is executed with the proper type and number of arguments. Pay special attention to the way that the overloaded Set( ) method is executed by the following code:
if(m.Name.Equals("Set", StringComparison.Ordinal) &&
pi[0].ParameterType == typeof(int)) {
object[] args = new object[2];
args[0] = 9;
args[1] = 18;
m.Invoke(reflectOb, args);
}
else if(m.Name.Equals("Set", StringComparison.Ordinal) &&
pi[0].ParameterType == typeof(double)) {
object[] args = new object[2];
args[0] = 1.12;
args[1] = 23.4;
m.Invoke(reflectOb, args);
}
If the name of the method is Set, then the type of the first parameter is tested to determine which version of the method was found. If it was Set(int, int), then int arguments are loaded into args. Otherwise, double arguments are used.
In the previous example, there is no advantage to using reflection to invoke methods on MyClass since an object of type MyClass was explicitly created. It would be easier to just call its methods normally. However, the power of reflection starts to become apparent when an object is created dynamically at runtime. To do this, you will need to first obtain a list of the constructors. Then, you will create an instance of the type by invoking one of the constructors. This mechanism allows you to instantiate an object at runtime without naming it in a declaration statement.
To obtain the public, non-static constructors for a type, call GetConstructors( ) on a Type object. One commonly used form is shown here:
ConstructorInfo[ ] GetConstructors( )
It returns an array of ConstructorInfo objects that describe the constructors.
ConstructorInfo is derived from the abstract class MethodBase, which inherits MemberInfo. It also defines several members of its own. The method we are interested in is GetParameters( ), which returns a list of the parameters associated with a constructor. It works just like GetParameters( ) defined by MethodInfo, described earlier.
Once an appropriate constructor has been found, an object is created by calling the Invoke( ) method defined by ConstructorInfo. One form is shown here:
object Invoke(object[ ] parameters)
Any arguments that need to be passed to the method are specified in the array parameters. If no arguments are needed, pass null to parameters. In all cases, parameters must contain exactly the same number of elements as there are arguments and the types of arguments must be compatible with the types of the parameters. Invoke( ) returns a reference to the object that was constructed.
The following program uses reflection to create an instance of MyClass:
// Create an object using reflection.
using System;
using System.Reflection;
class MyClass {
int x;
int y;
public MyClass(int i) {
Console.WriteLine("Constructing MyClass(int, int). ");
x = y = i;
}
public MyClass(int i, int j) {
Console.WriteLine("Constructing MyClass(int, int). ");
x = i;
y = j;
Show();
}
public int Sum() {
return x+y;
}
public bool IsBetween(int i) {
if((x < i) && (i < y)) return true;
else return false;
}
public void Set(int a, int b) {
Console.Write("Inside Set(int, int). ");
x = a;
y = b;
Show();
}
// Overload Set.
public void Set(double a, double b) {
Console.Write("Inside Set(double, double). ");
x = (int) a;
y = (int) b;
Show();
}
public void Show() {
Console.WriteLine("Values are x: {0}, y: {1}", x, y);
}
}
class InvokeConsDemo {
static void Main() {
Type t = typeof(MyClass);
int val;
// Get constructor info.
ConstructorInfo[] ci = t.GetConstructors();
Console.WriteLine("Available constructors: ");
foreach(ConstructorInfo c in ci) {
// Display return type and name.
Console.Write(" " + t.Name + "(");
// Display parameters.
ParameterInfo[] pi = c.GetParameters();
for(int i=0; i < pi.Length; i++) {
Console.Write(pi[i].ParameterType.Name +
" " + pi[i].Name);
if(i+1 < pi.Length) Console.Write(", ");
}
Console.WriteLine(")");
}
Console.WriteLine();
// Find matching constructor.
int x;
for(x=0; x < ci.Length; x++) {
ParameterInfo[] pi = ci[x].GetParameters();
if(pi.Length == 2) break;
}
if(x == ci.Length) {
Console.WriteLine("No matching constructor found.");
return;
}
else
Console.WriteLine("Two-parameter constructor found.\n");
// Construct the object.
object[] consargs = new object[2];
consargs[0] = 10;
consargs[1] = 20;
object reflectOb = ci[x].Invoke(consargs);
Console.WriteLine("\nInvoking methods on reflectOb.");
Console.WriteLine();
MethodInfo[] mi = t.GetMethods();
// Invoke each method.
foreach(MethodInfo m in mi) {
// Get the parameters.
ParameterInfo[] pi = m.GetParameters();
if(m.Name.Equals("Set", StringComparison.Ordinal) &&
pi[0].ParameterType == typeof(int)) {
// This is Set(int, int).
object[] args = new object[2];
args[0] = 9;
args[1] = 18;
m.Invoke(reflectOb, args);
}
else if(m.Name.Equals("Set", StringComparison.Ordinal) &&
pi[0].ParameterType == typeof(double)) {
// This is Set(double, double).
object[] args = new object[2];
args[0] = 1.12;
args[1] = 23.4;
m.Invoke(reflectOb, args);
}
else if(m.Name.Equals("Sum", StringComparison.Ordinal)) {
val = (int) m.Invoke(reflectOb, null);
Console.WriteLine("sum is " + val);
}
else if(m.Name.Equals("IsBetween", StringComparison.Ordinal)) {
object[] args = new object[1];
args[0] = 14;
if((bool) m.Invoke(reflectOb, args))
Console.WriteLine("14 is between x and y");
}
else if(m.Name.Equals("Show")) {
m.Invoke(reflectOb, null);
}
}
}
}
Available constructors:
MyClass(Int32 i)
MyClass(Int32 i, Int32 j)
Two-parameter constructor found.
Constructing MyClass(int, int).
Values are x: 10, y: 20
Invoking methods on reflectOb.
sum is 30
14 is between x and y
Inside Set(int, int). Values are x: 9, y: 18
Inside Set(double, double). Values are x: 1, y: 23
Values are x: 1, y: 23
Let’s look at how reflection is used to construct a MyClass object. First, a list of the public constructors is obtained using the following statement:
ConstructorInfo[] ci = t.GetConstructors();
Next, for the sake of illustration, the constructors are displayed. Then the list is searched for a constructor that takes two arguments, using this code:
for(x=0; x < ci.Length; x++) {
ParameterInfo[] pi = ci[x].GetParameters();
if(pi.Length == 2) break;
}
If the constructor is found (as it will be in this case), an object is instantiated by the following sequence:
// Construct the object.
object[] consargs = new object[2];
consargs[0] = 10;
consargs[1] = 20;
object reflectOb = ci[x].Invoke(consargs);
After the call to Invoke( ), reflectOb will refer to an object of type MyClass. The program then executes methods on that instance.
One important point needs to be made. In this example, for the sake of simplicity, it was assumed that the only two-argument constructor was one that took two int arguments. Obviously, in real-world code this would need to be verified by checking the parameter type of each argument.
In the preceding example, everything about MyClass has been discovered using reflection except for one item: the type MyClass, itself. That is, although the preceding examples dynamically determined information about MyClass, they still relied upon the fact that the type name MyClass was known in advance and used in a typeof statement to obtain a Type object upon which all of the reflection methods either directly or indirectly operated. Although this might be useful in a number of circumstances, the full power of reflection is found when the types available to a program are determined dynamically by analyzing the contents of other assemblies.
As you know from Chapter 16, an assembly carries with it type information about the classes, structures, and so on, that it contains. The Reflection API allows you to load an assembly, discover information about it, and create instances of any of its publicly available types. Using this mechanism, a program can search its environment, utilizing functionality that might be available without having to explicitly define that functionality at compile time. This is an extremely potent, and exciting, concept. For example, you can imagine a program that acts as a “type browser,” displaying the types available on a system. Another application could be a design tool that lets you visually “wire together” a program that is composed of the various types supported by the system. Since all information about a type is discoverable, there is no inherent limitation to the ways reflection can be applied.
To obtain information about an assembly, you will first create an Assembly object. The Assembly class does not define a public constructor. Instead, an Assembly object is obtained by calling one of its methods. The one we will use is LoadFrom( ), which loads an assembly given its filename. The form we will use is shown here:
static Assembly LoadFrom(string assemblyFile)
Here, assemblyFile specifies the filename of the assembly.
Once you have obtained an Assembly object, you can discover the types that it defines by calling GetTypes( ) on it. Here is its general form:
Type[ ] GetTypes( )
It returns an array of the types contained in the assembly.
To demonstrate the discovery of types in an assembly, you will need two files. The first will contain a set of classes that will be discovered by the second. To begin, create a file called MyClasses.cs that contains the following:
// A file that contains three classes. Call this file MyClasses.cs.
using System;
class MyClass {
int x;
int y;
public MyClass(int i) {
Console.WriteLine("Constructing MyClass(int). ");
x = y = i;
Show();
}
public MyClass(int i, int j) {
Console.WriteLine("Constructing MyClass(int, int). ");
x = i;
y = j;
Show();
}
public int Sum() {
return x+y;
}
public bool IsBetween(int i) {
if((x < i) && (i < y)) return true;
else return false;
}
public void Set(int a, int b) {
Console.Write("Inside Set(int, int). ");
x = a;
y = b;
Show();
}
// Overload Set.
public void Set(double a, double b) {
Console.Write("Inside Set(double, double). ");
x = (int) a;
y = (int) b;
Show();
}
public void Show() {
Console.WriteLine("Values are x: {0}, y: {1}", x, y);
}
}
class AnotherClass {
string msg;
public AnotherClass(string str) {
msg = str;
}
public void Show() {
Console.WriteLine(msg);
}
}
class Demo {
static void Main() {
Console.WriteLine("This is a placeholder.");
}
}
This file contains MyClass, which we have been using in the previous examples. It also adds a second class called AnotherClass and a third class called Demo. Thus, the assembly produced by this program will contain three classes. Next, compile this file so the file MyClasses.exe is produced. This is the assembly that will be interrogated.
The program that will discover information about MyClasses.exe is shown here. Enter it at this time.
/* Locate an assembly, determine types, and create
an object using reflection. */
using System;
using System.Reflection;
class ReflectAssemblyDemo {
static void Main() {
int val;
// Load the MyClasses.exe assembly.
Assembly asm = Assembly.LoadFrom("MyClasses.exe");
// Discover what types MyClasses.exe contains.
Type[] alltypes = asm.GetTypes();
foreach(Type temp in alltypes)
Console.WriteLine("Found: " + temp.Name);
Console.WriteLine();
// Use the first type, which is MyClass in this case.
Type t = alltypes[0]; // use first class found
Console.WriteLine("Using: " + t.Name);
// Obtain constructor info.
ConstructorInfo[] ci = t.GetConstructors();
Console.WriteLine("Available constructors: ");
foreach(ConstructorInfo c in ci) {
// Display return type and name.
Console.Write(" " + t.Name + "(");
// Display parameters.
ParameterInfo[] pi = c.GetParameters();
for(int i=0; i < pi.Length; i++) {
Console.Write(pi[i].ParameterType.Name +
" " + pi[i].Name);
if(i+1 < pi.Length) Console.Write(", ");
}
Console.WriteLine(")");
}
Console.WriteLine();
// Find matching constructor.
int x;
for(x=0; x < ci.Length; x++) {
ParameterInfo[] pi = ci[x].GetParameters();
if(pi.Length == 2) break;
}
if(x == ci.Length) {
Console.WriteLine("No matching constructor found.");
return;
}
else
Console.WriteLine("Two-parameter constructor found.\n");
// Construct the object.
object[] consargs = new object[2];
consargs[0] = 10;
consargs[1] = 20;
object reflectOb = ci[x].Invoke(consargs);
Console.WriteLine("\nInvoking methods on reflectOb.");
Console.WriteLine();
MethodInfo[] mi = t.GetMethods();
// Invoke each method.
foreach(MethodInfo m in mi) {
// Get the parameters.
ParameterInfo[] pi = m.GetParameters();
if(m.Name.Equals("Set", StringComparison.Ordinal) &&
pi[0].ParameterType == typeof(int)) {
// This is Set(int, int).
object[] args = new object[2];
args[0] = 9;
args[1] = 18;
m.Invoke(reflectOb, args);
}
else if(m.Name.Equals("Set", StringComparison.Ordinal) &&
pi[0].ParameterType == typeof(double)) {
// This is Set(double, double).
object[] args = new object[2];
args[0] = 1.12;
args[1] = 23.4;
m.Invoke(reflectOb, args);
}
else if(m.Name.Equals("Sum", StringComparison.Ordinal)) {
val = (int) m.Invoke(reflectOb, null);
Console.WriteLine("sum is " + val);
}
else if(m.Name.Equals("IsBetween", StringComparison.Ordinal)) {
object[] args = new object[1];
args[0] = 14;
if((bool) m.Invoke(reflectOb, args))
Console.WriteLine("14 is between x and y");
}
else if(m.Name.Equals("Show", StringComparison.Ordinal)) {
m.Invoke(reflectOb, null);
}
}
}
}
The output from the program is shown here:
Found: MyClass
Found: AnotherClass
Found: Demo
Using: MyClass
Available constructors:
MyClass(Int32 i)
MyClass(Int32 i, Int32 j)
Two-parameter constructor found.
Constructing MyClass(int, int).
Values are x: 10, y: 20
Invoking methods on reflectOb.
sum is 30
14 is between x and y
Inside Set(int, int). Values are x: 9, y: 18
Inside Set(double, double). Values are x: 1, y: 23
Values are x: 1, y: 23
As the output shows, all three classes contained within MyClasses.exe were found. The first one, which in this case was MyClass, was then used to instantiate an object and execute methods.
The types in MyClasses.exe are discovered using this sequence of code, which is near the start of Main( ):
// Load the MyClasses.exe assembly.
Assembly asm = Assembly.LoadFrom("MyClasses.exe");
// Discover what types MyClasses.exe contains.
Type[] alltypes = asm.GetTypes();
foreach(Type temp in alltypes)
Console.WriteLine("Found: " + temp.Name);
You can use such a sequence whenever you need to dynamically load and interrogate an assembly.
On a related point, an assembly need not be an exe file. Assemblies can also be contained in dynamic link library (DLL) files that use the dll extension. For example, if you were to compile MyClasses.cs using this command line,
csc /t:library MyClasses.cs
then the output file would be MyClasses.dll. One advantage to putting code into a DLL is that no Main( ) method is required. All exe files require an entry point, such as Main( ), that defines where execution begins. This is why the Demo class contained a placeholder Main( ) method. Such a method is not required by a DLL. If you try making MyClass into a DLL, you will need to change the call to LoadFrom( ) as shown here:
Assembly asm = Assembly.LoadFrom("MyClasses.dll");
Before we leave the topic of reflection, one last example will be instructive. Even though the preceding program was able to fully use MyClass without explicitly specifying MyClass in the program, it still relied upon prior knowledge of the contents of MyClass. For example, the program knew the names of its methods, such as Set and Sum. However, using reflection it is possible to utilize a type about which you have no prior knowledge. To do this, you must discover all information necessary to construct an object and to generate method calls. Such an approach would be useful to a visual design tool, for example, because it could utilize the types available on the system.
To see how the full dynamic discovery of a type can be accomplished, consider the following example, which loads the MyClasses.exe assembly, constructs a MyClass object, and then calls all of the methods declared by MyClass, all without assuming any prior knowledge:
// Utilize MyClass without assuming any prior knowledge.
using System;
using System.Reflection;
class ReflectAssemblyDemo {
static void Main() {
int val;
Assembly asm = Assembly.LoadFrom("MyClasses.exe");
Type[] alltypes = asm.GetTypes();
Type t = alltypes[0]; // use first class found
Console.WriteLine("Using: " + t.Name);
ConstructorInfo[] ci = t.GetConstructors();
// Use first constructor found.
ParameterInfo[] cpi = ci[0].GetParameters();
object reflectOb;
if(cpi.Length > 0) {
object[] consargs = new object[cpi.Length];
// Initialize args.
for(int n=0; n < cpi.Length; n++)
consargs[n] = 10 + n * 20;
// Construct the object.
reflectOb = ci[0].Invoke(consargs);
} else
reflectOb = ci[0].Invoke(null);
Console.WriteLine("\nInvoking methods on reflectOb.");
Console.WriteLine();
// Ignore inherited methods.
MethodInfo[] mi = t.GetMethods(BindingFlags.DeclaredOnly |
BindingFlags.Instance |
BindingFlags.Public) ;
// Invoke each method.
foreach(MethodInfo m in mi) {
Console.WriteLine("Calling {0} ", m.Name);
// Get the parameters.
ParameterInfo[] pi = m.GetParameters();
// Execute methods.
switch(pi.Length) {
case 0: // no args
if(m.ReturnType == typeof(int)) {
val = (int) m.Invoke(reflectOb, null);
Console.WriteLine("Result is " + val);
}
else if(m.ReturnType == typeof(void)) {
m.Invoke(reflectOb, null);
}
break;
case 1: // one arg
if(pi[0].ParameterType == typeof(int)) {
object[] args = new object[1];
args[0] = 14;
if((bool) m.Invoke(reflectOb, args))
Console.WriteLine("14 is between x and y");
else
Console.WriteLine("14 is not between x and y");
}
break;
case 2: // two args
if((pi[0].ParameterType == typeof(int)) &&
(pi[1].ParameterType == typeof(int))) {
object[] args = new object[2];
args[0] = 9;
args[1] = 18;
m.Invoke(reflectOb, args);
}
else if((pi[0].ParameterType == typeof(double)) &&
(pi[1].ParameterType == typeof(double))) {
object[] args = new object[2];
args[0] = 1.12;
args[1] = 23.4;
m.Invoke(reflectOb, args);
}
break;
}
Console.WriteLine();
}
}
}
Here is the output produced by the program:
Using: MyClass
Constructing MyClass(int).
Values are x: 10, y: 10
Invoking methods on reflectOb.
Calling Sum
Result is 20
Calling IsBetween
14 is not between x and y
Calling Set
Inside Set(int, int). Values are x: 9, y: 18
Calling Set
Inside Set(double, double). Values are x: 1, y: 23
Calling Show
Values are x: 1, y: 23
The operation of the program is straightforward, but a couple of points are worth mentioning. First, notice that only the methods explicitly declared by MyClass are obtained and used. This is accomplished by using the BindingFlags form of GetMethods( ). The reason for this is to prevent calling the methods inherited from object. Second, notice how the number of parameters and return type of each method are obtained dynamically. A switch statement determines the number of parameters. Within each case, the parameter type(s) and return type are checked. A method call is then constructed based on this information.
C# allows you to add declarative information to a program in the form of an attribute. An attribute defines additional information (metadata) that is associated with a class, structure, method, and so on. For example, you might define an attribute that determines the type of button that a class will display. Attributes are specified between square brackets, preceding the item to which they apply. Thus, an attribute is not a member of a class. Rather, an attribute specifies supplemental information that is attached to an item.
An attribute is supported by a class that inherits System.Attribute. Thus, all attribute classes must be subclasses of Attribute. Although Attribute defines substantial functionality, this functionality is not always needed when working with attributes. By convention, attribute classes often use the suffix Attribute. For example, ErrorAttribute would be a name for an attribute class that described an error.
When an attribute class is declared, it is preceded by an attribute called AttributeUsage. This built-in attribute specifies the types of items to which the attribute can be applied. Thus, the usage of an attribute can be restricted to methods, for example.
In an attribute class, you will define the members that support the attribute. Often attribute classes are quite simple, containing just a small number of fields or properties. For example, an attribute might define a remark that describes the item to which the attribute is being attached. Such an attribute might look like this:
[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
string pri_remark; // underlies Remark property
public RemarkAttribute(string comment) {
pri_remark = comment;
}
public string Remark {
get {
return pri_remark;
}
}
}
Let’s look at this class, line by line.
The name of this attribute is RemarkAttribute. Its declaration is preceded by the AttributeUsage attribute, which specifies that RemarkAttribute can be applied to all types of items. Using AttributeUsage, it is possible to narrow the list of items to which an attribute can be attached, and we will examine its capabilities later in this chapter.
Next, RemarkAttribute is declared and it inherits Attribute. Inside RemarkAttribute there is one private field, pri_remark, which supports one public, read-only property: Remark. This property holds the description that will be associated with the attribute. (Remark could also have been declared as an auto-implemented property with a private set accessor, but a read-only property is used for the purposes of illustration.) There is one public constructor that takes a string argument and assigns it to Remark.
At this point, no other steps are needed, and RemarkAttribute is ready for use.
Once you have defined an attribute class, you can attach the attribute to an item. An attribute precedes the item to which it is attached and is specified by enclosing its constructor inside square brackets. For example, here is how RemarkAttribute can be associated with a class:
[RemarkAttribute("This class uses an attribute.")]
class UseAttrib {
// ...
}
This constructs a RemarkAttribute that contains the comment, “This class uses an attribute.” This attribute is then associated with UseAttrib.
When attaching an attribute, it is not actually necessary to specify the Attribute suffix. For example, the preceding class could be declared this way:
[Remark("This class uses an attribute.")]
class UseAttrib {
// ...
}
Here, only the name Remark is used. Although the short form is correct, it is usually safer to use the full name when attaching attributes, because it avoids possible confusion and ambiguity.
Once an attribute has been attached to an item, other parts of the program can retrieve the attribute. To retrieve an attribute, you will often use one of two methods. The first is GetCustomAttributes( ), which is defined by MemberInfo and inherited by Type. It retrieves a list of all attributes attached to an item. Here is one of its forms:
object[ ] GetCustomAttributes(bool inherit)
If inherit is true, then the attributes of all base classes through the inheritance chain will be included. Otherwise, only those attributes defined for the specified type will be found.
The second method is GetCustomAttribute( ), which is defined by Attribute. One of its forms is shown here:
static Attribute GetCustomAttribute(MemberInfo element, Type attributeType)
Here, element is a MemberInfo object that describes the item for which the attributes are being obtained. The attribute desired is specified by attributeType. You will use this method when you know the name of the attribute you want to obtain, which is often the case. For example, assuming that the UseAttrib class has the RemarkAttribute, to obtain a reference to the RemarkAttribute, you can use a sequence like this:
// Get a MemberInfo instance associated with a
// class that has the RemarkAttribute.
Type t = typeof(UseAttrib);
// Retrieve the RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
Attribute.GetCustomAttribute(t, tRemAtt);
This sequence works because MemberInfo is a base class of Type. Thus, t is a MemberInfo instance.
Once you have a reference to an attribute, you can access its members. This makes information associated with an attribute available to a program that uses an element to which an attribute is attached. For example, the following statement displays the Remark property:
Console.WriteLine(ra.Remark);
The following program puts together all of the pieces and demonstrates the use of RemarkAttribute:
// A simple attribute example.
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
string pri_remark; // underlies Remark property
public RemarkAttribute(string comment) {
pri_remark = comment;
}
public string Remark {
get {
return pri_remark;
}
}
}
[RemarkAttribute("This class uses an attribute.")]
class UseAttrib {
// ...
}
class AttribDemo {
static void Main() {
Type t = typeof(UseAttrib);
Console.Write("Attributes in " + t.Name + ": ");
object[] attribs = t.GetCustomAttributes(false);
foreach(object o in attribs) {
Console.WriteLine(o);
}
Console.Write("Remark: ");
// Retrieve the RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
Attribute.GetCustomAttribute(t, tRemAtt);
Console.WriteLine(ra.Remark);
}
}
The output from the program is shown here:
Attributes in UseAttrib: RemarkAttribute
Remark: This class uses an attribute.
In the preceding example, RemarkAttribute was initialized by passing the description string to the constructor, using the normal constructor syntax. In this case, the comment parameter to RemarkAttribute( ) is called a positional parameter. This term relates to the fact that the argument is linked to a parameter by its position in the argument list. Thus, the first argument is passed to the first parameter, the second argument is passed to the second parameter, and so on.
However, for an attribute, you can also create named parameters, which can be assigned initial values by using their name. In this case, it is the name of the parameter, not its position, that is important.
NOTE Although named attribute parameters are conceptually similar to named arguments in methods, the specifics differ.
A named parameter is supported by either a public field or property, which must be read-write and non-static. Any such field or property is automatically able to be used as a named parameter. A named parameter is given a value by an assignment statement that is located within the argument list when the attribute’s constructor is invoked. Here is the general form of an attribute specification that includes named parameters:
[attrib (positional-param-list, named-param1 = value, named-param2 = value, ...)]
The positional parameters (if they exist) come first. Next, each named parameter is assigned a value. The order of the named parameters is not important. Named parameters do not need to be given a value. In this case, their default value will be used.
To understand how to use a named parameter, it is best to work through an example. Here is a version of RemarkAttribute that adds a field called Supplement, which can be used to hold a supplemental remark:
[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
string pri_remark; // underlies Remark property
// This can be used as a named parameter:
public string Supplement;
public RemarkAttribute(string comment) {
pri_remark = comment;
Supplement = "None";
}
public string Remark {
get {
return pri_remark;
}
}
}
As you can see, Supplement is initialized to the string “None” by the constructor. There is no way of using the constructor to assign it a different initial value. However, because Supplement is a public field of RemarkAttribute, it can be used as a named parameter, as shown here:
[RemarkAttribute("This class uses an attribute.",
Supplement = "This is additional info.")]
class UseAttrib {
// ...
}
Pay close attention to the way RemarkAttribute’s constructor is called. First, the positional argument is specified as it was before. Next is a comma, followed by the named parameter, Supplement, which is assigned a value. Finally, the closing ) ends the call to the constructor. Thus, the named parameter is initialized within the call to the constructor. This syntax can be generalized. Position parameters must be specified in the order in which they appear. Named parameters are specified by assigning values to their name.
Here is a program that demonstrates the Supplement field:
// Use a named attribute parameter.
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
string pri_remark; // underlies Remark property
public string Supplement; // this is a named parameter
public RemarkAttribute(string comment) {
pri_remark = comment;
Supplement = "None";
}
public string Remark {
get {
return pri_remark;
}
}
}
[RemarkAttribute("This class uses an attribute.",
Supplement = "This is additional info.")]
class UseAttrib {
// ...
}
class NamedParamDemo {
static void Main() {
Type t = typeof(UseAttrib);
Console.Write("Attributes in " + t.Name + ": ");
object[] attribs = t.GetCustomAttributes(false);
foreach(object o in attribs) {
Console.WriteLine(o);
}
// Retrieve the RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
Attribute.GetCustomAttribute(t, tRemAtt);
Console.Write("Remark: ");
Console.WriteLine(ra.Remark);
Console.Write("Supplement: ");
Console.WriteLine(ra.Supplement);
}
}
The output from the program is shown here:
Attributes in UseAttrib: RemarkAttribute
Remark: This class uses an attribute.
Supplement: This is additional info.
Before moving on, it is important to emphasize that pri_remark cannot be used as a named parameter because it is private to RemarkAttribute. The Remark property cannot be used as a named parameter because it is read-only. Remember that only public, read-write fields and properties can be used as named parameters.
A public, read-write property can be used as a named parameter in the same way as a field. For example, here an auto-implemented int property called Priority is added to RemarkAttribute:
// Use a property as a named attribute parameter.
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
string pri_remark; // underlies Remark property
public string Supplement; // this is a named parameter
public RemarkAttribute(string comment) {
pri_remark = comment;
Supplement = "None";
Priority = 1;
}
public string Remark {
get {
return pri_remark;
}
}
// Use a property as a named parameter.
public int Priority { get; set; }
}
[RemarkAttribute("This class uses an attribute.",
Supplement = "This is additional info.",
Priority = 10)]
class UseAttrib {
// ...
}
class NamedParamDemo {
static void Main() {
Type t = typeof(UseAttrib);
Console.Write("Attributes in " + t.Name + ": ");
object[] attribs = t.GetCustomAttributes(false);
foreach(object o in attribs) {
Console.WriteLine(o);
}
// Retrieve the RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
Attribute.GetCustomAttribute(t, tRemAtt);
Console.Write("Remark: ");
Console.WriteLine(ra.Remark);
Console.Write("Supplement: ");
Console.WriteLine(ra.Supplement);
Console.WriteLine("Priority: " + ra.Priority);
}
}
The output is shown here:
Attributes in UseAttrib: RemarkAttribute
Remark: This class uses an attribute.
Supplement: This is additional info.
Priority: 10
There is one point of interest in the program. Notice the attribute specified before UseAttrib that is shown here:
[RemarkAttribute("This class uses an attribute.",
Supplement = "This is additional info.",
Priority = 10)]
The named attributes Supplement and Priority are not in any special order. These two assignments can be reversed without any change to the attribute.
One last point: For both positional and named parameters, the type of an attribute parameter must be either one of the built-in primitive types, object, Type, an enumeration, or a one-dimensional array of one of these types.
C# defines many built-in attributes, but three are especially important because they apply to a wide variety of situations: AttributeUsage, Conditional, and Obsolete. They are examined here.
As mentioned earlier, the AttributeUsage attribute specifies the types of items to which an attribute can be applied. AttributeUsage is another name for the System.AttributeUsageAttribute class. AttributeUsage has the following constructor:
AttributeUsage(AttributeTargets validOn)
Here, validOn specifies the item or items upon which the attribute can be used. AttributeTargets is an enumeration that defines the following values:
Two or more of these values can be ORed together. For example, to specify an attribute that can be applied only to fields and properties, use
AttributeTargets.Field | AttributeTargets.Property
AttributeUsage supports two named parameters. The first is AllowMultiple, which is a bool value. If this value is true, then the attribute can be applied more than one time to a single item. The second is Inherited, which is also a bool value. If this value is true, then the attribute is inherited by derived classes. Otherwise, it is not inherited. The default setting is false for AllowMultiple and true for Inherited.
AttributeUsage also specifies a read-only property called ValidOn, which returns a value of type AttributeTargets, which specifies what types of items the attribute can be used on. The default is AttributeTargets.All.
The attribute Conditional is perhaps C#’s most interesting built-in attribute. It allows you to create conditional methods. A conditional method is invoked only when a specific symbol has been defined via #define. Otherwise, the method is bypassed. Thus, a conditional method offers an alternative to conditional compilation using #if.
Conditional is another name for System.Diagnostics.ConditionalAttribute. To use the Conditional attribute, you must include the System.Diagnostics namespace.
// Demonstrate the Conditional attribute.
#define TRIAL
using System;
using System.Diagnostics;
class Test {
[Conditional("TRIAL")]
void Trial() {
Console.WriteLine("Trial version, not for distribution.");
}
[Conditional("RELEASE")]
void Release() {
Console.WriteLine("Final release version.");
}
static void Main() {
Test t = new Test();
t.Trial(); // called only if TRIAL is defined
t.Release(); // called only if RELEASE is defined
}
}
The output from this program is shown here:
Trial version, not for distribution.
Let’s look closely at this program to understand why this output is produced. First, notice the program defines the symbol TRIAL. Next, notice how the methods Trial( ) and Release( ) are coded. They are both preceded with the Conditional attribute, which has this general form:
[Conditional symbol]
where symbol is the symbol that determines whether the method will be executed. If the symbol is defined, then when the method is called, it will be executed. If the symbol is not defined, then the method is not executed.
Inside Main( ), both Trial( ) and Release( ) are called. However, only TRIAL is defined. Thus, Trial( ) is executed. The call to Release( ) is ignored. If you define RELEASE, then Release( ) will also be called. If you remove the definition for TRIAL, then Trial( ) will not be called.
The Conditional attribute can also be applied to an attribute class (that is, a class that inherits Attribute). In this case, if the symbol is defined when the attribute is encountered during compilation, the attribute is applied. Otherwise, it is not.
Conditional methods have a few restrictions. First, they must return void. Second, they must be members of a class or structure, not an interface. Third, they cannot be preceded with the override keyword.
The Obsolete attribute, which is short for System.ObsoleteAttribute, lets you mark a program element as obsolete. Here is one of its forms:
[Obsolete(“message”)]
Here, message is displayed when that program element is compiled. Here is a short example:
// Demonstrate the Obsolete attribute.
using System;
class Test {
[Obsolete("Use MyMeth2, instead.")]
public static int MyMeth(int a, int b) {
return a / b;
}
// Improved version of MyMeth.
public static int MyMeth2(int a, int b) {
return b == 0 ? 0 : a /b;
}
static void Main() {
// Warning displayed for this.
Console.WriteLine("4 / 3 is " + Test.MyMeth(4, 3));
// No warning here.
Console.WriteLine("4 / 3 is " + Test.MyMeth2(4, 3));
}
}
When the call to MyMeth( ) is encountered in Main( ) when this program is compiled, a warning will be generated that tells the user to use MyMeth2( ) instead.
A second form of Obsolete is shown here:
[Obsolete(“message”, error)]
Here, error is a Boolean value. If it is true, then use of the obsolete item generates a compilation error rather than a warning. The difference is, of course, that a program containing an error cannot be compiled into an executable program.