0%

《Effective C#》笔记 C#中的动态编程

阅读《Effective C#》一书的总结,第五部分,C#中的动态编程。

Dynamic Programming in C#

动态类型可加速开发的过程,更易于与其它的系统进行交互,静态类型在运行时的执行效率更高;

在C#中绝大多数情况下使用静态类型。在某些时候使用动态类型会更加高效,C#也提供了动态类型编程的支持;

38 理解动态类型的优劣

泛型的一个限制就是,如果想访问定义在System.Object之外的方法,就必须给出约束。泛型的约束可以限制为基类、一系列的接口、值类型/引用类型、存在公有的无参构造函数等。约束无法定义必须存在某个方法。如果需要创建一个通用的操作如“+”时,这个限制会让人无所适从,而使用动态调用则可以解决此类问题;

动态类型的一个示例。只要求在运行时两个对象存在可用的“+”操作即可:

1
2
3
4
public static dynamic Add(dynamic left, dynamic right)
{
return left + right;
}

在编译期,动态变量仅拥有那些定义于System.Object中的方法,编译器会添加必要的代码让每个成员访问都以动态调用的方式进行。在运行时,代码将检查该对象并判断将要执行的方法是否存在,只要所需要的成员在运行时存在,那么即可正常工作。上边的Add方法不仅可用于两个数的相加,还可以用于字符串拼接,甚至将DataTime和TimeSpan相加,只要两个对象之间存在“+”操作符即可;

动态类型的缺点在于它抛弃了类型系统的安全性,并限制了编译器能够提供的辅助功能,所有与类型解释相关的问题只能在运行时才能发现;

动态类型要想被其它大多数代码使用,需要将其转换为静态类型,如:

1
2
3
4
5
// dynamic answer = ...

answer = Add(5, 12.3);
int value = (int)answer;
string stringLabel = System.Convert.ToString(answer);

使用表达式树(Expression Tree)也不需要在编译期知道类型,这是在运行时创建代码的另一种方法(lambda表达式和函数式编程都需要编译器的类型)。以下是使用表达式方法版本的Add方法,在真正的+操作前,会把两个操作数都转换为返回值类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static TResult AddExpressionWithConversion<T1, T2, TResult>(T1 left, T2 right)
{
var leftOperand = Expression.Parameter(typeof(T1), "left");
Expression convertedLeft = leftOperand;
if (typeof(T1) != typeof(TResult))
{
convertedLeft = Expression.Convert(leftOperand,
typeof(TResult));
}
var rightOperand = Expression.Parameter(typeof(T2), "right");
Expression convertedRight = rightOperand;
if (typeof(T2) != typeof(TResult))
{
convertedRight = Expression.Convert(rightOperand,
typeof(TResult));
}
var body = Expression.Add(convertedLeft, convertedRight);
var adder = Expression.Lambda<Func<T1, T2, TResult>>(body, leftOperand, rightOperand);
return adder.Compile()(left, right);
}

对于返回值类型和操作数类型不相同的情况,则需要更进一步增加代码来确保类型,这么做的话基本等于是重新实现了C#中处理动态分发的代码(条目41),此时使用动态类型会更好;

一个建议是:当操作数和返回值为同一类型时,应该使用表达式。这样既能提供泛型类型的参数,又能降低运行时出错的可能。以下是使用表达式实现运行时分发的建议版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static class BinaryOperators<T>
{
static Func<T, T, T> compiledExpression;
public static T Add(T left, T right)
{
if (compiledExpression == null)
createFunc();
return compiledExpression(left, right);
}
private static void createFunc()
{
var leftOperand = Expression.Parameter(typeof(T), "left");
var rightOperand = Expression.Parameter(typeof(T), "right");
var body = Expression.Add(leftOperand, rightOperand);
var adder = Expression.Lambda<Func<T, T, T>>(body, leftOperand, rightOperand);
compiledExpression = adder.Compile();
}
}

使用动态类型和在运行时创建表达式树都会带来性能上的影响。大多数情况下,使用动态类型会比使用反射手工实现延迟绑定更加高效。如果能使用静态类型解决问题,那么一定会比使用动态类型更加高效;

39 使用动态类型表达泛型类型参数的运行时类型

System.Linq.Enumerable.Cast<T>可将序列中的每个元素都转换为类型T,进而使用IEnumerable(而非IEnumerable<T>)。Cast<T>存在很多限制,下边通过一个实例展开;

