您的位置:首页 > 编程语言 > C#

【引用】网上的一篇很好的介绍C#的attributes的文章

2007-12-21 04:55 555 查看
出处:http://www.brainbell.com/tutors/C_Sharp/Attributes.htm

Attributes

Most programming languages are designed with a given set of abilities in mind. For example, when you set out to design a compiler, you think about how an application written in the new language will be structured, how code will call other code, how functionality will be packaged, and many other issues that will make the language a productive medium for developing software. Most of what a compiler designer comes up with is static. For example, in C#, you define a class by placing the keyword class before the class name. You then signify derivation by inserting a colon after the class name followed by the name of the base class. This is an example of a decision that, once made by the language designer, can't be changed.

Now, the people who write compilers are darn good at what they do. However, even they can't anticipate all the future developments in our industry and how those developments will alter how programmers want to express their types in a given language. For example, how do you create the relationship between a class in C++ and a documentation URL for that class? Or how do you associate the specific members of a C++ class with the XML fields for your company's new business-to-business solution? Because C++ was designed many years before the advent of the Internet and protocols such as XML, it's not easy to perform either of these tasks.

Until now, the solutions to problems like these involved storing extra information in a separate file (DEF, IDL, and so on) that was then loosely associated with the type or member in question. Because the compiler has no knowledge of the separate file or the code-generated relationship between your class and the file, this approach is usually called a "disconnected solution." The main problem is that the class is no longer "self-describing"—that is, a user can no longer look at the class definition by itself and know everything about that class. One advantage of a self-describing component is that the compiler and run time can ensure that the rules associated with the component are adhered to. Additionally, a self-describing component is easier to maintain because the developer can see all the information related to the component in one place.

This has been the way of the world for many decades of compiler evolution. The language designers try to determine what you'll need the language to do, they design the compiler with those capabilities, and, for better or worse, those are the capabilities you have until another compiler comes along. That is, until now. C# offers a different paradigm, which stems from the introduction of a feature called attributes.

What attributes allow you to do is nothing short of groundbreaking. They provide you a generic means of associating information (as annotations) with your defined C# types. You can use attributes to define design-time information (such as documentation information), run-time information (such as the name of a database column for a field), or even run-time behavioral characteristics (such as whether a given member is "transactionable"-that is, capable of participating in a transaction). The possibilities are endless, which is rather the point. Because you can create an attribute based on any information you like, a standard mechanism exists for defining the attributes themselves and for querying the member or type at run time as to its attached attributes.

An example will better illustrate how to use this powerful feature. Let's say you have an application that stores some of its information in the Windows Registry. One design issue would be deciding where to store the Registry key information. In most development environments, this information is typically stored in a resource file or in constants or it's even hard-coded in the calls to the Registry APIs. However, once again, what we have is a situation in which an integral part of a class is being stored apart from the rest of the class's definition. Using attributes, we could "attach" this information to the class members such that we have a completely self-describing component. Here's an example of how that would look, assuming that we had already defined the RegistryKey attribute elsewhere: -

class MyClass
{
[RegistryKey(HKEY_CURRENT_USER, "foo")]
public int Foo;
}

To attach a defined attribute to a C# type or member, you simply specify the attribute data in brackets before the target type or member. Here we attached an attribute named RegistryKey to the MyClass.Foo field. At run time-which you'll see shortly-all we'll have to do is query the field as to its Registry key and then use that value to save the date into the Registry.

In the previous example, note that the syntax used to attach an attribute to a type or member looks a bit like the instantiation of a class. This is because an attribute is actually a class derived from the System.Attribute base class.

Now let's flesh out the RegistryKey attribute a little bit: -

public enum RegistryHives
{
HKEY_CLASSES_ROOT,
HKEY_CURRENT_USER,
HKEY_LOCAL_MACHINE,
HKEY_USERS,
HKEY_CURRENT_CONFIG
}
public class RegistryKeyAttribute : Attribute
{
public RegistryKeyAttribute(RegistryHives Hive, String ValueName)
{
this.Hive = Hive;
this.ValueName = ValueName;
}
protected RegistryHives hive;
public RegistryHives Hive
{
get { return hive; }
set { hive = value; }
}
protected String valueName;
public String ValueName
{
get { return valueName; }
set { valueName = value; }
}
}

