反射示例






4.80/5 (50投票s)
运行时类型发现的过程称为反射。使用反射,我们可以动态地获取元数据信息。
引言
运行时类型发现的过程称为反射。使用反射,我们可以动态地获取元数据信息。例如,我们可以获取给定程序集中包含的所有类型列表,包括方法、属性、字段、自定义属性、属性等。
System.Type
在深入研究反射之前,请先查看 System.Type
类,因为它是反射 API 的基础。
System.Type
类定义了许多可用于探索类型元数据信息的成员。它的许多成员返回类型都来自 System.Reflection
命名空间。例如,GetProperties()
返回 PropertyInfo[]
,GetFields()
返回 FieldInfo[]
等等。
System.Type
包含 IsAbstarct
、IsArray
、IsClass
、IsCOMObject
、IsValueType
、IsInterface
、IsPrimitive
、IsEnum
等属性,并且列表还在不断增加,这些属性允许我们查找有关当前类型的基本特征,例如它是否是 abstract
类、数组等。
它还包含 GetMethods()
、GetProperties()
、GetNestedTypes()
、GetInterfaces()
、GetEvents()
、GetConstructors()
等方法,允许获取表示我们正在探索的项目(方法、属性、接口等)的数组。
注意:这些方法中的每一个都有一个单数形式,例如 GetMethod()、GetProperty() 等,允许通过名称获取特定项。
System.Reflection
此命名空间包含检索有关程序集、模块、成员、参数和其他实体信息的类型。这些也可以用来操作已加载类型的实例,例如调用方法等。
System.Reflection 命名空间的一些成员的简要说明
Assembly
:一个abstract
类,包含许多static
方法,允许加载、检查和操作程序集。AssemblyName
:此类允许发现有关程序集的更多详细信息,例如版本信息、区域信息等。EventInfo
:一个abstract
类,保存给定事件的信息。FieldInfo
:一个abstract
类,保存给定字段的信息。MemberInfo
:这个abstract
类定义了EventInfo
、FieldInfo
、MethodInfo
和PropertyTypeInfo
的通用行为。MethodInfo
:一个abstract
类,保存给定方法的信息。Module
:一个abstract
类,允许访问多文件程序集中的给定模块。ParameterInfo
:一个abstract
类,保存给定参数的信息。PropertyInfo
:一个abstract
类,保存给定属性的信息。
获取类型引用
我们可以通过多种方式获取 Type
类的实例。
使用 System.Object.GetType()
System.Object
定义了一个名为 GetType()
的方法,该方法返回一个 Type
类的实例,该实例代表当前对象的元数据。
// Obtain type information using a Contact instance.
Contact contact = new Contact();
Type t = contact.GetType();
此方法仅在我们需要探索的类型具有编译时知识且当前在内存中拥有该类型实例时才有效。
使用 typeof()
获取类型元数据的另一种方法是使用 typeof
运算符。
// Get the type using typeof.
Type t = typeof(Contact);
此方法不需要创建对象实例来获取类型信息。
使用 System.Type.GetType()
Type.GetType()
以更灵活的方式提供类型信息。使用此方法,我们不需要具有类型的编译时知识,因为我们指定了要探索的类型的完全限定 string
名称。
Type.GetType()
方法已重载,可指定两个布尔参数,其中一个控制如果找不到类型是否应引发异常,另一个控制 string
的区分大小写。
// Obtain type information using the static Type.GetType() method
// (don't throw an exception if 'Contact' cannot be found and ignore case).
Type t = Type.GetType("UnderstandingReflection.Contact", false, true);
在此代码中,假设 type
定义在当前正在执行的程序集中。但是,当我们想要获取外部程序集中文档的元数据时,string
参数的格式是 type
的完全限定名称,后跟逗号,后跟包含 type
的程序集的友好名称。
// obtain type information for a type within an external assembly.
Type t = Type.GetType("UnderstandingReflection.Contact, ContactLib");
在 Type.GetType()
方法中,我们还可以使用加号(+)来表示嵌套类型。假设我们想获取一个名为 {Class_Name
} 的类中嵌套的类(ClassName
)的 type
信息。
// Obtain type information for a nested class
// within the current assembly.
Type t = Type.GetType("UnderstandingReflection.Contact+ContactOption");
获取方法信息
/// <summary>
/// To print all method with its return type and parameter info
/// </summary>
/// <param name="t">type to be reflect</param>
static void ListofMethodsWithParams(Type t)
{
Console.WriteLine("\n***** Methods *****\n");
MethodInfo[] methodInfos = t.GetMethods();//get all method of current type
StringBuilder paramInfo = new StringBuilder();
foreach (MethodInfo methodInfo in methodInfos)
{
// Get return type.
string retVal = methodInfo.ReturnType.FullName;
paramInfo.Append("( ");
// Get params.
foreach (ParameterInfo parameterInfo in methodInfo.GetParameters())
{
paramInfo.Append(string.Format("{0} {1} ", parameterInfo.ParameterType, parameterInfo.Name));
}
paramInfo.Append(" )");
// Now display the basic method signature.
Console.WriteLine(String.Format("{0} {1} {2}\n", retVal, methodInfo.Name, paramInfo.ToString()));
paramInfo.Clear();
}
}
在此方法中,传递了一个 Type
类型的参数。从此类型调用 GetMethod()
来获取所有方法,该方法返回一个 MethodInfo
实例的数组。现在遍历所有 methodInfo
,我们可以使用 methodInfo.ReturnType.FullName.
获取该方法的返回类型。现在要获取该方法的所有参数,我们调用 methodInfo.GetParameters().
从 parameterInfo
中,我们获取参数类型及其名称。
获取字段信息
/// <summary>
/// To print list of field with its type
/// </summary>
/// <param name="type">type to be reflect</param>
static void ListOfField(Type type)
{
Console.WriteLine("***********Field************");
//getting all field
FieldInfo[] fieldInfos = type.GetFields();
foreach (var fieldInfo in fieldInfos)
{
Console.WriteLine(String.Format
("{0} {1}\n", fieldInfo.FieldType.Name, fieldInfo.Name));
}
}
为了获取所有字段,我使用了 GetFields()
,它返回一个 FieldInfo[]
对象,从 fieldInfo
对象中,我们可以获取其信息。
在此方法中,我使用了 BindingFlags
(这是一个 enum
),它控制绑定以及反射用于搜索成员和类型的方式。这提供了对我们正在搜索内容的更大程度的控制(例如,仅 static
成员,仅 public
成员,包括 private
成员等)。
一些绑定标志是
BindingFlags.Public
:指定在搜索中包含public
成员。BindingFlags.Instance
:指定在搜索中包含instance
成员。BindingFlags.NonPublic
:指定在搜索中包含非public
成员。
获取属性信息
/// <summary>
/// To print list of property with its type
/// </summary>
/// <param name="type">type to be explore</param>
static void ListOfProperty(Type type)
{
Console.WriteLine("***********Property************");
//getting all property
PropertyInfo[] propertyInfos = type.GetProperties();
foreach (var propertyInfo in propertyInfos)
{
Console.WriteLine(String.Format
("{0} {1}\n", propertyInfo.PropertyType.Name, propertyInfo.Name));
}
}
获取各种统计数据
最后但并非最不重要的一点是,您还有最后一个辅助方法,它将简单地显示有关传入类型的各种统计信息(指示类型是否为泛型,基类是什么,类型是否为密封类,等等)。
// Just for Understanding
private void ListVariousStats(Type t)
{
Console.WriteLine( "\n***** Various Statistics *****\n");
Console.WriteLine( String.Format("Base class is: {0}\n", t.BaseType));
Console.WriteLine( String.Format("Is type sealed? {0}\n", t.IsSealed));
Console.WriteLine( String.Format("Is type generic? {0}\n", t.IsGenericTypeDefinition));
Console.WriteLine(String.Format("Is type a class type? {0}\n", t.IsClass));
}
示例应用程序
示例应用程序是一个 Web 应用程序,通过在运行时创建实例、基本数据库操作(CRUD 操作和存储过程创建)等方式来提供反射的使用。
因此,想法是执行需要数据库中存在存储过程的 CRUD 操作,并在调用存储过程时传递 SQL 参数。为了在 C# 应用程序中演示反射,我通过反射 API 执行所有操作,例如创建存储库实例、创建存储过程、创建 SqlParameter
、将 DataTable
转换为 List<T>
等。
首先,我为表示我的数据库表的类属性创建了自定义属性,以标识 Identity
列和通用列,并映射类到表。
我使用此属性将表名映射到类。
[AttributeUsage(AttributeTargets.Class,AllowMultiple=false)]
public class TableNameAttribute : Attribute
{
public string Name { get; set; }
public TableNameAttribute(string name)
{
this.Name = name;
}
}
此属性应用于类属性,以表示该属性是数据库中的标识列。
[AttributeUsage(AttributeTargets.Property,AllowMultiple=false)]
public class IdentityAttribute:Attribute
{
public bool IsID { get; set; }
public IdentityAttribute()
{
this.IsID = true;
}
}
此属性用于表示表中对应的列名。
[AttributeUsage(AttributeTargets.Property,AllowMultiple=false)]
public class DbColumnAttribute:Attribute
{
public string Name { get; set; }
public DbColumnAttribute(string name)
{
this.Name = name;
}
}
我就是这样在我的模型类中使用了这些属性。
[TableName("ContactDetails")]
public class Contact:IEntity
{
#region IEntity Members
[Identity]
[DbColumn("ID")]
public int ID { get; set; }
[DbColumn("GUID")]
public string GUID { get; set; }
#endregion
[DbColumn("PersonName")]
public string Name { get; set; }
[DbColumn("Address")]
public string Address { get; set; }
[DbColumn("EmailId")]
public string Email { get; set; }
}
获取 SqlParameter 列表
在此方法中,接收了一个对象实例,需要获取 SqlParameter
以传递给存储过程。首先,我获取所有 public
和非 static
属性,并通过迭代所有属性信息,将其添加到 SqlParameter
列表中。
private List<SqlParameter> GetAddParameter(object obj)
{
PropertyInfo[] fields = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
var sqlParams = new List<SqlParameter>();
foreach (var f in fields)
{
var cols = f.GetCustomAttributes(typeof(DbColumnAttribute), false) ;
if (cols.Length > 0 &&
f.GetCustomAttributes(typeof(IdentityAttribute), false).Length == 0)
{
var p = cols[0];
sqlParams.Add(new SqlParameter(((DbColumnAttribute)p).Name, f.GetValue(obj, null)));
}
}
return sqlParams;
}
获取表名
我创建了这个扩展方法来获取用于存储过程的对象表。
public static string GetTableName<T>(this T entity) where T : IEntity
{
//Getting TableNAmeAtrribute from entity
var tableNameAttr = entity.GetType().GetCustomAttributes(typeof(TableNameAttribute), false);
//If not found the TableNameAttribute raise an exception
if (tableNameAttr.Length == 0)
throw new ArgumentException(String.Format("{0}
Class has not been mapped to Database table with 'TableNameAttribute'.",
entity.GetType().Name));
//returning Table Name
return ((TableNameAttribute)tableNameAttr[0]).Name;
}
创建存储过程
我为数据库操作创建了存储过程,方法是调用
public static bool CreateAddEntityProcedure(object entity, SqlConnection con)
{
string tableColumnName = "", tableColNameForValue = "",
procedureParameter = "", spname = "";
string tableName = GetTableName(entity);
#region Building procedure
procedureParameter = CreateParameter(entity, out tableColumnName, out tableColNameForValue);
#region
spname = "Add" + tableName;
string spCreate = @"
-- =============================================
-- Author: Demo Reflection
-- Create date: {0}
-- Description: CREATING New Row in {1}
-- =============================================
CREATE PROCEDURE {2}
{3}
AS
BEGIN
INSERT INTO {4}
({5})
values
({6})
SELECT SCOPE_IDENTITY()
END";
#endregion
#endregion
string addProcedureDetails = String.Format(spCreate, DateTime.Now,
tableName, spname, procedureParameter, tableName, tableColumnName, tableColNameForValue);
return StoringProcedure(addProcedureDetails, con);
}
我创建了 CreateParameter
方法,用于以编程方式为实体创建存储过程。在此方法中,我们想要创建过程的对象应使用 TableNameAttribute
等属性进行装饰,以获取表名,并获取列名及其类型。
private static string CreateParameter(object entity, out string tableColName, out string tableValueParam)
{
if (!(entity is IEntity))
{
throw new ArgumentException("Invalid Entity to create Procedure");
}
StringBuilder parameter = new StringBuilder();
StringBuilder TableColumnName = new StringBuilder();
StringBuilder paramForValue = new StringBuilder();
//Getting the type of entity
var type = entity.GetType();
// Get the PropertyInfo object:
var properties = type.GetProperties();
foreach (var property in properties)
{
if (property.GetCustomAttributes(typeof(IdentityAttribute), false).Length > 0)
continue;
var attributes = property.GetCustomAttributes(typeof(DbColumnAttribute), false);
foreach (var attribute in attributes)
{
if (attribute.GetType() == typeof(DbColumnAttribute))
{
switch (property.PropertyType.FullName)
{
case "System.Int32":
TableColumnName.Append(String.Format("{0},", ((DbColumnAttribute)attribute).Name));
paramForValue.Append(String.Format("@{0},", ((DbColumnAttribute)attribute).Name));
parameter.AppendLine();
parameter.Append(String.Format("@{0} int=0,", ((DbColumnAttribute)attribute).Name));
break;
case "System.Int64":
TableColumnName.Append(String.Format("{0},", ((DbColumnAttribute)attribute).Name));
paramForValue.Append(String.Format("@{0},", ((DbColumnAttribute)attribute).Name));
parameter.AppendLine();
parameter.Append(String.Format("@{0} bigint=0,", ((DbColumnAttribute)attribute).Name));
break;
case "System.DateTime":
TableColumnName.Append(String.Format("{0},", ((DbColumnAttribute)attribute).Name));
paramForValue.Append(String.Format("@{0},", ((DbColumnAttribute)attribute).Name));
parameter.AppendLine();
parameter.Append(String.Format("@{0} datetime=1/1/0001,", ((DbColumnAttribute)attribute).Name));
break;
default:
TableColumnName.Append(String.Format("{0},", ((DbColumnAttribute)attribute).Name));
paramForValue.Append(String.Format("@{0},", ((DbColumnAttribute)attribute).Name));
parameter.AppendLine();
parameter.Append(String.Format("@{0} varchar(max)='',", ((DbColumnAttribute)attribute).Name));
break;
}
}
}
}
GetTableName(entity);
var allParams = parameter.ToString().Substring(0, parameter.ToString().LastIndexOf(","));
tableColName = TableColumnName.ToString().Substring(0, TableColumnName.ToString().LastIndexOf(","));
tableValueParam = paramForValue.ToString().Substring(0, paramForValue.ToString().LastIndexOf(","));
return allParams;
}
将数据保存到数据库
这就是我从 aspx 页面调用存储库实例来保存数据的方式。在此方法中,Activator.CreateInstance()
用于创建指定类型的实例。
protected void SaveEnity(object sender, EventArgs e)
{
//Creating an instance of typeEntity
var objEntity = Activator.CreateInstance(_typeEntity);
var allControls = PlaceHolder1.Controls;
var entityProps = _typeEntity.GetProperties(flag);
foreach (var control in allControls.Cast<object>().Where
(control => control.GetType() == typeof (TextBox)))
{
var control1 = control;
foreach (var props in entityProps.Where(props => props.Name == ((TextBox) control1).ID))
{
//Setting value in instance property
props.SetValue(objEntity, ((TextBox) control).Text, null);
break;
}
}
//Instance of Repository
var objRepo = Activator.CreateInstance(_typeRepository);
var allMethods = objRepo.GetType().GetMethods();
foreach (var method in allMethods.Where(method => method.Name == "Add"))
{
//Calling the Method of repository instance
method.Invoke(objRepo, new[] {objEntity});
break;
}
}
将表转换为 List<T>
在此扩展方法中,我做的是将 datatable
转换为其对应的对象列表,方法是匹配 datatable 的标题与类属性。这段代码实际上取自 这篇文章,并进行了一些修改。
public static List<T> ToList<T>(this DataTable dataTable) where T : new()
{
var dataList = new List<T>();
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic;
var objFieldNames = (from PropertyInfo aProp in typeof(T).GetProperties(flags)
where aProp.GetCustomAttributes(typeof(DbColumnAttribute),false).Length>0
select new
{
Name =((DbColumnAttribute)aProp.GetCustomAttributes(typeof(DbColumnAttribute), false)[0]).Name,
Type = Nullable.GetUnderlyingType(aProp.PropertyType) ?? aProp.PropertyType
}).ToList();
var dataTblFieldNames = (from DataColumn aHeader in dataTable.Columns
select new { Name = aHeader.ColumnName, Type = aHeader.DataType }).ToList();
var commonFields = objFieldNames.Intersect(dataTblFieldNames).ToList();
var fro = (from PropertyInfo aProp in typeof(T).GetProperties(flags)
where aProp.GetCustomAttributes(typeof(DbColumnAttribute), false).Length > 0
select aProp).ToList();
foreach (DataRow dataRow in dataTable.AsEnumerable().ToList())
{
var aTSource = new T();
int i = 0;
foreach (var aField in commonFields)
{
//here
PropertyInfo pi = aTSource.GetType().GetProperty(fro[i++].Name);
//PropertyInfo propertyInfos = aTSource.GetType().GetProperty(objFieldNames[i++].Name);
pi.SetValue(aTSource, dataRow[aField.Name] == DBNull.Value ? null : dataRow[aField.Name], null);
}
dataList.Add(aTSource);
}
return dataList;
}
性能和反射
反射是有代价的。但这个代价取决于实现(重复调用应进行缓存,例如:entity.GetType().GetProperty(BindingFlag.Public)
)。
例如,在本篇文章的代码示例中,有 GetAddParameter
、GetUpdateParameter
方法。在这两个方法中,我每次调用方法时都调用了 obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
。例如,如果我有一百万个 Widget,并且为其中每一个调用您的 GetAddParameter 方法,它将调用 GetType 和 GetProperties 一百万次,并且每次都会返回与所有其他时间完全相同的信息。这是极其浪费的。因此,对于这种情况,PIEBALDconsult 先生建议缓存此方法。所以我最终得到了 GetAddParameter
和 GetUpdateParameter
的最终版本。
private List<SqlParameter> GetAddParameter(object obj)
{
var cacheKeyName = ((T)obj).GetTableName();
var propertyInfos= MyCache.GetMyCachedItem(cacheKeyName);
if (propertyInfos == null)
{
propertyInfos = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
MyCache.AddToMyCache(cacheKeyName, propertyInfos, CacheItemPriority.Default);
}
var sqlParams= (from f in propertyInfos let cols = f.GetCustomAttributes(typeof (DbColumnAttribute), false)
where cols.Length > 0 && f.GetCustomAttributes(typeof (IdentityAttribute), false).Length == 0
let p = cols[0]
select new SqlParameter(((DbColumnAttribute) p).Name, f.GetValue(obj, null))).ToList();
return sqlParams;
}
private List<SqlParameter> GetUpdateParameter(object obj)
{
var cacheKeyName = ((T)obj).GetTableName();
var propertyInfos = MyCache.GetMyCachedItem(cacheKeyName);
if (propertyInfos == null)
{
propertyInfos = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
MyCache.AddToMyCache(cacheKeyName, propertyInfos, CacheItemPriority.Default);
}
var sqlParams= (from f in propertyInfos let cols = f.GetCustomAttributes(typeof (DbColumnAttribute), false)
where cols.Length > 0
let p = cols[0]
select new SqlParameter(((DbColumnAttribute) p).Name, f.GetValue(obj, null))).ToList();
return sqlParams;
}
在学习如何提高反射使用性能的过程中,我发现了很多有趣的文章。
引用反射既不好,也不慢。它只是一种工具。像所有工具一样,它在某些情况下非常有价值,而在其他情况下则价值不大。
关注点
通过这个例子,我学到的反射的主要价值在于它可用于检查程序集、类型和成员。它是确定未知程序集或对象内容的非常强大的工具,并且可以在各种情况下使用。
修订历史
- 2015-02-18:首次发布
- 2015-02-28:更新了示例代码,实现了缓存和重构。