C# 中的反射、特性与动态编程
在 C# 编程中,反射、特性和动态编程是非常重要的概念。下面将详细介绍这些概念及其应用。
1. 特性(Attributes)
特性是为程序元素(如类、方法等)添加额外元数据的一种方式。例如,
SerializableAttribute
就是一个特性,它在元数据表中对应一个设置位,属于伪特性。
以下是
SerializableAttribute
的 CIL 示例:
beforefieldinit Person
extends [mscorlib]System.Object
{
} // end of class Person
而一般的特性通常出现在类定义内部,示例如下:
.class private auto ansi beforefieldinit Person
extends [mscorlib]System.Object
{
} // end of class Person
2. 动态对象编程
C# 4.0 引入的动态对象简化了许多编程场景,并开启了一些之前无法实现的新场景。其核心是让开发者可以使用动态调度机制编写操作代码,运行时再由运行时环境解析,而非在编译时由编译器验证和绑定。
在很多情况下,对象本身并非静态类型,比如从 XML/CSV 文件、数据库表、Internet Explorer DOM、COM 的 IDispatch 接口加载数据,或者调用动态语言(如 IronPython 对象)的代码。C# 4.0 的动态对象支持为与这些没有编译时定义结构的运行时环境交互提供了通用解决方案。
C# 4.0 中动态对象的初始实现提供了四种绑定方法:
1. 对底层 CLR 类型使用反射。
2. 调用自定义的
IDynamicMetaObjectProvider
以获取
DynamicMetaObject
。
3. 通过 COM 的
IUnknown
和
IDispatch
接口进行调用。
4. 调用动态语言(如 IronPython)定义的类型。
下面重点介绍前两种方法。
2.1 使用 dynamic 调用反射
反射的一个关键特性是能够根据运行时确定的成员名称或其他属性(如特性),动态地查找并调用特定类型的成员。而 C# 4.0 引入的动态对象为通过反射调用成员提供了更简单的方式,但前提是在编译时需要知道成员的签名(包括参数数量以及指定参数是否与签名类型兼容)。
以下是一个使用 “反射” 进行动态编程的示例:
using System;
// ...
dynamic data =
"Hello! My name is Inigo Montoya";
Console.WriteLine(data);
data = (double)data.Length;
data = data*3.5 + 28.6;
if(data == 2.4 + 112 + 26.2)
{
Console.WriteLine(
"{0} makes for a long triathlon.", data);
}
else
{
data.NonExistentMethodCallStillCompiles()
}
// ...
该示例中,没有显式的代码来确定对象类型、查找特定的
MemberInfo
实例并调用它。而是将
data
声明为
dynamic
类型,直接调用其方法。在编译时,不会检查指定的成员是否可用,也不会检查动态对象的底层类型。只要语法有效,编译时可以进行任何调用。但类型安全并未完全放弃,对于标准 CLR 类型,在运行时会使用通常用于非动态类型的类型检查器。如果运行时没有可用的成员,调用将导致
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
。
需要注意的是,这种方式不如本章前面介绍的反射灵活,但 API 无疑更简单。使用动态对象的关键区别在于需要在编译时确定签名,而不是在运行时确定成员名称等信息。
2.2 dynamic 的原理和行为
通过上述示例和相关说明,可以总结出
dynamic
数据类型的几个特点:
-
编译器指令
:
dynamic
是给编译器的指令,用于生成代码。它涉及一种拦截机制,当运行时遇到动态调用时,会将请求编译为 CIL 代码,然后调用新编译的调用。
-
类型转换
:任何类型都可以转换为
dynamic
。存在从任何引用类型到
dynamic
的隐式转换,以及从值类型到
dynamic
的隐式装箱转换,还有从
dynamic
到
dynamic
的隐式转换。
-
转换依赖底层类型
:从
dynamic
对象转换为标准 CLR 类型需要显式转换。如果目标类型是值类型,则需要进行拆箱转换。转换是否成功取决于底层类型是否支持。
-
底层类型可改变
:与隐式类型变量
var
不同,
dynamic
的底层类型可以在不同赋值之间改变。这是因为在执行底层类型的代码之前,
dynamic
涉及一个编译的拦截机制。
-
运行时验证
:对底层类型上指定签名的验证在运行时进行。编译器不会验证动态类型的操作,这些工作留给运行时。如果代码从未执行,则不会进行成员的验证和绑定。
-
返回动态对象
:对动态对象的任何成员调用都将返回一个动态对象。但在运行时调用
GetType()
时,返回的是编译后的类型。
-
运行时异常
:如果运行时指定的成员不存在,运行时将抛出
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
异常。
-
不支持扩展方法
:与使用
System.Type
进行反射一样,使用
dynamic
进行反射不支持扩展方法。扩展方法仍可在实现类型上调用,但不能直接在扩展类型上调用。
-
本质是
System.Object
:
dynamic
本质上是
System.Object
。任何对象都可以成功转换为
dynamic
,
dynamic
也可以显式转换为其他对象类型。它的默认值为
null
,表明它是引用类型。其特殊的动态行为仅在调用时才会体现。
下面是
dynamic
调用成员的流程图:
graph TD;
A[动态调用] --> B[编译器声明 CallSite<T>变量];
B --> C[使用 Create() 方法实例化 CallSite<T>];
C --> D[调用 CallSite<T>.Target() 调用成员];
D --> E[运行时使用反射查找成员并验证签名];
E --> F[构建表达式树];
F --> G[编译表达式树生成 CIL 代码];
G --> H[将 CIL 代码注入调用点并调用];
3. 为什么使用动态绑定
除了反射,我们还可以定义自定义类型进行动态调用。例如,使用动态调用获取 XML 元素的值。
以下是使用强类型语法获取 XML 元素值的示例:
using System;
using System.Xml.Linq;
// ...
XElement person = XElement.Parse(
@"<Person>
<FirstName>Inigo</FirstName>
<LastName>Montoya</LastName>
</Person>");
Console.WriteLine("{0} {1}",
person.Descendants("FirstName").FirstOrDefault().Value,
person.Descendants("LastName").FirstOrDefault().Value);
// ...
而使用动态类型对象的替代方法如下:
using System;
// ...
dynamic person = DynamicXml.Parse(
@"<Person>
<FirstName>Inigo</FirstName>
<LastName>Montoya</LastName>
</Person>");
Console.WriteLine("{0} {1}",
person.FirstName, person.LastName);
// ...
虽然前一个示例的代码并不复杂,但后一个示例明显更简单。不过,这并不意味着动态编程就一定优于静态编译。
4. 静态编译与动态编程对比
前一个示例是完全静态类型的,编译时会验证所有类型及其成员签名,方法名必须匹配,所有参数都会进行类型兼容性检查。而后一个示例几乎没有静态类型的代码,
person
变量是动态的。因此,编译时不会验证
person
是否有
FirstName
或
LastName
属性,在 IDE 中编码时也没有智能感知提示成员信息。
虽然静态类型编程具有类型安全的优势,但在访问 XML 元素中的动态数据时,类型安全并没有太大优势。因为即使使用静态类型代码,编译器也不会验证用于标识元素名称的字符串是否正确,若元素不存在或大小写不匹配,运行时仍会抛出
NullReferenceException
。
综上所述,在某些情况下,类型安全无法或难以进行某些检查,此时使用仅在运行时验证的动态调用会使代码更易读、更简洁。当然,如果可以进行编译时验证,静态类型编程是首选,因为它可以伴随易读且简洁的 API。但在类型安全无效的情况下,C# 4.0 允许编写更简单的代码,而不是追求纯粹的类型安全。
5. 实现自定义动态对象
前面的示例中调用了
DynamicXml.Parse(...)
方法,它本质上是
DynamicXml
这个自定义类型的工厂方法。
DynamicXml
没有实现
FirstName
或
LastName
属性,因为这样会破坏在运行时从 XML 文件中动态检索数据的支持,而不是基于编译时实现 XML 元素。也就是说,
DynamicXml
不是通过反射访问其成员,而是根据 XML 内容动态绑定值。
定义自定义动态类型的关键是实现
System.Dynamic.IDynamicMetaObjectProvider
接口。通常,推荐的做法是从
System.Dynamic.DynamicObject
派生自定义动态类型,这样可以获得许多成员的默认实现,并可以覆盖不适用的成员。以下是完整的实现示例:
using System;
using System.Dynamic;
using System.Xml.Linq;
public class DynamicXml : DynamicObject
{
private XElement Element { get; set; }
public DynamicXml(System.Xml.Linq.XElement element)
{
Element = element;
}
public static DynamicXml Parse(string text)
{
return new DynamicXml(XElement.Parse(text));
}
public override bool TryGetMember(
GetMemberBinder binder, out object result)
{
bool success = false;
result = null;
XElement firstDescendant =
Element.Descendants(binder.Name).FirstOrDefault();
if (firstDescendant != null)
{
if (firstDescendant.Descendants().Count() > 0)
{
result = new DynamicXml(firstDescendant);
}
else
{
result = firstDescendant.Value;
}
success = true;
}
return success;
}
public override bool TrySetMember(
SetMemberBinder binder, object value)
{
bool success = false;
XElement firstDescendant =
Element.Descendants(binder.Name).FirstOrDefault();
if (firstDescendant != null)
{
if (value.GetType() == typeof(XElement))
{
firstDescendant.ReplaceWith(value);
}
else
{
firstDescendant.Value = value.ToString();
}
success = true;
}
return success;
}
}
对于这个用例,关键的动态实现方法是
TryGetMember()
和
TrySetMember()
(如果还需要赋值元素的话)。这两个方法的实现很直接,首先检查包含的
XElement
,查找与调用成员名称相同的元素。如果存在对应的 XML 元素,则检索(或设置)其值。如果元素存在,返回值为
true
;否则为
false
。返回
false
会导致运行时在动态成员调用点抛出
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
异常。
System.Dynamic.DynamicObject
还支持其他虚拟方法,以满足更多的动态调用需求。以下是所有可重写成员的列表:
using System.Dynamic;
public class DynamicObject : IDynamicMetaObjectProvider
{
protected DynamicObject();
public virtual IEnumerable<string> GetDynamicMemberNames();
public virtual DynamicMetaObject GetMetaObject(
Expression parameter);
public virtual bool TryBinaryOperation(
BinaryOperationBinder binder, object arg,
out object result);
public virtual bool TryConvert(
ConvertBinder binder, out object result);
public virtual bool TryCreateInstance(
CreateInstanceBinder binder, object[] args,
out object result);
public virtual bool TryDeleteIndex(
DeleteIndexBinder binder, object[] indexes);
public virtual bool TryDeleteMember(
DeleteMemberBinder binder);
public virtual bool TryGetIndex(
GetIndexBinder binder, object[] indexes,
out object result);
public virtual bool TryGetMember(
GetMemberBinder binder, out object result);
public virtual bool TryInvoke(
InvokeBinder binder, object[] args, out object result);
public virtual bool TryInvokeMember(
InvokeMemberBinder binder, object[] args,
out object result);
public virtual bool TrySetIndex(
SetIndexBinder binder, object[] indexes, object value);
public virtual bool TrySetMember(
SetMemberBinder binder, object value);
public virtual bool TryUnaryOperation(
UnaryOperationBinder binder, out object result);
}
这些成员涵盖了从类型转换、各种操作到索引调用的所有方面,此外还有一个用于检索所有可能成员名称的方法
GetDynamicMemberNames()
。
综上所述,反射、特性和动态编程为 C# 开发者提供了强大的工具,能够应对各种复杂的编程场景。合理运用这些特性,可以提高代码的灵活性和可维护性。
C# 中的反射、特性与动态编程
6. 总结
反射、特性和动态编程是 C# 中强大的编程技术,它们各自有着独特的用途和优势。
| 技术 | 用途 | 优势 |
|---|---|---|
| 反射 | 读取编译到 CIL 中的元数据,实现后期绑定 | 可在运行时动态调用代码,适用于开发工具等场景 |
| 特性 | 为程序元素添加额外元数据 | 可自定义元数据,是面向切面编程的铺垫 |
| 动态编程 |
使用
dynamic
类型简化编程,处理动态数据
| 代码更简洁,适用于处理动态数据场景 |
反射虽然能实现动态系统,但相较于静态链接代码,速度较慢,因此在开发工具中更为常用。特性可以为代码添加额外的元数据,并且可以自定义特性,在运行时获取并使用这些元数据,它是面向切面编程的一个过渡。而动态编程是 C# 4.0 引入的新特性,在处理动态数据时,它能让代码更加简洁易读,尽管会损失一些编译时的类型检查。
7. 反射、特性与动态编程的应用场景分析
为了更好地理解这些技术,下面分析它们在不同场景下的应用。
7.1 反射的应用场景
- 插件系统 :在插件系统中,主程序需要在运行时加载和调用插件的功能。通过反射,可以在运行时动态查找和调用插件中的类和方法。
- 序列化和反序列化 :在序列化和反序列化过程中,反射可以用于读取对象的属性和字段,从而将对象转换为字节流或从字节流中恢复对象。
7.2 特性的应用场景
-
数据验证
:可以定义自定义特性,用于验证数据的合法性。例如,定义一个
RangeAttribute特性,用于验证数值是否在指定范围内。 - 日志记录 :通过定义自定义特性,可以在方法执行前后添加日志记录功能,实现面向切面编程的部分功能。
7.3 动态编程的应用场景
- 处理动态数据 :当数据的结构在运行时才能确定时,动态编程可以简化代码。例如,处理从 XML 文件或数据库中读取的动态数据。
- 与动态语言交互 :在与动态语言(如 IronPython)交互时,动态编程可以方便地调用动态语言中的对象和方法。
以下是一个使用特性进行数据验证的示例:
using System;
// 定义自定义特性
[AttributeUsage(AttributeTargets.Property)]
public class RangeAttribute : Attribute
{
public int Min { get; set; }
public int Max { get; set; }
public RangeAttribute(int min, int max)
{
Min = min;
Max = max;
}
}
// 定义一个类,使用特性进行数据验证
public class Person
{
[Range(18, 60)]
public int Age { get; set; }
}
class Program
{
static void Main()
{
Person person = new Person { Age = 20 };
var type = person.GetType();
var property = type.GetProperty("Age");
var rangeAttribute = (RangeAttribute)Attribute.GetCustomAttribute(property, typeof(RangeAttribute));
if (person.Age < rangeAttribute.Min || person.Age > rangeAttribute.Max)
{
Console.WriteLine("Age is out of range.");
}
else
{
Console.WriteLine("Age is valid.");
}
}
}
8. 动态编程的性能考虑
虽然动态编程能让代码更简洁,但在性能方面需要考虑一些因素。动态编程在运行时需要进行额外的反射和编译操作,这会带来一定的性能开销。
以下是动态编程调用成员的性能流程图:
graph TD;
A[动态调用] --> B[运行时查找成员并验证签名];
B --> C[构建表达式树];
C --> D[编译表达式树生成 CIL 代码];
D --> E[执行 CIL 代码];
在性能敏感的场景中,应该谨慎使用动态编程。如果可能,尽量使用静态类型编程,以提高代码的执行效率。例如,在一个高频交易系统中,对性能要求极高,应避免使用动态编程。
9. 未来发展趋势
随着 C# 语言的不断发展,反射、特性和动态编程可能会有更多的应用和改进。
- 更强大的特性支持 :未来可能会引入更多的特性,以支持更复杂的元数据定义和使用,进一步推动面向切面编程的发展。
- 动态编程性能优化 :随着技术的进步,动态编程的性能可能会得到进一步优化,减少反射和编译的开销。
- 与其他技术的融合 :反射、特性和动态编程可能会与其他技术(如人工智能、大数据等)进行更深入的融合,为开发者提供更强大的编程能力。
总之,反射、特性和动态编程是 C# 中非常重要的技术,开发者应该根据具体的应用场景合理使用这些技术,以提高代码的质量和性能。
超级会员免费看
2876

被折叠的 条评论
为什么被折叠?



