C# 特性

Posted CoderPro

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C# 特性相关的知识,希望对你有一定的参考价值。

1. 特性的作用

使用特性,可以有效地将元数据或声明性信息与代码(程序集、类型、方法、属性等)相关联。将特性与程序实体相关联后,可以在运行时使用反射查询特性。

2. 特性具有的属性

  1. 特性向程序添加元数据。元数据是程序中定义的类型的相关信息。所有 .NET 程序集都包含一组指定的元数据,用于描述程序集中定义的类型和类型成员。可以添加自定义特性来指定所需的其他任何信息。
  2. 可以将一个或多个特性应用于整个程序集、模块或较小的程序元素(如类和属性)
  3. 特性可以像方法和属性一样接受自变量
  4. 程序可使用反射来检查自己的元数据或其他程序中的元数据

3. 特性使用

在 C# 中,通过用方括号 ([]) 将特性名称括起来,并置于应用该特性的实体的声明上方以指定特性。如下所示:

// Serializable指定当前类为可序列化对象
[Serializable]
public class SampleClass
{
    // Objects of this type can be serialized.
}
// 带参数形式,表示C#中使用到了非托管代码的方法
[System.Runtime.InteropServices.DllImport("user32.dll")]
extern static void SampleMethod();

特性类的类名约定以"Attribute"结尾,如DllImport特性的类名为"DllImportAttribute",但在使用时不同写"Attribute"

4. 特性目标

项目 说明
assembly 整个程序集
module module 当前程序集模块
field 类或结构中的字段
event 事件
method 方法或get和set属性访问器
param 方法参数或set属性访问器参数
property 属性
return 方法、属性索引器或get属性访问器的返回值
type 结构、类、接口、枚举或委托

更多模板类型参见System.AttributeTargets

5. 自定义特性

自定义特性需要继承Attribute,代码如下:

namespace CustomAttribute
{
    // 指定MyAttribute可以被哪些目标使用
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    class MyAttribute : Attribute
    {
        private string name;
        public double version;

        public MyAttribute(string name)
        {
            this.name = name;
            version = 1.0;
        }
    }
}

namespace CustomAttribute
{
 // 使用自定义特性
    [My("my",version = 1.1)]
    class UseMyAttribute
    {
    }
}

6. 特性常见用途

  1. 在 Web 服务中使用 WebMethod 特性标记方法,以指明方法应可通过 SOAP 协议进行调用。有关详细信息,请参阅 WebMethodAttribute
  2. 描述在与本机代码互操作时如何封送方法参数。有关详细信息,请参阅 MarshalAsAttribute
  3. 描述类、方法和接口的 COM 属性
  4. 使用 DllImportAttribute 类调用非托管代码
  5. 从标题、版本、说明或商标方面描述程序集
  6. 描述要序列化并暂留类的哪些成员
  7. 描述如何为了执行 XML 序列化在类成员和 XML 节点之间进行映射
  8. 描述的方法的安全要求
  9. 指定用于强制实施安全规范的特征
  10. 通过实时 (JIT) 编译器控制优化,这样代码就一直都易于调试
  11. 获取方法调用方的相关信息

7. 开发中常用 - 例子(根据个人经验)

  1. 使用反射访问特性代码示例如下:

    // 核心代码如下,此例子基于自定义属性的理解完成
    private static void PrintCustomAttributeInfo(Type t)
    {
        Console.WriteLine($"MyAttribute 信息 {t}");

        // 使用反射获取自定以特性
        Attribute[] attrs = Attribute.GetCustomAttributes(t);

        // 输出自定义属性内容
        foreach (Attribute attr in attrs)
        {
            if (attr is MyAttribute)
            {
                MyAttribute a = attr as MyAttribute;
                Console.WriteLine("  name: {0}, version {1:f}", a.GetVersionName(), a.version);
            }
        }
    }
  2. MarshalAsAttribute : MarshalAsAttribute属性指示如何在托管代码和非托管代码之间封送数据,代码示例参见DllImportAttribute调用非托管代码

  3. 使用DllImportAttribute 类调用非托管代码

    1. 创建非托管代码库,代码如下:

      HMODULE g_hModule;

      BOOL APIENTRY DllMain( HMODULE hModule, // 本模块句柄
                             DWORD  ul_reason_for_call, // 调用原因
                             LPVOID lpReserved // 隐式加载和显式加载
                           )

      {
          switch (ul_reason_for_call)
          {
              // 进程映射

              // 个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接的LoadLibrary或者LoadLibraryEx。

              // 当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为DLL_PROCESS_ATTACH,这种调用只会发生在第一次映射时。
              // 如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
              // 不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
          case DLL_PROCESS_ATTACH:
              g_hModule = hModule;
              break;
              // 线程映射
              // 当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数
              // 新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数
              // 注意跟DLL_PROCESS_ATTACH的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。
          case DLL_THREAD_ATTACH:
              break;
              // 线程卸载
              // 如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。
              // 注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
          case DLL_THREAD_DETACH:
              break;
              // 进程卸载
              // 当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。
              // DLL被从进程的地址空间解除映射的两种情况:
              // 1. FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary)
              // 2. 进程结束而解除DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。)
              // 注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保清理那些没有成功初始化的东西。
          case DLL_PROCESS_DETACH:
              break;
          }
          return TRUE;
      }

      //自定义导出函数
      __declspec(dllexport) void ExportFunc(LPCSTR pszContent)
      {
          char sz[MAX_PATH];
          ::GetModuleFileNameA(g_hModule, sz, MAX_PATH);
          ::MessageBoxA(NULL, pszContent, strrchr(sz, '\\') + 1, MB_OK);
      }
    2. C#中使用非托管代码,代码如下:

              [DllImport(@"..\..\..\Debug\CSharpCallbackTest.dll",CallingConvention = CallingConvention.Cdecl, EntryPoint = "ExportFunc")]
              public static extern void CallTest([MarshalAs(UnmanagedType.LPStr)] string content);

              static void Main(string[] args)
              
      {
                  CallTest("测试");
                  Console.ReadKey();
              }
  4. 调用方信息: 使用信息属性,可以获取有关方法调用方的信息。可以获取源代码的文件路径、源代码中的行号和调用方的成员名称。

    // 此示例代码来自微软官方文档
    // CallerFilePathAttribute  : 包含调用方的源文件的完整路径。 完整路径是编译时的路径
    // CallerLineNumberAttribute : 源文件中调用方法的行号
    // CallerMemberNameAttribute : 调用方的方法名称或属性名称
    public void DoProcessing()
    {
        TraceMessage("Something happened.");
    }

    public void TraceMessage(string message,
            [CallerMemberName] string memberName = "",
            [CallerFilePath] string sourceFilePath = "",
            [CallerLineNumber] int sourceLineNumber = 0
    )

    {
        Trace.WriteLine("message: " + message);
        Trace.WriteLine("member name: " + memberName);
        Trace.WriteLine("source file path: " + sourceFilePath);
        Trace.WriteLine("source line number: " + sourceLineNumber);
    }

    // Sample Output:
    //  message: Something happened.
    //  member name: DoProcessing
    //  source file path: c:\Visual Studio Projects\CallerInfoCS\CallerInfoCS\Form1.cs
    //  source line number: 31

以上是关于C# 特性的主要内容,如果未能解决你的问题,请参考以下文章

此 Canon SDK C++ 代码片段的等效 C# 代码是啥?

是否可以动态编译和执行 C# 代码片段?

C#常用代码片段备忘

优化 C# 代码片段、ObservableCollection 和 AddRange

Unity常用标签

VS2015使用技巧 打开代码片段C#部分