What I've done here is add an enum for the different Registry types, a constructor for the attribute class (which takes a Registry type and a value name), and two properties for the Registry hive and value name. There's much more you can do when defining attributes, but at this point, because we know how to define and attach attributes, let's go ahead and learn how to query for attributes at run time. That way, we'll have a fully working example to play with. Once we've done that, we'll move on to some of the more advanced issues with defining and attaching attributes.

NOTE

Note that in the examples the attribute class names are appended with the word Attribute. However, when I then attach the attribute to a type or member, I don't include the Attribute suffix. This is a shortcut thrown in for free by the C# language designers. When the compiler sees that an attribute is being attached to a type or member, it will search for a System.Attribute derived class with the name of the attribute specified. If a class can't be located, the compiler will append Attribute to the specified attribute name and search for that. Therefore, it's common practice to define attribute class names as ending in Attribute and then to omit that part of the name.

We know how to define an attribute by deriving it from System.Attribute and how to attach it to a type or member. Now what? How can we use attributes in code? In other words, how can we query a type or member as to the attributes (and its parameters) that have been attached? -

To query a type or member about its attached attributes, we must use reflection. Reflection is an advanced topic that's covered in Chapter 16, "Querying Metadata with Reflection," so I'll discuss only enough about it here to illustrate what's needed to retrieve attribute information at run time. If you want to learn more about reflection, refer to Chapter 16.

Reflection is a feature that allows you to dynamically determine at run time the type characteristics for an application. For example, you can use the .NET Framework Reflection APIs to iterate through the metadata for an entire assembly and produce a list of all classes, types, and methods that have been defined for that assembly. Let's look at some examples of attributes and how they would be queried using reflection.

How you retrieve an attribute depends on the member type being queried. Let's say you want to define an attribute that will define the remote server on which an object is to be created. Without attributes, you'd save this information in a constant or in the application's resource file. Using attributes, you can simply annotate the class with its remote server like this: -

using System;
public enum RemoteServers
{
JEANVALJEAN,
JAVERT,
COSETTE
}
public class RemoteObjectAttribute : Attribute
{
public RemoteObjectAttribute(RemoteServers Server)
{
this.server = Server;
}
protected RemoteServers server;
public string Server
{
get
{
return RemoteServers.GetName(typeof(RemoteServers),
this.server);
}
}
}
[RemoteObject(RemoteServers.COSETTE)]
class MyRemotableClass
{
}

To determine the server on which to create the object, use code like the following: -

class ClassAttrApp
{
public static void Main()
{
Type type = typeof(MyRemotableClass);
foreach (Attribute attr in type.GetCustomAttributes())
{
RemoteObjectAttribute remoteAttr =
attr as RemoteObjectAttribute;
if (null != remoteAttr)
{
Console.WriteLine("Create this object on {0}.",
remoteAttr.Server);
}
}
}
}

As you might expect, the output from this application is the following: -

Create this object on COSETTE.

Because all variations of this example will use some common code, let's examine what's going on here regarding reflection and how it returns the attribute's value at run time.

The first line you'll notice in the Main method is the use of the typeof operator: -

Type type = typeof(MyRemotableClass);

This operator returns the System.Type object associated with the type that is passed as its only argument. Once you have that object, you can start to query it.

There are two parts to explain about the next line of code: -

foreach (Attribute attr in type.GetCustomAttributes(true))

The first is the call to the Type.GetCustomAttributes method. This method returns an array of type Attribute that in this case will contain all the attributes attached to the class named MyRemotableClass. The second is the foreach statement, which iterates the return array, stuffing each successive value into a variable (attr) of type Attribute.

The next statement uses the as operator to attempt to convert the attr variable to a type of RemoteObjectAttribte: -

RemoteObjectAttribute remoteAttr =
attr as RemoteObjectAttribute;

We then test for a null value, the result of a failed case via the as operator. If the value is not null-meaning that the remoteAttr variable holds a valid attribute attached to the MyRemotableClass type-we call one of the RemoteObjectAttribute properties to print the remote server name: -

if (null != remoteAttr)
{
Console.WriteLine("Create this object on {0}",
remoteAttr.Server);
}


Now that we've seen how to work with class attributes, let's look at using method attributes. This discussion is a separate section because the reflection code needed to query a method attribute is different from that needed to query a class attribute. In this example, we'll use an attribute that would be used to define a method as being transactionable: -

