65.9K
CodeProject 正在变化。 阅读更多。
Home

在您的 .NET 应用程序中创建和使用特性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (46投票s)

2002 年 2 月 10 日

BSD

9分钟阅读

viewsIcon

580157

downloadIcon

4848

展示了如何使用现有特性以及如何创建和使用自己的特性。

Screenshot of the console output, using the web palette for Colin and Paul :-)

目录

引言

在本文中,我希望向您展示特性是什么,如何使用现有的特性,以及如何创建您自己的特性供您在自己的项目中使用。

本文假设您了解或拥有什么

  • 您可以阅读 C# 代码
  • 您已安装了某种版本的 .NET Framework,我的演示程序使用的是最终发布版本,因此如果您有早期版本,则需要重新编译。

什么是特性

本质上,特性是在编译时用各种属性来修饰代码的一种方式。这可以是被动的,例如使用 SerializableAttribute 将类标记为 Serializable,也可以扮演更积极的角色,例如 MarshalAsAttribute,它告诉运行时如何在托管代码和非托管代码之间传递数据。

在前一种情况下,当您尝试序列化一个对象时,运行时会检查该对象是否应用了 SerializableAttribute。在这种情况下,该特性只不过是一个标志,用于告知运行时类的作者已批准序列化(因为这可能会向不应看到它的人暴露某些敏感数据)。

在后一种情况下,扮演的角色更为积极。当 MarshalAsAttribute 应用于结构中的字段时,它会告诉运行时在将 .NET 类型发送到非托管代码时应如何格式化,以及当返回值来自非托管代码时应将其转换为哪种类型。

void MyMethod([MarshalAs(LPStr)] string s);

上面的方法将字符串作为 LPStr(以零结尾的 ANSI 字符串)封送,字符串由运行时修改,以便函数 MyMethod 可以使用它。

所有特性都继承自 System.Attribute,并且与框架中 90% 的类一样,它们也是类。这也意味着您对类所能做的事情,对特性同样可以做到;给定一个特性的实例,您可以获得其底层类型。您还可以使用 Reflection.Emit 类在运行时创建特性,并将它们绑定到您使用 Emit 包构建的类。

如何使用特性

简单特性

在 C# 中,特性放置在它们将要“修饰”的项之前,并放在方括号 [] 中。

[Serializable()]
public class MySerializableClass
....

与 C# 一样,VB.NET 将特性放置在将要修饰的项之前,不同之处在于 VB.NET 使用尖括号 <>。

<Serializable()>
Public Class MySerializableClass
....

请注意,尽管该特性的实际名称是 SerializableAttribute,但我们没有写 Attribute?只有当您将 Attribute 应用于类时,才可以省略名称的 Attribute 部分。

上面的代码实际上扩展为

[SerializableAttribute()]
public class MySerializableClass

此缩写名称只能在应用特性时使用,在使用特性的代码中必须使用完整名称。

SerializableAttribute [] sa = (SerializableAttribute []) type.GetCustomAttributes(typeof(SerializableAttribute), false);

请注意,这里我必须提供完整名称,就像对待任何其他类一样。

更复杂的特性

MarshalAs 的构造函数定义为 public MarshalAsAttribute(UnmanagedType);。请注意,它只接受一个参数,那么其他属性呢?

这是关于特性的最令人困惑的部分,所以您可能需要做几个例子才能理解这部分,因为它偏离了常规的 C 风格方法,而进入了 VB 风格。

您在构造函数中设置属性,在所有构造函数参数之后。例如,我们将一个包含三个 32 位整数的数组传递给一个方法,我将省略实际的 void MyMethod 的代码,因为您在上面已经看到过了。

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.I4)]

我不想用 MarshalAs 的具体细节来让您头疼,我只是想用它作为例子,但您可以看到,第一个传入的是 UnmanagedType 枚举的一个成员,然后设置了两个属性 SizeConstArraySubType。所有特性都将这样工作,如果构造函数不允许您设置该属性,您可以通过在构造函数参数后进行设置。

VB 程序员会记得这叫做“按名称传递”。

创建您自己的特性

好的,您已经看到了如何使用特性,那么如何创建一个呢?

如果您正在使用 VS.NET,请创建一个名为 TestAttribute 的新控制台项目,并删除添加到其中的任何文件,我们将用我们自己的版本替换它们。如果您正在使用 .NET Framework SDK,只需创建一个新目录来存放该项目。

代码的注释版本可以在 源代码下载 中找到。

项目指南

我们将在这里创建的特性将在我们的演示项目中具有虚构的重要性。假设它作为我们程序的标志,以便对它所应用的类执行某些操作,数字表示要使用的测试方法。我们还希望拥有类名本身,但这并非必需。

将有两个测试类,一个使用默认名称,另一个指定其名称。

驱动类将搜索传入对象上的我们的特性。如果特性存在,它将打印出类名并调用特性的 PrintOut 方法。如果特性不存在,它将打印出一条消息说明其不存在。

打开一个名为 TestAttribute.cs 的新文件(如果您使用 VS.NET,请创建一个名为 TestAttribute.cs 的新文件),并将以下代码放入其中。

TestAttribute.cs

using System;

namespace Test
{
    public class TestAttribute : Attribute
    {
        public int TheNumber;

        public string Name;

        public TestAttribute(int number)
        {
            TheNumber = number;
        }

        public void PrintOut()
        {
            Console.WriteLine("\tTheNumber = {0}", TheNumber);
            Console.WriteLine("\tName = \"{0}\"", Name);
        }
    }
}

