在.NET中,反射(reflection)是一個運行庫類型發現的過程。使用反射服務,可以通過編程使用一個友好的對象模型得到與通過ildasm.exe顯示的相同元數據信息。
這段話引用自《精通C#》,通俗地講就是,通過反射我們能在運行時獲得程序或程序集所包含的所有類型的列表以及給定類型定義的方法、字段、屬性和事件;還可以動態發現一組給定類型支持的接口、方法的參數和其他相關細節。
1. 反射的一個最直觀的用法
我曾經在開發的時候遇到這樣一個例子:在一個類A中增加了一些field
,但是它們暫時不暴露給創建和修改類A的方法。但是又要保證這些field
是可以通過專門的API
來修改,所以要對每個field
增加一對API
來進行查看和修改。
然后我就寫啊寫啊,發現怎么這些API
都是做的相同的事情,唯一不同的是函數名和訪問的字段名。
所以,我就想,能不能只寫一個函數來搞定這些事情呢?
于是就想到了反射,通過傳進來參數表示要修改的字段名,然后動態地獲取到該字段,并對它進行賦值或者讀取它的值。
下面是一個簡單的實例:
namespace ReflectionEx1
{
public class ReflectionEx1
{
public int fieldInt { get; set; }
public double fieldDouble { get; set; }
public string fieldString { get; set; }
}
class Program
{
public static string GetFieldValue(string FieldName, object obj)
{
try
{
var property = obj.GetType().GetProperty(FieldName);
if (property != null)
{
object o = property.GetValue(obj, null);
return Convert.ToString(o);
}
Console.WriteLine("{0} does not have property {1}", obj.GetType(), FieldName);
return null;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
public static bool SetFieldValue(string FieldName, string Value, object obj)
{
try
{
var property = obj.GetType().GetProperty(FieldName);
if (property != null)
{
object v = Convert.ChangeType(Value, property.PropertyType);
property.SetValue(obj, v, null);
return true;
}
Console.WriteLine("{0} does not have property {1}", obj.GetType(), FieldName);
return false;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return false;
}
}
static void Main(string[] args)
{
var testClass = new ReflectionEx1();
if (SetFieldValue("fieldInt", "1", testClass))
{
Console.WriteLine(GetFieldValue("fieldInt", testClass));
}
if (SetFieldValue("fieldDouble", "2.5", testClass))
{
Console.WriteLine(GetFieldValue("fieldDouble", testClass));
}
if (SetFieldValue("fieldString", "test", testClass))
{
Console.WriteLine(GetFieldValue("fieldString", testClass));
}
}
}
}
這里所有的值都是通過string
的方式進行轉換的,所以在用SetFieldValue
方法時傳入的值是統一的string
類型。這樣在對某個域進行賦值時完全是看這個域的類型,然后嘗試把string
轉成那個類型。如果我們外面調用的時候傳入了錯誤的類型呢?
所以下面又寫了另外一個版本,通過泛型來支持不同的類型,而不是通過string
來轉換。這樣,當你試圖用string
對fieldInt
進行賦值時會出現下面的異常:
Object of type 'System.String' cannot be converted to type 'System.Int32'.
namespace ReflectionEx1
{
public class ReflectionEx1
{
public int fieldInt { get; set; }
public double fieldDouble { get; set; }
public string fieldString { get; set; }
}
class Program
{
public static T GetFieldValue<T>(string FieldName, object obj)
{
try
{
var property = obj.GetType().GetProperty(FieldName);
if (property != null)
{
return (T)property.GetValue(obj, null);
}
Console.WriteLine("{0} does not have property {1}", obj.GetType(), FieldName);
return default(T);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return default(T);
}
}
public static bool SetFieldValue<T>(string FieldName, T Value, object obj)
{
try
{
var property = obj.GetType().GetProperty(FieldName);
if (property != null)
{
property.SetValue(obj, Value, null);
return true;
}
Console.WriteLine("{0} does not have property {1}", obj.GetType(), FieldName);
return false;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return false;
}
}
static void Main(string[] args)
{
var testClass = new ReflectionEx1();
if (SetFieldValue("fieldInt", 1, testClass))
{
Console.WriteLine(GetFieldValue<int>("fieldInt", testClass));
}
if (SetFieldValue("fieldDouble", 2.5, testClass))
{
Console.WriteLine(GetFieldValue<double>("fieldDouble", testClass));
}
if (SetFieldValue("fieldString", "test", testClass))
{
Console.WriteLine(GetFieldValue<string>("fieldString", testClass));
}
}
}
}
不過,遺憾的是我最后并沒有使用反射。因為這些暴露出來的API
與類A是分離的東西,使用API
的人并不是十分清楚類A的結構。也就是說你可以告訴用戶,這個API是修改field1
的,但是你不能妄想他們清楚地知道field1
的名字(包括大小寫)。這里看起來有點不合乎邏輯,既然他們知道是修改field1
,為什么又不知道field1
。舉個例子,假設有一個布爾值field1
,它被用來控制類A是否具有功能x
。那么我們可以給用戶提供一個API
叫做EnableX()
,這個API
做的事情就是將field1
設置為true
(這個是不必暴露給用戶的)。
2. 構建自定義的元數據查看器
上面的一個簡單的例子可以看到我們可以在運行的時候通過反射,利用property的名字獲取它。下面我們將自定義一個元數據查看器,通過這個例子來看看反射還能做到什么。
(1)反射方法
我們可以通過Type
類的GetMethods()
函數得到一個MethodInfo[]
。下面我們利用它寫一個ListMethods()
方法,來輸出傳入類型的所有方法名。傳入的類型可以是C#
的內置類型(值類型和引用類型都行),也可以是自定義的類型。而調用ListMethods()
方法時傳的參數可以是利用對象的GetType()
函數,也可以使用typeof
運算符。
using System.Reflection;
namespace MyTypeViewer
{
struct TestStruct
{
public void Func3() { }
}
public class TestClass
{
public void Func1() { }
private void Func2() { }
}
class Program
{
static void ListMethods(Type type)
{
Console.WriteLine("***** Methods *****");
var methodNames = from m in type.GetMethods() select m.Name;
foreach (var name in methodNames)
{
Console.WriteLine("-> {0}", name);
}
Console.WriteLine();
}
static void Main(string[] args)
{
var test = new TestClass();
ListMethods(test.GetType());
ListMethods(typeof(TestStruct));
ListMethods(typeof(string));
}
}
}
下面的運行結果中由于string
類的方法太多,所以省略了部分沒有顯示。
***** Methods *****
-> Func1
-> ToString
-> Equals
-> GetHashCode
-> GetType
***** Methods *****
-> Func3
-> Equals
-> GetHashCode
-> ToString
-> GetType
***** Methods *****
// ignore some methods
-> CopyTo
-> ToCharArray
-> IsNullOrEmpty
-> IsNullOrWhiteSpace
-> Split
-> Substring
這里或許你會有一個疑問,不是獲取所有的方法嗎?為什么TestClass.Func2
沒有顯示呢?這是因為Func2
是私有方法,而GetMethods()
方法還有一個帶參數的重寫方法,參數為BindingFlags 枚舉。
public abstract MethodInfo[] GetMethods(
BindingFlags bindingAttr
)
所以要讀取私有的(private
或者protected
)方法,你需要在調用GetMethods()
時傳入參數BindingFlags.NonPublic | BindingFlags.Instance
,這樣上例的方法除了輸出Func2
之外,還會輸出下面兩個方法。這兩個方法是Object
類中的protected
方法,如果不想獲取它們,可以在上面的參數中再添加BindingFlags.DeclaredOnly
。
-> Finalize
-> MemberwiseClone
(2)反射字段和屬性
運行時獲取字段和屬性的方式跟方法類似,采用GetFileds()
函數。GetFileds()
方法返回當前 Type
的所有公共字段;GetFields(BindingFlags)
方法則可以使用指定綁定約束,搜索當前 Type
定義的字段, 它們返回的結果都是FieldInfo[]
類型。
namespace MyTypeViewer
{
public class TestClass
{
public int field1;
private string field2;
protected double field3;
public string property1 { get; set; }
public void Func1() { }
protected void Func2() { }
private void Func3() { }
}
class Program
{
static void Display(string title, IEnumerable<string> names)
{
Console.WriteLine("***** {0} *****", title);
foreach (var name in names)
{
Console.WriteLine("-> {0}", name);
}
Console.WriteLine();
}
static void ListMethods(Type type)
{
var methodNames = from m in type.GetMethods() select m.Name;
Display("Methods", methodNames);
}
static void ListFields(Type type)
{
var fieldNames = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Select(f => f.Name);
Display("Fields", fieldNames);
}
static void ListProperties(Type type)
{
var propertyNames = type.GetProperties().Select(f => f.Name);
Display("Properties", propertyNames);
}
static void Main(string[] args)
{
ListFields(typeof(TestClass));
}
}
}
輸出結果:
***** Fields *****
-> field1
-> field2
-> field3
-> <property1>k__BackingField
***** Properties *****
-> property1
在GetFields()
的返回結果中我們可以看到<property1>k__BackingField
,但是要真正獲取property1
的話需要采用GetProperties()
方法。
(3)反射實現的接口
反射實現的接口也是類似的情況,采用Type
類的GetInterfaces()
方法,它返回的是Type[]
。
namespace MyTypeViewer
{
interface ISampleInterface
{
void SampleMethod();
}
public class TestClass : ISampleInterface
{
void ISampleInterface.SampleMethod()
{
Console.WriteLine("implement interface member");
}
}
class Program
{
static void Display(string title, IEnumerable<string> names)
{
Console.WriteLine("***** {0} *****", title);
foreach (var name in names)
{
Console.WriteLine("-> {0}", name);
}
Console.WriteLine();
}
static void ListInterfaces(Type type)
{
var interfaces = type.GetInterfaces().Select(t => t.Name);
Display("Interfaces", interfaces);
}
static void Main(string[] args)
{
ListInterfaces(typeof(TestClass));
}
}
}
輸出結果:
***** Interfaces *****
-> ISampleInterface
(4)反射泛型類型
上面的例子中我們都采用typeof
操作符獲取某種類型,對于泛型類型,則必須指定T
,如ListMethods(typeof(List<string>));
是可以的,但是不能直接使用typeof(List)
。
另外我們還可以通過Type.GetType(typeName)
方法來獲取某種類型,如Type t = Type.GetType("System.Int32");
。這里必須使用完全限定名,而且要求大小寫完全匹配。如果使用"System.int32"
或者"Int32"
會找不到我們想要的類型,t
為空。
而對于泛型類型,若我們采用上述方法來獲取類型,則需要使用反勾號(`)加上數字值的語法來表示類型支持的參數個數。
例如,System.Collections.Generic.List<T>
可以寫成"System.Collections.Generic.List\
1"; 而
System.Collections.Generic.Dictionary<TKey, TValue>可以寫成
"System.Collections.Generic.Dictionary`2"`。
(5)反射方法參數和返回值
上面的所有示例中,我們都只返回了名字,其實我們還可以從MethodInfo
或者FieldInfo
或者Type
中獲取更多其他的信息。
下面我們以MethodInfo
為例,來豐富上面的ListMethods()
方法,除了列出所有方法的名字外,還輸出它們的參數(MethodInfo.GetParameters()
)和返回值(MethodInfo.ReturnType
)。
static void ListMethods(Type type)
{
var methods = type.GetMethods();
Console.WriteLine("***** Methods *****");
foreach (var m in methods)
{
string retVal = m.ReturnType.FullName;
var paramInfos = m.GetParameters().Select(p => string.Format("{0} {1}", p.ParameterType, p.Name));
string paramInfo = String.Join(", ", paramInfos.ToArray());
Console.WriteLine("-> {0} {1}( {2} )", retVal, m.Name, paramInfo);
}
}
輸出結果:
***** Methods *****
-> System.String get_property1( )
-> System.Void set_property1( System.String value )
-> System.String Func1( System.Int32 p1, System.Double p2 )
-> System.String ToString( )
-> System.Boolean Equals( System.Object obj )
-> System.Int32 GetHashCode( )
-> System.Type GetType( )
3. 自定義的元數據查看器有什么用
上面我們通過幾個例子可以看到Type
類型有很多功能很強大的方法可以查看元數據。那么,查看這些元數據有什么用呢?當你想動態使用這些類型時,它們就變得非常有用了,下面通過幾個常用的例子來說明如何使用。
(1)如何動態創建對象
動態創建對象可以使用Assembly.CreateInstance()
或Activator.CreateInstance()
方法。動態創建出來的對象與new
創建的對象一樣,只是前者不需要在編譯前就知道類的名字。
namespace MyTypeViewer
{
public class TestClass
{
public string property1 { get; set; }
public void Func1()
{
Console.WriteLine("Hello World!");
}
}
public static class ReflectionHelper
{
public static T CreateInstance<T>(string assemblyName, string nameSpace, string className)
{
try
{
string fullName = string.Fortmat("{0}.{1}", nameSpace, className);
object obj = Assembly.Load(assemblyName).CreateInstance(fullName);
return (T)obj;
}
catch
{
return default(T);
}
}
}
class Program
{
static void Main(string[] args)
{
var testInstance = ReflectionHelper.CreateInstance<TestClass>("MyTypeViewer", "MyTypeViewer", "TestClass");
testInstance.property1 = "Test Property";
testInstance.Func1();
}
}
}
(2)如何動態創建委托
CreateDelegate有很多種不同的重載方法,我們這里采用的是用MethodInfo
。
public static Delegate CreateDelegate(
Type type,
object firstArgument,
MethodInfo method
)
下面的例子來自MSDN
namespace CreateDelegate
{
class ExampleForm : Form
{
public ExampleForm()
: base()
{
this.Text = "Click me";
}
}
class Example
{
public static void Main()
{
Example ex = new Example();
ex.HookUpDelegate();
}
private void HookUpDelegate()
{
Assembly assem = typeof(Example).Assembly;
Type tExForm = assem.GetType("CreateDelegate.ExampleForm");
Object exFormAsObj = Activator.CreateInstance(tExForm);
EventInfo evClick = tExForm.GetEvent("Click");
Type tDelegate = evClick.EventHandlerType;
MethodInfo miHandler =
typeof(Example).GetMethod("LuckyHandler",
BindingFlags.NonPublic | BindingFlags.Instance);
Delegate d = Delegate.CreateDelegate(tDelegate, this, miHandler);
MethodInfo addHandler = evClick.GetAddMethod();
Object[] addHandlerArgs = { d };
addHandler.Invoke(exFormAsObj, addHandlerArgs);
Application.Run((Form)exFormAsObj);
}
private void LuckyHandler(Object sender, EventArgs e)
{
MessageBox.Show("This event handler just happened to be lying around.");
}
}
}
這里我們先動態創建了一個ExampleForm
窗體實例,然后取得它上面的Click
事件的委托類型tDelegate
,再根據該委托類型和我們的LuckyHandler
方法創建委托d
。最后,運行該程序時會彈出一個窗體,點擊任意位置就會調用LuckyHandler
方法。
4. 反射的應用實例1
為了更好地理解反射的機制,下面我們通過一個簡單的動態加載插件的實例來說明反射的作用。
(1)首先,我們建立一個簡單的ClassLibrary
來定義我們接下來要用到的插件的接口,編譯下面的這個接口我們將得到CommonTypeForPlugIn.dll
(里面定義了一個接口IFunctionality
和它的一個DoIt()
函數,以及一個屬性類CompanyInfoAttribute
)。
namespace CommonTypeForPlugIn
{
public interface IFunctionality
{
void DoIt();
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class CompanyInfoAttribute : Attribute
{
public string CompanyName { get; set; }
public string CompanyUrl { get; set; }
}
}
(2)在CommonTypeForPlugIn
的基礎上實現不同的插件,這里提供了Demo1
和Demo2
。
using CommonTypeForPlugIn; // You should add CommonTypeForPlugIn.dll to References first
namespace Demo1
{
[CompanyInfo(CompanyName = "Demo1", CompanyUrl = "http://demo1.com")]
public class Demo1 : IFunctionality
{
void IFunctionality.DoIt()
{
Console.WriteLine("Demo1 is in using now.");
}
}
}
using CommonTypeForPlugIn;
namespace Demo2
{
[CompanyInfo(CompanyName = "Demo2", CompanyUrl = "http://demo2.com")]
public class Demo2 : IFunctionality
{
void IFunctionality.DoIt()
{
Console.WriteLine("Demo2 is in using now.");
}
}
}
(3)接下來我們創建一個應用臺程序來實現動態加載上面不同的插件。
在PlugInExample
類里我們提供了DisplayPlugInsForCommonType()
和DisplayAndRunPlugIn()
兩個公共的函數用來分別顯示所有的實現CommonType
的插件(這里我們上面編譯生成的dll
都拷貝到這個控制臺程序的objd/debug/
目錄下新建的plugins
文件夾下了)和運行一個插件。
DisplayAndRunPlugIn(fileName)
函數根據用戶輸入的插件名去加載那個dll
,然后動態給插件里的實現IFunctionality
接口的類創建實例,并執行它的DoIt()
方法,最后動態獲取插件里的CompanyInfoAttribute
類,并輸出其中的特性值 DisplayCompanyInfo(t)
。
using System.IO;
using CommonTypeForPlugIn;
using System.Reflection;
namespace LearnCSharp
{
public class PlugInExample
{
public void DisplayPlugInsForCommonType()
{
var plugins = ListAllExternalModule();
if (plugins == null || !plugins.Any())
{
Console.WriteLine("No plugin provided.");
}
else
{
Console.WriteLine("List of all plugins:");
foreach (var plugin in plugins)
{
Console.WriteLine(Path.GetFileName(plugin));
}
}
}
public void DisplayAndRunPlugIn(string fileName)
{
if (fileName == "CommonTypeForPlugIn.dll")
{
Console.WriteLine("Dll {0} has no plug-in.", fileName);
}
else if (!LoadExternalModule(fileName))
{
Console.WriteLine("Load Dll {0} failed.", fileName);
}
}
private IEnumerable<string> ListAllExternalModule()
{
var directory = string.Format("{0}/plugins/", Directory.GetCurrentDirectory().TrimEnd('/'));
return Directory.GetFiles(directory, "*.dll");
}
private bool LoadExternalModule(string fileName)
{
Assembly plugInAsm = null;
var path = string.Format("{0}/plugins/{1}", Directory.GetCurrentDirectory().TrimEnd('/'), fileName);
try
{
plugInAsm = Assembly.LoadFrom(path);
}
catch (Exception ex)
{
Console.WriteLine("Failed to load assembly {0}, error: {1}", path, ex.Message);
return false;
}
var classTypes = plugInAsm.GetTypes().Where(t => t.IsClass && t.GetInterface("IFunctionality") != null);
foreach (Type t in classTypes)
{
IFunctionality func = (IFunctionality)plugInAsm.CreateInstance(t.FullName, true);
func.DoIt();
DisplayCompanyInfo(t);
}
return true;
}
private void DisplayCompanyInfo(Type t)
{
var companyInfos = t.GetCustomAttributes(false).Where(c => c.GetType() == typeof(CompanyInfoAttribute));
foreach (CompanyInfoAttribute companyInfo in companyInfos)
{
Console.WriteLine("CompanyName: {0}, CompanyUrl: {1}", companyInfo.CompanyName, companyInfo.CompanyUrl);
}
}
}
class Program
{
static void Main(string[] args)
{
var ex = new PlugInExample();
ex.DisplayPlugInsForCommonType();
Console.WriteLine("Please choose a dll to load:");
string fileName = Console.ReadLine();
ex.DisplayAndRunPlugIn(fileName);
}
}
}
輸出結果:
List of all plugins:
Demo1.dll
Demo2.dll
Please choose a dll to load:
Demo1.dll
Demo1 is in using now.
CompanyName: Demo1, CompanyUrl: http://demo1.com