定义一个类,定义和实现该类型和string类型互相转换的操作符:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyType
{
public String StringMember { get; set; }
public static implicit operator String(MyType aString)
{
return aString.StringMember;
}
public static implicit operator MyType(String aString)
{
return new MyType { StringMember = aString };
}
}

假设方法GetSomeStrings()返回一个字符串组成的序列,则有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var answer1 = GetSomeStrings().Cast<MyType>();
try
{
foreach (var v in answer1)
Console.WriteLine(v);
}
catch (InvalidCastException)
{
Console.WriteLine("Cast Failed!");
}

var answer2 = from MyType v in GetSomeStrings()
select v;
try
{
foreach (var v in answer2)
Console.WriteLine(v);
}
catch (InvalidCastException)
{
Console.WriteLine("Cast failed again");
}

这两种写法都会使用Cast<T>,会抛出InvalidCastException异常,而下边的写法显式类型转换,避免了使用Cast<T>,可以正常运行:

1
2
3
4
var answer3 = from v in GetSomeStrings()
select (MyType)v;
foreach (var v in answer3)
Console.WriteLine(v);

这是因为Cast<T>无法获取类型T中的方法。注意Cast<T>仅适用于两种情况:引用类型的转型(is操作符返回成功时,引用类型的转型也会成功),值类型和引用类型之间的装箱/拆箱操作;

使用dynamic类型则可以一定程度上绕开这一问题,会在类型转换时调用类型声明的类型转换操作符。如定义一个Convert<T>方法:

1
2
3
4
5
6
7
8
9
public static IEnumerable<TResult> Convert<TResult>(
this System.Collections.IEnumerable sequence)
{
foreach (object item in sequence)
{
dynamic coercion = (dynamic)item;
yield return (TResult)coercion;
}
}

回到刚才的情景,使用Convert<T>,可以实现MyType和string之间转换:

1
var convertedSequence = GetSomeStrings().Convert<MyType>();

Convert<T>适用性更广,会兼容更多的情况,但是也会执行更多工作;

40 将接受匿名类型的参数声明为dynamic

当你需要操作可能带有同样属性名称的不同匿名类型,且这些类型不属于你的核心应用程序,你又因为各种原因不能创建具名类型时,考虑使用dynamic这个类型来绕开这些限制。动态类型支持运行时绑定,还会让编译器生成所有必须的代码,以便配合运行时出现的各种可能类型;

一个示例:现在需要打印出一张价格单的信息,价格单中的信息来自多个不同的数据库,其中的商品product对象定义方式不同,既不会有共同的基类,也不会实现同一个接口。此时可以考虑使用适配器模式,也可以使用基于动态类型的方法:

1
2
3
4
5
public static void WritePricingInformation(dynamic product)
{
Console.WriteLine("The price of one {0} is {1}",
product.Name, product.Price);
}

还需要创建一个匿名类型,给出上边方法中所需要的属性名称,并用从任意一个数据源中查询的数据填充,

1
2
3
var price = from n in Inventory
where n.Cost > 20
select new { n.Name, Price = n.Cost * 1.15M };

这里可以使用任何必要的类型转换操作,只要生成的匿名类型中包括PriceName两个属性即可;除此之外也可以包含其它额外的属性信息。对于静态类型,如果这个静态类型中包含了属性PriceName,就也可以在WritePricingInformation中使用(类型不相符也不会有问题,例如匿名类型中Price类行为decimal而使用的静态类型中Price为double,不会有问题);

大量使用动态类型,会带来额外性能开销。一种做法是为WritePricingInformation方法提供重载,用于特定的商品静态类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Product
{
public decimal Cost { get; set; }
public string Name { get; set; }
public decimal Price
{
get { return Cost * 1.15M; }
}
}

// Derived Product class:
public class SpecialProduct : Product
{
public string ReasonOnSpecial { get; set; }
// other methods elided
}

// elsewhere
public static void WritePricingInformation(dynamic product)
{
Console.WriteLine("The price of one {0} is {1}",
product.Name, product.Price);
}

public static void WritePricingInformation(Product product)
{
Console.WriteLine("In type safe version");
Console.WriteLine("The price of one {0} is {1}", product.Name, product.Price); }

这么做之后,对于任何ProductSpecialProduct或二者的衍生类,都会使用静态类型版本,而其它对象(含匿名对象)编译器会选择动态类型的版本。在内部实现中,动态绑定将用各种方法缓存下你常用的方法,以尽可能降低开销,某个方法绑定在第一次调用执行之后,后续的每个调用都会直接重用它,此举虽不能完全避免开销,但动态实现也已经尽可能地降低了动态所带来的代价;