using System;
using System.Reflection;
public class TransactionableAttribute : Attribute
{
public TransactionableAttribute()
{
}
}
class TestClass
{
[Transactionable]
public void Foo()
{}
public void Bar()
{}
[Transactionable]
public void Baz()
{}
}
class MethodAttrApp
{
public static void Main()
{
Type type = Type.GetType("TestClass");
foreach(MethodInfo method in type.GetMethods())
{
foreach (Attribute attr in
method.GetCustomAttributes())
{
if (attr is TransactionableAttribute)
{
Console.WriteLine("{0} is transactionable.",
method.Name);
}
}
}
}
}

The code outputs the following: -

Foo is transactionable.
Baz is transactionable.

In this particular example, the mere presence of the TransactionableAttribute will be enough to tell the code that the method decorated with this attribute can belong in a transaction. That's why it's defined with only a bare-bones, parameterless constructor and no other members.

TestClass is then defined with three methods (Foo, Bar,and Baz) where two of them (Foo and Baz) are defined as transactionable. Notice that when attaching an attribute with a constructor that takes no parameters, you do not need to include the open and closed parentheses.

Now for the fun stuff. Let's look more closely at how we can query a class's methods as to the methods' attributes. We start off by using the static Type method GetType to obtain a System.Type object for the TestClass class: -

Type type = Type.GetType("TestClass");

Then we use the Type.GetMethods method to retrieve an array of MethodInfo objects. Each of these objects contains the information about a method in the TestClass class. Using a foreach statement, we iterate through every method: -

foreach(MethodInfo method in type.GetMethods())

Now that we have a MethodInfo object, we can use the MethodInfo.GetCustomAttributes method to retrieve all the user-created attributes for the method. Once again, we use a foreach statement to iterate through the returned array of objects: -

foreach (Attribute attr in method.GetCustomAttributes(true))

At this point in the code, we have an attribute for a method. Now, using the is operator, we query it as to whether it's a TransactionableAttribute attribute. If it is, we print the name of the method: -

if (attr is TransactionableAttribute)
{
Console.WriteLine("{0} is transactionable.",
method.Name);
}


In the last example of querying members as to their attached attributes, we'll look at how to query a class's fields. Let's say you have a class that contains some fields the values of which you want to save in the Registry. To do that, you could define an attribute with a constructor that takes as its parameters an enum representing the correct Registry hive and a string representing the Registry value name. You could then query the field at run time as to its Registry key: -

using System;
using System.Reflection;
public enum RegistryHives { HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS, HKEY_CURRENT_CONFIG } public class RegistryKeyAttribute : Attribute { public RegistryKeyAttribute(RegistryHives Hive, String ValueName) { this.Hive = Hive; this.ValueName = ValueName; } protected RegistryHives hive; public RegistryHives Hive { get { return hive; } set { hive = value; } } protected String valueName; public String ValueName { get { return valueName; } set { valueName = value; } } }class TestClass
{
[RegistryKey(RegistryHives.HKEY_CURRENT_USER, "Foo")]
public int Foo;
public int Bar;
}
class FieldAttrApp
{
public static void Main()
{
Type type = Type.GetType("TestClass");foreach(FieldInfo field in type.GetFields())
{
foreach (Attribute attr in field.GetCustomAttributes())
{
RegistryKeyAttribute registryKeyAttr =
attr as RegistryKeyAttribute;
if (null != registryKeyAttr)
{
Console.WriteLine
("{0} will be saved in {1}\\\\{2}",
field.Name,
registryKeyAttr.Hive,
registryKeyAttr.ValueName);
}
}
}
}
}

I won't walk through all of this code because some of it is duplicated from the previous example. However, a couple of details are important. First notice that just as a MethodInfo object is defined for retrieving method information from a type object, a FieldInfo object provides the same functionality for obtaining field information from a type object. As in the previous example, we start by obtaining the type object associated with our test class. We then iterate through the FieldInfo array, and for each FieldInfo object, we iterate through its attributes until we find the one we're looking for: RegistryKeyAttribute. If and when we locate that, we print the name of the field and retrieve from the attribute its Hive and ValueName fields.

In the examples above, I covered the use of attaching attributes by way of their constructors. Now I'll look at some issues regarding attribute constructors that I didn't cover earlier.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: