面对这么多“泛滥”的扩展,很多人都会感到很别扭,的确有种“喧宾夺主”的感觉,想从中找出真正想用的方法来太难了!尽管经过扩展后的string类很“强大”,但易用性确很差。
很多人因此感觉扩展应适可而止,不该再继续下去...其实这是一种逃避问题的态度,出现问题我们应该主动去解决,而不是去回避!
有很多种方法可以解决以上问题,最简单的就是使用将扩展放入不同namespace中,使用时按需using相应namespace,可达到一定效果。但这种方法有很大缺点: 一个命名空间中的扩展若太多同样会让我们的智能提示充斥着扩展方法,扩展太少每次使用都要using多个命名空间,很麻烦。
先介绍一种简单的方式,先看效果:
图1中前三个以As开始的三个扩展就是采用分组技术后的三类扩展,分别是中文处理、转换操作、正则操作,后面三个图分别对就这三类扩展的具体应用。图2中的有三个中文处理的扩展ToDBC、ToSBC、GetChineseSpell分别是转为半角、转为全角、获取拼音首字母。
通过这样分组后,string类的智能提示中扩展泛滥的现象得到了解决,使用AsXXX,是以字母A开始,会出现在提示的最前面,与原生方法区分开来。
采用这种方式有几个缺点:
1.使用一个扩展要先As一次,再使用具体扩展,比之前多了一步操作:这是分组管理必然的,建议使用频率非常高的还是直接扩展给string类,不要分组。只对使用频率不高的进行分组。
2.扩展后的智能提示不友好,扩展的方法与Equals、ToString混在了一起,而且没有扩展方法的标志。
先给出这种方法的实现参考代码,再来改进:
2 {
3 public static ChineseString AsChineseString(this string s) { return new ChineseString(s); }
4 public static ConvertableString AsConvertableString(this string s) { return new ConvertableString(s); }
5 public static RegexableString AsRegexableString(this string s) { return new RegexableString(s); }
6 }
7 public class ChineseString
8 {
9 private string s;
10 public ChineseString(string s) { this.s = s; }
11 //转全角
12 public string ToSBC(string input) { throw new NotImplementedException(); }
13 //转半角
14 public string ToDBC(string input) { throw new NotImplementedException(); }
15 //获取汉字拼音首字母
16 public string GetChineseSpell(string input) { throw new NotImplementedException(); }
17 }
18 public class ConvertableString
19 {
20 private string s;
21 public ConvertableString(string s) { this.s = s; }
22 public bool IsInt(string s) { throw new NotImplementedException(); }
23 public bool IsDateTime(string s) { throw new NotImplementedException(); }
24 public int ToInt(string s) { throw new NotImplementedException(); }
25 public DateTime ToDateTime(string s) { throw new NotImplementedException(); }
26 }
27 public class RegexableString
28 {
29 private string s;
30 public RegexableString(string s) { this.s = s; }
31 public bool IsMatch(string s, string pattern) { throw new NotImplementedException(); }
32 public string Match(string s, string pattern) { throw new NotImplementedException(); }
33 public string Relplace(string s, string pattern, MatchEvaluator evaluator) { throw new NotImplementedException(); }
34 }
代码仅是为了说明怎么分组,没有实现,具体实现请参见本系列前面的文章。为了节省空间,很多代码都写成了一行。
前面提到的第二条缺点,我们改进后,方式二的显示效果如下:
Equals、GetHashCode、ToString 实在去不了,哪位朋友有好办法分享一下吧!不过这次把扩展方法的标志加上。实现比方式一麻烦一下:
2 {
3 private string s;
4 public ChineseString(string s) { this.s = s; }
5 public string GetValue() { return s; }
6 }
7
8 public static class CheseStringExtension
9 {
10 public static ChineseString AsChineseString(this string s) { return new ChineseString(s); }
11
12 public static string ToSBC(this ChineseString cs)
13 {
14 string s = cs.GetValue();//从ChineseString取出原string
15 char[] c = s.ToCharArray();
16 for (int i = 0; i < c.Length; i++)
17 {
18 if (c[i] == 32) { c[i] = (char)12288; continue; }
19 if (c[i] < 127) c[i] = (char)(c[i] + 65248);
20 }
21 return new string(c);
22 }
23 public static string ToDBC(this ChineseString cs) { throw new NotImplementedException(); }
24 public static string GetChineseSpell(this ChineseString cs) { throw new NotImplementedException(); }
25 }
这里需要两个类,一个类ChineseString作为AsXXX的返回值,第二个类ChineseStringExtension是对ChineseString进行扩展的类。能过这种方式,才能显示出扩展的标识符号!每组扩展要两个类,比较麻烦。
方式一、方式二感觉都不太好,而且扩展组多了,还会有新的问题出现,如下:
也是很要命的!再来看第三种方式,这是我和韦恩卑鄙在讨论单一职责原则时想出来的,先看效果:
方法三将所有的扩展精简为一个As<T>!是的,我们仅需要As<T>这一个扩展!T为一接口,通过输入不同的T,展示相应的扩展。这样又解决了扩展组的泛滥问题,先看下实现一个新的扩展组需要写什么代码,先看左图的代码:
2
3 public static class ConvertableString
4 {
5 public static bool IsInt(this IConvertableString s)
6 {
7 int i; return int.TryParse(s.GetValue(), out i);
8 }
9 public static bool IsDateTime(this IConvertableString s)
10 {
11 DateTime d; return DateTime.TryParse(s.GetValue(), out d);
12 }
13
14 public static int ToInt(this IConvertableString s)
15 {
16 return int.Parse(s.GetValue());
17 }
18
19 public static DateTime ToDateTime(this IConvertableString s)
20 {
21 return DateTime.Parse(s.GetValue());
22 }
23 }
首先定义一个接口IConvertableString,它继承泛型接口IExtension<T>(我定义的一个接口,稍后给出),因为是对string类作扩展,所以泛型参数为string。IConvertableString只需要一个空架子。然后再编写一个扩展类,所有的方法扩展在IConvertableString接口上。
再来看右图IRegexableString的代码:
2 {
3 public static bool IsMatch(this IRegexableString s, string pattern)
4 { throw new NotImplementedException(); }
5 public static string Match(this IRegexableString s, string pattern)
6 { throw new NotImplementedException(); }
7 public static string Relplace(this IRegexableString s, string pattern, MatchEvaluator evaluator)
8 { throw new NotImplementedException(); }
9 }
与上一个一样,也是先定义一个空接口,再定义一个扩展类,将方法扩展在空接口上。
有一点注意一下,扩展的实现中都要使用GetValue获取原始字符串的值。
最后给出IExtension<T>接口及As<T>扩展的实现:
1 public interface IExtension<V>
2 {
3 V GetValue();
4 }
5
6 public static class ExtensionGroup
7 {
8 private static Dictionary<Type, Type> cache = new Dictionary<Type, Type>();
9
10 public static T As<T>(this string v) where T : IExtension<string>
11 {
12 return As<T, string>(v);
13 }
14
15 public static T As<T, V>(this V v) where T : IExtension<V>
16 {
17 Type t;
18 Type valueType = typeof(V);
19 if (cache.ContainsKey(valueType))
20 {
21 t = cache[valueType];
22 }
23 else
24 {
25 t = CreateType<T, V>();
26 cache.Add(valueType, t);
27 }
28 object result = Activator.CreateInstance(t, v);
29 return (T)result;
30 }
31 // 通过反射发出动态实现接口T
32 private static Type CreateType<T, V>() where T : IExtension<V>
33 {
34 Type targetInterfaceType = typeof(T);
35 string generatedClassName = targetInterfaceType.Name.Remove(0, 1);
36 //
37 AssemblyName aName = new AssemblyName("ExtensionDynamicAssembly");
38 AssemblyBuilder ab =
39 AppDomain.CurrentDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.Run);
40 ModuleBuilder mb = ab.DefineDynamicModule(aName.Name);
41 TypeBuilder tb = mb.DefineType(generatedClassName, TypeAttributes.Public);
42 //实现接口
43 tb.AddInterfaceImplementation(typeof(T));
44 //value字段
45 FieldBuilder valueFiled = tb.DefineField("value", typeof(V), FieldAttributes.Private);
46 //构造函数
47 ConstructorBuilder ctor = tb.DefineConstructor(MethodAttributes.Public,
48 CallingConventions.Standard, new Type[] { typeof(V) });
49 ILGenerator ctor1IL = ctor.GetILGenerator();
50 ctor1IL.Emit(OpCodes.Ldarg_0);
51 ctor1IL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
52 ctor1IL.Emit(OpCodes.Ldarg_0);
53 ctor1IL.Emit(OpCodes.Ldarg_1);
54 ctor1IL.Emit(OpCodes.Stfld, valueFiled);
55 ctor1IL.Emit(OpCodes.Ret);
56 //GetValue方法
57 MethodBuilder getValueMethod = tb.DefineMethod("GetValue",
58 MethodAttributes.Public | MethodAttributes.Virtual, typeof(V), Type.EmptyTypes);
59 ILGenerator numberGetIL = getValueMethod.GetILGenerator();
60 numberGetIL.Emit(OpCodes.Ldarg_0);
61 numberGetIL.Emit(OpCodes.Ldfld, valueFiled);
62 numberGetIL.Emit(OpCodes.Ret);
63 //接口实现
64 MethodInfo getValueInfo = targetInterfaceType.GetInterfaces()[0].GetMethod("GetValue");
65 tb.DefineMethodOverride(getValueMethod, getValueInfo);
66 //
67 Type t = tb.CreateType();
68 return t;
69 }
70 }
代码比较长,先折叠起来,逐层打开分析吧!
IExtension<V>只定义一个方法GetValue,用于将As<T>后将原始的值取出。
ExtensionGroup定义了As<T>扩展,我们先看下值的传递过程。调用语句:"123".As<IConvertableString>().ToInt();
首先,"123" 是个字符串,As<IConvertableString>后转换成了IConvertableString接口的实例,ToInt时使用GetValue将"123"从IConvertableString接口的实例中取出进行处理。
关键在“IConvertableString接口的实例”,前面我们并没有具体实现IConvertableString接口的类,怎么出来的实例呢?我们这里用反射发出动态生成了一个实现IConvertableString接口的类。具体是由ExtensionGroup中的私有函数CreateType<T, V>完成的,在这里T传入的是IConvertableString,V传入的是string,返回的值就是实现了IConvertableString接口的一个类的Type.由CreateType<T, V>动态实现的类“模样”如下:
2 {
3 private string value;
4 public ConvertableString(string value)
5 {
6 this.value = value;
7 }
8 public string GetValue()
9 {
10 return value;
11 }
12 }
如果此处不用反射发出动态生成这么一个,那么我们就要手工写一个,每个扩展组都要相应的写一个,很麻烦的。
为了提高性能,对反射发出的类型进行了缓存,保存在cache成员中。
方式三有点复杂,主要是因为我们是给sealed类进行扩展,无法从它们继承。
最后给出测试代码:
2 {
3 int i = "123".As<IConvertableString>().ToInt();
4 DateTime d = "2009年8月29日".As<IConvertableString>().ToDateTime();
5 }
三种方式,我最喜欢第三种,它仅需要一个As<T>,而且是对接口进行扩展,感觉更OO一些。
三种方式都不完美,我会努力改进,大家多提些建议啊。
最后,谢谢大家对我的运行,特别感谢韦恩卑鄙。
本人系列文章《c#扩展方法奇思妙用》,敬请关注!