41 用DynamicObject或IDynamicMetaObjectProvider实现数据驱动的动态类型

C#通过动态类型、System.Dynamic.DynamicObject基类和System.Dynamic.IDynamicMetaObjectProvider接口来创建带有动态功能的类型,类型的公有接口可以在运行时改变;

最简单的方法是继承于System.Dynamic.DynamicObject。以下是一个示例,创造一个名为DynamicPropertyBag的动态类型,可以获取和设置它的属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
dynamic dynamicProperties = new DynamicPropertyBag();
try
{
Console.WriteLine(dynamicProperties.Marker);
}
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)
{
Console.WriteLine("There are no properties");
}
dynamicProperties.Date = DateTime.Now;
dynamicProperties.Name = "Bill Wagner";
dynamicProperties.Title = "Effective C#";
dynamicProperties.Content = "Building a dynamic dictionary";

上边的DynamicPropertyBag类应当这样定义,继承于DynamicObject,覆写TrySetMemberTryGetMember方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class DynamicPropertyBag : DynamicObject
{
private Dictionary<string, object> storage =
new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (storage.ContainsKey(binder.Name))
{
result = storage[binder.Name];
return true;
}
result = null;
return false;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
string key = binder.Name;
if (storage.ContainsKey(key))
storage[key] = value;
else
storage.Add(key, value);
return true;
}
public override string ToString()
{
StringWriter message = new StringWriter();
foreach (var item in storage)
message.WriteLine("{0}:\t{1}", item.Key, item.Value);
return message.ToString();
}
}

DynamicObject包含了用来处理索引器、方法、构造函数、一元和二元操作符的调用的相关方法。覆写这些方法来创建更适合需要的动态对象。返回的布尔值表示你的重载是否处理了本次请求;

使用动态类型可以简化对xml对象的访问,将类型用数据驱动,使用点(.)和元素名称来逐层深入访问xml对象;

有的时候,动态类型必须要继承于其它的基类,即无法继承DynamicObject,此时可以选择实现接口IDynamicMetaObjectProvider。实现该接口需要实现GetMetaObject()方法,以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class DynamicDictionary2 : IDynamicMetaObjectProvider
{
#region IDynamicMetaObjectProvider Members
DynamicMetaObject IDynamicMetaObjectProvider.
GetMetaObject(
System.Linq.Expressions.Expression parameter)
{
return new DynamicDictionaryMetaObject(parameter,
this);
}
#endregion

private Dictionary<string, object> storage = new
Dictionary<string, object>();

public object SetDictionaryEntry(string key,
object value)
{
if (storage.ContainsKey(key))
storage[key] = value;
else
storage.Add(key, value);
return value;
}

public object GetDictionaryEntry(string key)
{
object result = null;
if (storage.ContainsKey(key))
{
result = storage[key];
}
return result;
}

public override string ToString()
{
StringWriter message = new StringWriter();
foreach (var item in storage)
message.WriteLine("{0}:\t{1}", item.Key,
item.Value);
return message.ToString();
}
}

调用GetMetaObject时会返回一个新的DynamicDictionaryMetaObject对象,且GetMetaObject会在每次访问DynamicDictionary2的成员时被调用,即便是DynamicDictionary2中提供了静态定义的方法,因为需要解析这些方法来唤醒可能出现的动态行为;

使用动态对象编程较为复杂,表达式树很难调试也很难保证正确性。编写动态对象的代码时需要注意效率和性能,使用动态类型会带来性能上的损失,而手工实现所带来的损失往往会更大一些。

42 如何使用表达式API

在运行时基于反射检查代码或创建代码的功能非常强大,但是太过底层且难用,可以使用表达式和表达式树作为替代方案。以下会有两个示例:

第一个例子是通过解析表达式将代码转换成能实现运行时算法的数据元素。在通信框架中常常会使用某种代码生成工具来为特定的服务生成客户端的代理。ClientProxy<T>了解如何将方法和各个参数转换成最终的调用,不过它实际上并不了解所访问的服务,调用方式如下:

1
2
3
var client = new ClientProxy<IService>();
var result = client.CallInterface<string>(
srver => srver.DoWork(172));

