[TOC]html
【微信平臺,此文僅受權《NCC 開源社區》訂閱號發佈】 本章的內容,主要是對屬性和字段進行賦值和讀值、自定義特性、將特性應用到實際場景。git
本文內容已經上傳到 https://gitee.com/whuanle/reflection_and_properties/blob/master/C%23反射與特性(7)自定義特性以及應用.cs正則表達式
第五篇中,介紹了成員方法的重載已經調用方式,第六篇中,對以往知識進行了總結以及實踐練習,這一節將介紹對屬性和字段的操做。編程
從前面咱們知道,經過反射能夠獲取到屬性 PropertyInfo 、字段 FieldInfo,在《C#反射與特性(三):反射類型的成員》的 1.2 獲取屬性、字段成員中,有詳細介紹。這裏再也不詳細贅述,下面正式進入話題。c#
PropertyInfo 中的 GetValue()
和 SetValue()
能夠得到或者設置 實例屬性和字段的值。微信
建立一個類型框架
public class MyClass { public string A { get; set; } }
編寫測試代碼ide
// 獲取 Type 以及 PropertyInfo Type type = typeof(MyClass); PropertyInfo property = type.GetProperty(nameof(MyClass.A)); // 實例化 MyClass object example1 = Activator.CreateInstance(type); object example2 = Activator.CreateInstance(type); // 對實例 example 中的屬性 A 進行賦值 property.SetValue(example1,"賦值測試"); property.SetValue(example2, "Natasha牛逼"); // 讀取實例中的屬性值 Console.WriteLine(property.GetValue(example1)); Console.WriteLine(property.GetValue(example2));
這裏要強調的是,反射中的類型調用操做(調用方法屬性等),必須是經過實例來完成。函數
那些 Type 、PropertyInfo 都是對元數據的讀取,只能讀,只有實例才能對程序產生影響。工具
從上面的操做中,咱們經過反射,建立兩個 example 實例,而後再經過反射對實例進行操做,實現讀值賦值。
屬性的值操做很是簡單,沒有別的內容要說明了。
在 ASP.NET Core 中,對於 Controller 和 Action ,咱們可使用 [HttpGet]
、[HttpPost]
、[HttpDelete]
等特性,定義請求類型以及路由地址。
在 EFCore 中,咱們可使用 [Key]
、[Required]
等特性,其它框架也有各類各樣的特性。
特性能夠用來修飾類、屬性、接口、結構、枚舉、委託、事件、方法、構造函數、字段、參數、返回值、程序集、類型參數和模塊等。
C# 中,預約義了三種特性類型:
名稱 | 類型 | 說明 |
---|---|---|
Conditional | 位映射特性 | 能夠映射到類型元數據的特定位上,public、abstract 以及 sealed 都會編譯爲位映射特性 |
AttributeUsage | 自定義特性 | 自定義的特性 |
Obsolete | 僞自定義特性 | 與自定義特性相似,但僞自定義特性會被編譯器或者CLR內部進行優化 |
位映射特性大多數只在空間中佔據一位空間,很是高效。
特性是一個類,繼承了 Attribute ,特性(類)的命名,必須以 Attribute
做爲後綴。
首先建立一個類繼承 System.Attribute
public class MyTestAttribute : Attribute { }
經過 AttributeUsageAttribute
限定定義特性能夠應用在哪一種類型上。
使用示例
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class MyTestAttribute : Attribute { }
AttributeUsageAttribute 定義一個特性時,大概格式以下
[AttributeUsage( validon, AllowMultiple=allowmultiple, Inherited=inherited )]
validon 指 AttributeTargets 枚舉,AttributeTargets 枚舉類型以下
枚舉 | 值 | 說明 |
---|---|---|
All | 32767 | 能夠對任何應用程序元素應用屬性 |
Assembly | 1 | 能夠對程序集應用屬性 |
Class | 4 | 能夠對類應用屬性 |
Constructor | 32 | 能夠對構造函數應用屬性 |
Delegate | 4096 | 能夠對委託應用屬性 |
Enum | 16 | 能夠對枚舉應用屬性 |
Event | 512 | 能夠對事件應用屬性 |
Field | 256 | 能夠對字段應用屬性 |
GenericParameter | 16384 | 能夠對泛型參數應用屬性。 目前,此屬性僅可應用於 C#、Microsoft 中間語言 (MSIL) 和已發出的代碼中 |
Interface | 1024 | 能夠對接口應用屬性 |
Method | 64 | 能夠對方法應用屬性 |
Module | 2 | 能夠對模塊應用屬性。 Module 引用的是可移植可執行文件(.dll 或 .exe),而不是 Visual Basic 標準模塊 |
Parameter | 2048 | 能夠對參數應用屬性 |
Property | 128 | 能夠對屬性 (Property) 應用屬性 (Attribute) |
ReturnValue | 8192 | 能夠對返回值應用屬性 |
Struct | 8 | 能夠對結構應用屬性,即值類型 |
AllowMultiple 標識是否容許在同一個地方屢次使用此特性,默認不容許。若是設置爲 true,則能夠在同一個屬性或字段等,屢次使用此特性。
Inherited 指派生類繼承一個使用此特性的類型時,是否容許派生類繼承此特性。例如 A 使用了此特性,B 繼承於 A,若是 Inherited = true
,則派生類也會擁有此特性。
特性能夠擁有構造函數和屬性字段等,這些信息經過使用特性時配置。
定義一個特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] public class MyTestAttribute : Attribute { private string A; public string Name { get; set; } public MyTestAttribute(string message) { A = message; } }
使用
public class MyClass { [MyTest("test", Name = "666")] public string A { get; set; } }
前面建立了自定義特性,而後就到了查找/檢索特性的環節。
可是這些步驟有什麼用處呢?做用於什麼場景呢?這裏先不用管,按照步驟作一次先。
檢索特性的方式有兩種
先定義特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] public class ATestAttribute : Attribute { public string NameA { get; set; } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] public class BTestAttribute : Attribute { public string NameB { get; set; } }
使用特性
[ATest(NameA = "Myclass")] public class MyClass { [Required] [EmailAddress] [ATest(NameA = "A")] public string A { get; set; } [Required] [EmailAddress] [ATest(NameA = "B")] [BTest(NameB = "BB")] public string B { get; set; } }
運行時檢索
Type type = typeof(MyClass); MemberInfo[] member = type.GetMembers(); // Type 或者 MemberInfo 的 GetCustomAttributes 方法 // Type.GetCustomAttributes() 獲取類型的特性 IEnumerable<Attribute> attrs = type.GetCustomAttributes(); Console.WriteLine(type.Name + "具備的特性:"); foreach (ATestAttribute item in attrs) { Console.WriteLine(item.NameA); } Console.WriteLine("**********"); // 循環每一個成員 foreach (MemberInfo item in member) { // 獲取每一個成員擁有的特性 var attrList = item.GetCustomAttributes(); foreach (Attribute itemNode in attrList) { // 若是是特性 ATestAttribute if (itemNode.GetType() == typeof(ATestAttribute)) Console.WriteLine(((ATestAttribute)itemNode).NameA); else if (itemNode.GetType() == typeof(BTestAttribute)) Console.WriteLine(((BTestAttribute)itemNode).NameB); else Console.WriteLine("這不是我定義的特性:" + itemNode.GetType()); } }
上面的自定義特性和 MyClass 類不做改變,將 Main 方法的代碼改爲以下
Type type = typeof(MyClass); // Attribute[] classAttr = Attribute.GetCustomAttributes(type); // 獲取類型的指定特性 Attribute classAttr = Attribute.GetCustomAttribute(type,typeof(ATestAttribute)); Console.WriteLine(((ATestAttribute)classAttr).NameA);
爲了學以至用,這裏實現一個數據驗證功能,可否檢查類型中的屬性是否符合要求。
要求實現:
可以檢查對象的屬性是否符合格式要求;
自定義驗證失敗消息;
動態實現
良好的編程風格和可拓展性
代碼完成後大約這個樣子(250行左右):
首先定義一個抽象特性類,做爲咱們自定義驗證的基礎類,方便後面實現拓展。
/// <summary> /// 自定義驗證特性的抽象類 /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public abstract class MyValidationAttribute : Attribute { private string Message; /// <summary> /// 驗證不經過時,提示信息 /// </summary> public string ErrorMessage { get { return string.IsNullOrEmpty(Message) ? "默認報錯" : Message; } set { Message = value; } } /// <summary> /// 檢查驗證是否經過 /// </summary> /// <param name="value"></param> /// <returns></returns> public virtual bool IsValid(object value) { return value == null ? false : true; } }
設計原理:
ErrorMessage 爲自定義的驗證失敗提示消息;若是使用時不填寫,默認爲 "默認報錯"
。
IsValid 指示自定義驗證特性類的驗證入口,經過此方法能夠檢查屬性是否經過了驗證。
基於 MyValidationAttribute ,咱們繼承後,開始實現不一樣類型的數據驗證。
這裏實現了四個驗證:非空驗證、手機號驗證、郵箱格式驗證、是否爲數字驗證。
/// <summary> /// 標識屬性或字段不能爲空 /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public class MyEmptyAttribute : MyValidationAttribute { /// <summary> /// 驗證是否爲空 /// </summary> /// <param name="value"></param> /// <returns></returns> public override bool IsValid(object value) { if (value == null) return false; if (string.IsNullOrEmpty(value.ToString())) return false; return true; } } /// <summary> /// 是不是手機號格式 /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public class MyPhoneAttribute : MyValidationAttribute { public override bool IsValid(object value) { if (value == null) return false; if (string.IsNullOrEmpty(value.ToString())) return false; string pattern = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$"; Regex regex = new Regex(pattern); return regex.IsMatch(value.ToString()); } } /// <summary> /// 是不是郵箱格式 /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public class MyEmailAttribute : MyValidationAttribute { public override bool IsValid(object value) { if (value == null) return false; if (string.IsNullOrEmpty(value.ToString())) return false; string pattern = @"^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$"; Regex regex = new Regex(pattern); return regex.IsMatch(value.ToString()); } } /// <summary> /// 是否全是數字 /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public class MyNumberAttribute : MyValidationAttribute { public override bool IsValid(object value) { if (value == null) return false; if (string.IsNullOrEmpty(value.ToString())) return false; string pattern = "^[0-9]*$"; Regex regex = new Regex(pattern); return regex.IsMatch(value.ToString()); } }
實現原理:
經過正則表達式去判斷屬性值是否符合格式(正則表達式都是我抄來的,筆者本人對正則表達式不熟)。
須要說明的是,上面的驗證代碼,仍是須要改進的,要適應各類類型的驗證。
檢查一個特性是否屬於咱們自定義驗證的特性。
若是不是的話,就不須要理會。
/// <summary> /// 檢查特性是否屬於 MyValidationAttribute 類型的特性 /// </summary> /// <param name="attribute">要檢查的特性</param> /// <returns></returns> private static bool IsMyValidationAttribute(Attribute attribute) { Type type = attribute.GetType(); return type.BaseType == typeof(MyValidationAttribute); }
實現原理:
咱們自定義的驗證特性類,都繼承了 MyValidationAttribute 類型,若是一個特性的父類不是 MyValidationAttribute
,那確定不是咱們實現的特性。
這裏涉及到屬性取值、方法調用等,咱們經過實例對象、特性對象、屬性對象三者去判斷一個屬性的值是否符合這個特性的要求。
/// <summary> /// 驗證此屬性是否經過驗證,只能驗證 繼承了 MyValidationAttribute 的屬性 /// </summary> /// <param name="attr">屬性帶有的特性</param> /// <param name="property">要驗證的屬性</param> /// <param name="obj">實例對象</param> /// <returns></returns> private static (bool, string) StartValid(Attribute attr, PropertyInfo property, object obj) { // 指定獲取實例對象的屬性值 object value = property.GetValue(obj); // 獲取特性的 IsValid 方法 MethodInfo attrMethod = attr.GetType().GetMethod("IsValid", new Type[] { typeof(object) }); // 獲取特性的 IsValid 屬性 PropertyInfo attrProperty = attr.GetType().GetProperty("ErrorMessage"); // 開始檢查,獲取檢查結果 bool checkResult = (bool)attrMethod.Invoke(attr, new object[] { value }); // 獲取特性的 ErrorMessage 屬性 string errorMessage = (string)attrProperty.GetValue(attr); // 經過驗證的話,就沒有報錯信息 if (checkResult == true) return (true, null); // 驗證不經過,返回預約義的信息 return (false, errorMessage); }
設計原理:
首先要驗證的屬性的值;
調用這個特性的 IsValid
方法,檢查值是否經過驗證;
獲取自定義的驗證失敗消息;
返回驗證結果;
咱們要實現一個功能:
解析對象的全部屬性,逐一對屬性進行檢索,使用到咱們設計的自定義驗證特性的屬性,就執行檢查,去獲取驗證結果。
/// <summary> /// 解析功能 /// </summary> /// <param name="list"></param> private static void Analysis(List<object> list) { foreach (var item in list) { Console.WriteLine("\n\n檢查對象屬性是否經過檢查"); // 獲取實例對象的類型 Type type = item.GetType(); // 獲取類的屬性列表 PropertyInfo[] properties = type.GetProperties(); // 對每一個屬性進行檢查,是否符合要求 foreach (PropertyInfo itemNode in properties) { Console.WriteLine($"\n屬性:{itemNode.Name},值爲 {itemNode.GetValue(item)}"); // 此屬性的全部特性 IEnumerable<Attribute> attList = itemNode.GetCustomAttributes(); if (attList != null) { // 開始對屬性進行特性驗證 foreach (Attribute itemNodeNode in attList) { // 若是不是咱們自定義的驗證特性,則跳過 if (!IsMyValidationAttribute(itemNodeNode)) continue; var result = StartValid(itemNodeNode, itemNode, item); // 驗證跳過,提示消息 if (result.Item1) { Console.WriteLine($"經過了 {itemNodeNode.GetType().Name} 驗證"); } // 沒經過驗證的話 else { Console.WriteLine($"未經過了 {itemNodeNode.GetType().Name} 驗證,報錯信息: {result.Item2}"); } } } Console.WriteLine("*****屬性分割線******"); } Console.WriteLine("########對象分割線########"); } }
設計原理:
上面有三個循環,第一個是沒什麼意義;
由於咱們的參數對象是一個對象列表,批量驗證對象,因此須要逐個對象進行分析;
第二個循環,是逐個獲取屬性;
第三個循環是逐個獲取屬性的特性;
上面消息獲取完畢,便可開始進行驗證。
這裏必須拿到三個參數:
咱們編寫一個模型類型,來使用自定義的驗證特性
public class User { [MyNumber(ErrorMessage = "Id必須所有爲數字")] public int Id { get; set; } [MyEmpty(ErrorMessage = "用戶名不能爲空")] public string Name { get; set; } [MyEmpty] [MyPhone(ErrorMessage = "這不是手機號")] public long Phone { get; set; } [MyEmpty] [MyEmail] public string Email { get; set; } }
使用方法跟 EFCore 的差很少,很是簡單。
你也能夠多建立幾個模型類進行測試。
咱們來實例化多個模型類並設置值,而後調用解析功能進行驗證。
在 Main 功能加上如下代碼:
List<object> users = new List<object>() { new User { Id = 0 }, new User { Id=1, Name="癡者工良", Phone=13510070650, Email="666@qq.com" }, new User { Id=2, Name="NCC牛逼", Phone=6666666, Email="NCC@NCC.NCC" } }; Analysis(users);
如無心外,執行結果應該是這樣的
檢查對象屬性是否經過檢查 屬性:Id,值爲 0 經過了 MyNumberAttribute 驗證 *****屬性分割線****** 屬性:Name,值爲 未經過了 MyEmptyAttribute 驗證,報錯信息: 用戶名不能爲空 *****屬性分割線****** 屬性:Phone,值爲 0 經過了 MyEmptyAttribute 驗證 未經過了 MyPhoneAttribute 驗證,報錯信息: 這不是手機號 *****屬性分割線****** 屬性:Email,值爲 未經過了 MyEmptyAttribute 驗證,報錯信息: 默認報錯 未經過了 MyEmailAttribute 驗證,報錯信息: 默認報錯 *****屬性分割線****** ########對象分割線######## 檢查對象屬性是否經過檢查 屬性:Id,值爲 1 經過了 MyNumberAttribute 驗證 *****屬性分割線****** 屬性:Name,值爲 癡者工良 經過了 MyEmptyAttribute 驗證 *****屬性分割線****** 屬性:Phone,值爲 13510070650 經過了 MyEmptyAttribute 驗證 經過了 MyPhoneAttribute 驗證 *****屬性分割線****** 屬性:Email,值爲 666@qq.com 經過了 MyEmptyAttribute 驗證 經過了 MyEmailAttribute 驗證 *****屬性分割線****** ########對象分割線######## 檢查對象屬性是否經過檢查 屬性:Id,值爲 2 經過了 MyNumberAttribute 驗證 *****屬性分割線****** 屬性:Name,值爲 NCC牛逼 經過了 MyEmptyAttribute 驗證 *****屬性分割線****** 屬性:Phone,值爲 6666666 經過了 MyEmptyAttribute 驗證 未經過了 MyPhoneAttribute 驗證,報錯信息: 這不是手機號 *****屬性分割線****** 屬性:Email,值爲 NCC@NCC.NCC 經過了 MyEmptyAttribute 驗證 經過了 MyEmailAttribute 驗證 *****屬性分割線****** ########對象分割線########
經過七篇文章的示例,估計你已經學會了反射的基礎操做和應用了吧?
本篇文章實現了特性的應用。
單純學會 「自定義特性」 ,沒有卵用,要學會如何利用特性去實現業務,纔有用處。
本篇對特性的使用, ORM 、ASP.NET Core 等都有常見的應用。
第六篇的時候,咱們實現了簡單的依賴注入和 Controller / Action 導航,利用本篇的內容,能夠修改第六篇實現的代碼,增長一個路由表的功能,訪問 URL 時,不須要經過 {/Controller/Action}
的路徑去訪問,能夠隨意映射 URL 規則。
原文出處:https://www.cnblogs.com/whuanle/p/12182962.html