这就是您最基本的特性的全部内容,数据在其上定义,并且有一个单一的方法可以打印这些字段的值。正如您所见,它只是一个继承自 System.Attribute 的类,并且有两个公共字段,如果愿意,这些也可以是属性,但我的重点是尽可能少地编写代码。

现在让我们来使用它!

TestClasses.cs

创建一个名为 TestClasses.cs 的文件(如果您使用 VS.NET,请添加一个名为 TestClasses 的新类),并添加以下代码。

using System;

namespace Test
{
    [Test(3)]
    public class TestClassA
    {
        public TestClassA()
        {
        
        }
    }

    [Test(4, Name = "TestClassB")]
    public class TestClassB
    {
        public TestClassB()
        {

        }
    }
}

在这里,我们定义了两个类,我们的特性使用了它们。

现在让我们添加驱动程序,以便我们可以看到特性确实存在以及我们如何访问它!

Driver.cs

创建一个名为 Driver.cs 的新文件(如果您使用 VS.NET,请添加一个名为 Driver 的新类),并添加以下代码。

using System;

namespace Test
{
    public class Driver
    {
        public static void Main(string [] Args)
        {
            TestClassA a = new TestClassA();
            TestClassB b = new TestClassB();
            string c = "";

            PrintTestAttributes(a);        
            PrintTestAttributes(b);        
            PrintTestAttributes(c);        
        }

        public static void PrintTestAttributes(object obj)
        {
            Type type = obj.GetType();    

            TestAttribute [] AttributeArray = 
               (TestAttribute []) type.GetCustomAttributes(typeof(TestAttribute), 
                                                           false);
            
            Console.WriteLine("Class:\t{0}", type.Name);
            if( AttributeArray.Length == 0 )
            {
                Console.WriteLine("There are no TestAttributes applied to this class {0}",
                                  type.Name);
                return ;
            }

            TestAttribute ta = AttributeArray[0];

            ta.PrintOut();
        }
    }
}

现在在命令行上通过运行 csc 编译程序,如下所示:csc /out:test.exe /target:exe *.cs

执行程序时,您应该会看到类似顶部图像所示的输出。

工作原理

所有工作都由框架通过调用 Type 对象的 GetCustomAttributes 来完成。GetCustomAttributes 接受两个参数,第一个是我们要获取的特性的 Type 对象,第二个是一个布尔值,告诉框架是否应查看该类派生的类型。GetCustomAttributes 返回一个对象数组,其中包含匹配传入的 Type 的每个找到的特性。在这种情况下,我们请求了特定类型的​​所有特性,因此我们可以安全地将其转换为我们的特性的数组。至少,我们可以将其转换为 Attribute 的数组。如果您想获取附加到类型的 **所有** 特性,只需为布尔值传递 true。

如果您查找 GetCustomAttributes 方法,您会发现它定义在 MemberInfo 类上。这是一个抽象类,有几个派生类;TypeEventInfoMethodBasePropertyInfoFieldInfo。这些类中的每一个都代表类型系统的基础部分。现在,TestAttribute 可以应用于类的所有这些部分,如果 TestAttribute 只对类有意义呢?这时就有了 AttributeUsage 特性。

在获取了 TestAttribute 的数组后,它会确定数组中有多少个;如果数组中没有,我们就知道该类没有应用 TestAttribute;否则,我们取数组中的第一个,并通过我们在 TestAttribute 类中创建的 PrintOut 方法让它输出其值。

AttributeUsage 特性

此特性在编译时用于确保特性只能在使用特性的作者允许的地方使用。此特性还允许作者指定是否可以将同一个特性多次应用于同一个对象,以及该特性是否适用于派生自其应用的类的对象。

对 TestAttribute.cs 的简单修改将我们的特性限制为只能应用于类。

....
[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute : Attribute
....

现在 TestAttribute 只能应用于类。默认情况下,特性不会被继承,并且只能应用一次,因此我们无需对其执行任何其他操作。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]

上面的用法将允许该特性仅应用于类和结构。

下面是 AttributeTargets 枚举的所有标志的列表。该枚举被标记为 Flags,因此您可以组合两个或多个不同的值,以允许该特性应用于多个元素。

AttributeTargets
全部
程序集
构造函数
委托
枚举
事件
字段
接口
方法
Module (这指的是 .NET 可执行文件,而不是 VB 模块)
参数
属性
返回值
结构

想要一个脑筋急转弯吗? 查看 AttributeUsage 特性的文档,注意它本身也应用了 AttributeUsage 特性……那么哪个先出现?AttributeUsage 特性还是 AttributeUsage 特性!

总结

特性几乎可以附加到所有内容:类、方法、字段、属性、方法中的参数、方法的返回值;至少对特性的基本了解以及如何使用它们对长期来说非常有帮助。通过阅读本文,您应该已经学会了如何使用它们以及如何创建您可以在自己的代码中使用的基本特性。

一如既往,任何评论或问题都可以在下方发布或通过电子邮件发送。

更新

  • 2002 年 2 月 16 日
    • 添加了 AttributeTargets 枚举列表
    • 尝试澄清应用特性时使用的简写方法
    • 项目指南 中阐明了 Driver 的作用
  • 2002 年 2 月 19 日
    • 修复了下载链接;我使用 FrontPage 创建了基本 HTML,并且在添加链接时,我还重命名了 zip 文件以便在其他地方上传它们
© . All rights reserved.