ClientProxy<T>没有使用老式的代码生成器,而是使用表达式树和泛型来判断调用了哪个方法以及使用了哪些参数。重点就在这个CallInterface方法,这里其实并不需要实现了IService接口的实例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public TResult CallInterface<TResult>(Expression<
Func<T, TResult>> op)
{
var exp = op.Body as MethodCallExpression;
var methodName = exp.Method.Name;
var methodInfo = exp.Method;
var allParameters = from element in exp.Arguments
select processArgument(element);
Console.WriteLine("Calling {0}", methodName);
foreach (var parm in allParameters)
Console.WriteLine(
"\tParameter type = {0}, Value = {1}",
parm.Item1, parm.Item2);
return default(TResult);
}

private Tuple<Type, object> processArgument(Expression
element)
{
object argument = default(object);
LambdaExpression l = Expression.Lambda(
Expression.Convert(element, element.Type));
Type parmType = l.ReturnType;
argument = l.Compile().DynamicInvoke();
return Tuple.Create(parmType, argument);
}

方法processArgument将每个参数当作表达式来求值,这里只需要在表达式中提供一个定义在服务器端的一个成员方法即可;

第二个例子是在运行时生成代码。比如在一个大型的系统中,用于将不同子系统中的不同类型互相转换,代码类似于这样:

1
2
3
var converter = new Converter<SourceContact,
DestinationContact>();
DestinationContact dest2 = converter.ConvertFrom(source);

实现的功能是这样的:将源对象中公有的get访问器且目标类型中有公有的同名的set访问器的属性值逐一复制,即实现类似这样的功能(并非合法的C#代码,仅用来解释):

1
2
3
4
5
6
7
TDest ConvertFromImaginary(TSource source)
{
TDest destination = new TDest();
foreach (var prop in sharedProperties)
destination.prop = source.prop;
return destination;
}

为了实现上边的伪代码,我们需要创建一个表达式,完整的代码如下(实现前边的converter):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void createConverterIfNeeded()
{
if (converter == null)
{
var source = Expression.Parameter(typeof(TSource),
"source");
var dest = Expression.Variable(typeof(TDest),
"dest");
var assignments = from srcProp in
typeof(TSource).GetProperties(
BindingFlags.Public |
BindingFlags.Instance)
where srcProp.CanRead
let destProp = typeof(TDest).
GetProperty(
srcProp.Name,
BindingFlags.Public |
BindingFlags.Instance)
where (destProp != null) &&
(destProp.CanWrite)
select Expression.Assign(
Expression.Property(dest,
destProp),
Expression.Property(source,
srcProp));

// put together the body:
var body = new List<Expression>();
body.Add(Expression.Assign(dest,
Expression.New(typeof(TDest))));
body.AddRange(assignments);
body.Add(dest);
var expr =
Expression.Lambda<Func<TSource, TDest>>(
Expression.Block(
new[] { dest }, // expression parameters
body.ToArray() // body
),
source // lambda expression
);

var func = expr.Compile();
converter = func;
}
}

代码中涉及的主要步骤如下:

  • 源和目标的参数表达式,sourcedest
  • 筛选满足条件的属性,srcPropdestProp
  • 赋值,使用Expression.Assign
  • 组装成表达式的主体部分,是一个List<Expression>
  • 创建lambda表达式expr
  • 编译成一个委托,赋值给converter

43 使用表达式将延迟绑定转换为预先绑定

延迟绑定API需要使用符号(symbol)信息来实现,而预先编译好的API则无需这些信息,因为编译器已经解释了符号引用。表达式API可以在二者之间架起桥梁。表达式对象包含了一种抽象的符号树,用于表示想要执行的算法。你可以使用表达式API执行那些代码,也可以检查所有的符号,包括变量、方法和属性的名称等。你可以使用表达式API创建强类型的、编译后的方法,与系统中依赖延迟绑定的部分交互,并使用属性或其他符号的名称;

还是拿一个例子来说明。首先是延迟绑定的例子,在Silverlight和WPF中使用的属性通知接口,需要支持对绑定属性变化的响应,以便让用户界面元素能够在底层数据该懂事做出更新,需要实现两个接口,INotifyPropertyChangedINotifyPropertyChanging。接口的实现很简单,每个里边定义了一个事件,事件的参数也只是简单地包含了发生变化的属性的名称,以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MemoryMonitor : INotifyPropertyChanged,
INotifyPropertyChanging
{
System.Threading.Timer updater;
public MemoryMonitor()
{
updater = new System.Threading.Timer((_) =>
timerCallback(_),
null, 0, 5000);
}

private void timerCallback(object unused)
{
UsedMemory = GC.GetTotalMemory(false);
}

public long UsedMemory
{
get { return mem; }
private set
{
if (value != mem)
{
if (PropertyChanging != null)
PropertyChanging(this,
new PropertyChangingEventArgs(
"UsedMemory"));
mem = value;
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(
"UsedMemory"));
}
}
}

private long mem;

#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler
PropertyChanged;
#endregion
#region INotifyPropertyChanging Members
public event PropertyChangingEventHandler
PropertyChanging;
#endregion
}

