【微信平台,此文僅授權《NCC 開源社區》訂閱號發布】
本章的內容,主要是對屬性和字段進行賦值和讀值、自定義特性、將特性應用到實際場景。
本文內容已經上傳到 https://gitee.com/whuanle/reflection_and_properties/blob/master/C%23反射與特性(7)自定義特性以及應用.cs
1,屬性字段的賦值和讀值
第五篇中,介紹了成員方法的重載已經調用方式,第六篇中,對以往知識進行了總結以及實踐練習,這一節將介紹對屬性和字段的操作。
從前面我們知道,通過反射可以獲取到屬性 PropertyInfo 、字段 FieldInfo,在《C#反射與特性(三):反射類型的成員》的 1.2 獲取屬性、字段成員中,有詳細介紹。這里不再詳細贅述,下面正式進入話題。
PropertyInfo 中的 GetValue()
和 SetValue()
可以獲得或者設置 實例屬性和字段的值。
創建一個類型
public class MyClass
{
public string A { get; set; }
}
編寫測試代碼
// 獲取 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 實例,然后再通過反射對實例進行操作,實現讀值賦值。
屬性的值操作非常簡單,沒有別的內容要說明了。
2,自定義特性和特性查找
在 ASP.NET Core 中,對於 Controller 和 Action ,我們可以使用 [HttpGet]
、[HttpPost]
、[HttpDelete]
等特性,定義請求類型以及路由地址。
在 EFCore 中,我們可以使用 [Key]
、[Required]
等特性,其它框架也有各種各樣的特性。
特性可以用來修飾類、屬性、接口、結構、枚舉、委托、事件、方法、構造函數、字段、參數、返回值、程序集、類型參數和模塊等。
2.1 特性規范和自定義特性
C# 中,預定義了三種特性類型:
名稱 | 類型 | 說明 |
---|---|---|
Conditional | 位映射特性 | 可以映射到類型元數據的特定位上,public、abstract 以及 sealed 都會編譯為位映射特性 |
AttributeUsage | 自定義特性 | 自定義的特性 |
Obsolete | 偽自定義特性 | 與自定義特性類似,但偽自定義特性會被編譯器或者CLR內部進行優化 |
位映射特性大多數只在空間中占據一位空間,非常高效。
特性是一個類,繼承了 Attribute ,特性(類)的命名,必須以 Attribute
作為后綴。
2.1.1 定義特性
首先創建一個類繼承 System.Attribute
public class MyTestAttribute : Attribute
{
}
2.1.2 限制特性的使用
通過 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
,則派生類也會擁有此特性。
2.1.3 特性的構造函數和屬性
特性可以擁有構造函數和屬性字段等,這些信息通過使用特性時配置。
定義一個特性
[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; }
}
2.2 檢索特性
前面創建了自定義特性,然后就到了查找/檢索特性的環節。
但是這些步驟有什么用處呢?作用於什么場景呢?這里先不用管,按照步驟做一次先。
檢索特性的方式有兩種
- 調用 Type 或者 MemberInfo 的 GetCustomAttributes 方法;
- 調用 Attribute.GetCustomAttribute 或者 Attribute.GetCustomAttributes 方法;
2.2.1 方式一
先定義特性
[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());
}
}
2.2.2 方式二
上面的自定義特性和 MyClass 類不作改變,將 Main 方法的代碼改成如下
Type type = typeof(MyClass);
// Attribute[] classAttr = Attribute.GetCustomAttributes(type);
// 獲取類型的指定特性
Attribute classAttr = Attribute.GetCustomAttribute(type,typeof(ATestAttribute));
Console.WriteLine(((ATestAttribute)classAttr).NameA);
3,設計一個數據驗證工具
為了學以致用,這里實現一個數據驗證功能,能否檢查類型中的屬性是否符合要求。
要求實現:
-
能夠檢查對象的屬性是否符合格式要求;
-
自定義驗證失敗消息;
-
動態實現
-
良好的編程風格和可拓展性
代碼完成后大約這個樣子(250行左右):
3.1 定義抽象驗證特性類
首先定義一個抽象特性類,作為我們自定義驗證的基礎類,方便后面實現拓展。
/// <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 指示自定義驗證特性類的驗證入口,通過此方法可以檢查屬性是否通過了驗證。
3.2 實現多個自定義驗證特性
基於 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());
}
}
實現原理:
通過正則表達式去判斷屬性值是否符合格式(正則表達式都是我抄來的,筆者本人對正則表達式不熟)。
需要說明的是,上面的驗證代碼,還是需要改進的,要適應各種類型的驗證。
3.3 檢查特性是否屬於自定義驗證特性
檢查一個特性是否屬於我們自定義驗證的特性。
如果不是的話,就不需要理會。
/// <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
,那肯定不是我們實現的特性。
3.4 檢查屬性值是否符合自定義驗證特性的要求
這里涉及到屬性取值、方法調用等,我們通過實例對象、特性對象、屬性對象三者去判斷一個屬性的值是否符合這個特性的要求。
/// <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
方法,檢查值是否通過驗證; -
獲取自定義的驗證失敗消息;
-
返回驗證結果;
3.5 實現解析功能
我們要實現一個功能:
解析對象的所有屬性,逐一對屬性進行檢索,使用到我們設計的自定義驗證特性的屬性,就執行檢查,去獲取驗證結果。
/// <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("########對象分割線########");
}
}
設計原理:
上面有三個循環,第一個是沒什么意義;
因為我們的參數對象是一個對象列表,批量驗證對象,所以需要逐個對象進行分析;
第二個循環,是逐個獲取屬性;
第三個循環是逐個獲取屬性的特性;
上面消息獲取完畢,即可開始進行驗證。
這里必須拿到三個參數:
- 實例化的對象:反射的基礎是元數據,反射操作的基礎是實例對象;
- 類型的屬性 PropertyInfo :要通過 PropertyInfo 獲取到實例對象的屬性值;
- 特性對象 Attribute:從實例對象中獲取到的特性 Attribute 對象;
3.6 編寫一個模型類
我們編寫一個模型類型,來使用自定義的驗證特性
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 的差不多,非常簡單。
你也可以多創建幾個模型類進行測試。
3.7 執行驗證
我們來實例化多個模型類並設置值,然后調用解析功能進行驗證。
在 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 驗證
*****屬性分割線******
########對象分割線########
3.8 總結
通過七篇文章的示例,估計你已經學會了反射的基礎操作和應用了吧?
本篇文章實現了特性的應用。
單純學會 “自定義特性” ,沒有卵用,要學會如何利用特性去實現業務,才有用處。
本篇對特性的使用, ORM 、ASP.NET Core 等都有常見的應用。
第六篇的時候,我們實現了簡單的依賴注入和 Controller / Action 導航,利用本篇的內容,可以修改第六篇實現的代碼,增加一個路由表的功能,訪問 URL 時,不需要通過 {/Controller/Action}
的路徑去訪問,可以隨意映射 URL 規則。