代码如上。问题在于,每次实现这个接口时都要重复这么一段冗长的代码,每个属性的setter都要触发事件(变化前和变化后),并且事件参数中使用字符串来表示属性的名字。为了解决这些问题,可以借助扩展方法来实现,主要分为以下几步:

  1. 查看新的值和旧的值是否有差别;
  2. 触发INotifyPropertyChanging的事件;
  3. 更新值;
  4. 触发INotifyPropertyChanged的事件;

为了增强代码的健壮性,应避免使用属性名称的字符串来硬编码。我们可以使用扩展方法来扩展PropertyChanged事件,改为类似下边这样的实现形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
// MemoryMonitor, using the extension methods
private void timerCallback(object unused)
{
long updatedValue = GC.GetTotalMemory(false);
PropertyChanged.SetNotifyProperty(updatedValue,
() => UsedMemory);
}

public long UsedMemory
{
get;
private set;
}

这使MemoryMonitor的实现大大简化,避免了使用硬编码字符串,UsedMemory是一个自动的隐式属性,代码中也无需做复杂的逻辑。但是实现这些功能的代码稍微有点复杂,其中使用了反射和表达式树,下面是完整的扩展方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public static class PropertyNotifyExtensions
{
public static T SetNotifyProperty<T>(this
PropertyChangedEventHandler handler,
T newValue, Expression<Func<T>> oldValueExpression,
Action<T> setter)
{
return SetNotifyProperty(handler, null, newValue,
oldValueExpression, setter);
}

public static T SetNotifyProperty<T>(this
PropertyChangedEventHandler postHandler,
PropertyChangingEventHandler preHandler,
T newValue, Expression<Func<T>> oldValueExpression,
Action<T> setter)
{
Func<T> getter = oldValueExpression.Compile();
T oldValue = getter();
if (!oldValue.Equals(newValue))
{
var body = oldValueExpression.Body as
System.Linq.Expressions.MemberExpression;
var propInfo = body.Member as PropertyInfo;
string propName = body.Member.Name;
// Get the target object
var targetExpression = body.Expression as
ConstantExpression;
object target = targetExpression.Value;
if (preHandler != null)
preHandler(target, new
PropertyChangingEventArgs(propName));
// Use Reflection to do the set:
// propInfo.SetValue(target, newValue, null);
//var compiledSetter = setter.Compile();
setter(newValue);

if (postHandler != null)
postHandler(target, new
PropertyChangedEventArgs(propName));
}
return newValue;
}
}

以上代码省略了一些错误处理的逻辑,如类型转换是否成功或者属性的setter是否存在等。SetNotifyProperty的实现可以分为以下的步骤:

  1. 编译并执行getter表达式,即oldValueExpression,对应于前边的例子就是() => UsedMemory。执行getter得到oldValue
  2. 只有当oldValuenewValue不相等时才会走后边的逻辑;
  3. 解析表达式oldValueExpression,从中取出属性的名称、目标对象的类型、属性的setter;
  4. 如果有preHandler,调用,即PropertyChangingEvent
  5. 修改属性的值,即调用setter;
  6. 如果有postHandler,调用,即PropertyChangedEvent

在其它需要方法或属性名称的地方,也可以使用同样的技术,LINQ to SQL和Entity Framework都构建于System.Linq.Expression API之上,这些API可以让你将代码当作数据来使用。你可以使用表达式API检查这些代码、修改实现逻辑、创建新的代码并最终执行;

44 尽量减少在公有API中使用动态对象

动态对象有一些强制性,如果某个操作的某个参数是动态的,那么其结果也会是动态的;

若要在程序中使用动态特性,尽量不要在公有接口中使用,这样即可将动态类型限制在一个单独的对象(或类型)中,避免动态类型脱离控制,影响到程序的其他部分或其他使用你的程序的代码中;

当代码依赖于其他环境中创建的动态类型时,可以用专门的静态类型封装这些动态对象,并提供静态的公有接口;

REFERENCE

《C#高效编程 改进C#代码的50个行之有效的办法》