CodeModel的范例:一个为项目所有相关类添加工厂方法的AddIn
1、问题描述
对一个程序做性能优化,发现程序里会大量创建动态对象,是影响性能的一个瓶颈。程序里都是采用Activator.CreateInstance(Type)的方法,记得在codeproject看过一篇文章(原文在此:Dynamic Objects,Factories,and Runtime Machines to Boost Performance),对动态创建对象的几种方式进行效率对比,Activator.CreateInstance是效率较低的一种。采用FormatterServices.GetUninitializedObject得到一个未初始化的类,再调用类的工厂方法,效率有大幅提高。下面是简单的代码说明。更多内容请参见上文提到的文章。
- using System;
- using System.Collections.Generic;
- using System.Text;
- using System.Runtime.Serialization;
- //第一种方式:Activator.CreateInstance(Type)方式。效率较低
- namespace ActivatorCreator
- {
- public class Widget { }
- public class WidgetA : Widget { }
- public class WidgetB : Widget { }
- //..
- public class WidgetZ : Widget { }
-
- public abstract class Creator
- {
- public static Widget DynamicCreate(Widget w)
- {
- return (Widget)Activator.CreateInstance(w.GetType());
- }
- }
- }
- //第二种动态创建对象方式
- //采用FormatterServices.GetUninitializedObject(Type)
- //获得一个未初始化对象,再调用对象的工厂方法得到对象。
- //经测试时间效率大概是前一种的150倍左右。
- namespace SerializationCreator
- public class Widget
- public virtual Widget GetInstance()
- return new Widget();
- public class WidgetA : Widget
- public override Widget GetInstance()
- return new WidgetA();
- public class WidgetB : Widget
- return new WidgetB();
- //
- public class WidgetZ : Widget
- return new WidgetZ();
- //Serialization配合工厂方法动态生成对象,高效率
- Widget widgetFactory =
- (Widget)FormatterServices.GetUninitializedObject(w.GetType());
- return widgetFactory.GetInstance();
- }
复制代码
因此,决定采用FormatterServices.GetUninitializedObject
方法来代替Activator.CreateInstance(Type)。整个项目有两个基类以及从该基类继承的大量子类(数目大概有100+)需要更改,而且继承关系最深达到5层。
代码的
修改工作量不小。
2、解决办法
2.1 复制粘贴
大概是最常规也最无趣的
方法,我在复制了10个左右的类后实在受不了这种单调和枯燥,放弃了。
2.2 Code Snippet
复制的进阶,就是用Code Snippet了。把相同
代码做成Snippet,效率有大幅提高。Snippet
文件内容如下:
@H_
502_176@
- <?xml version="1.0" encoding="utf-8"?>
- <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
- <CodeSnippet Format="1.0.0">
- <Header>
- <Title>Add Factory Method</Title>
- <Author>lumber</Author>
- <Description>为类添加工厂方法</Description>
- <HelpUrl></HelpUrl>
- <SnippetTypes />
- <Keywords />
- <Shortcut>fm</Shortcut>
- </Header>
- <Snippet>
- <References />
- <Imports />
- <Declarations>
- <Literal Editable="false">
- <ID> RetType </ID>
- <Type></Type>
- <ToolTip>返回的类型</ToolTip>
- <Default> RetType </Default>
- <!--注意这里的Function,以及ClassName()函数-->
- <Function>ClassName()</Function>
- </Literal>
- </Declarations>
- <Code Language="csharp" Kind="" Delimiter="$">
- <![CDATA[internal override CADEntityData GetInstance()
- {
- return new $RetType$();
- }]]>
- </Code>
- </Snippet>x
- </CodeSnippet>
- </CodeSnippets>
复制代码
在上面的snippet中,值得一提的是,使用了snippet function。即先定义了一个Literal,
名称为RetType,代表工厂
方法返回类型。我们知道不同的子类,工厂
方法的
函数签名相同,不同的是返回该类的实例。即RetType的值要等于被插入的类的
名称。于是我们为RetType这个Literal提供了一个function,ClassName(),该
函数返回snippet所在类的
名称。
实际我们不需要手工来写这个
文件,这里推荐Code Snippet Editor这个小
工具。
2.3 CodeModel
现在只需要找到需要更改的类,敲下快捷键fm,再双击tab就ok了,委实比当初复制、编辑幸福多了。但人心难足,Snippet还是有不爽的地方:
1、找到并打开所有要
修改的类(100+啊兄弟,项目里每个类都是一个单独
文件)不停地重复按键,也是挺无聊的活。更重要的是,要保证不能遗漏——上文说了,很多子类都经过了多达5层的继承……
2、不同的基类要重新编制一个snippet。另外考虑到,如果以后别的项目要有类似的更改呢?如果项目是用VB.NET而不是c#呢?。。。。
看来,最好的
解决办法是写一个AddIn了。
简单分析下任务,其实就是两个:1、寻找项目中要
添加工厂
方法的基类及所有派生类。2、为这些类
添加一个相应的工厂
函数。
很自然需要用到CodeModel。
关于VS的扩展开发,推荐园子里Anders Cui的
系列文章。而Anders Cui的系列恰好没有写关于CodeModel的
内容,既有珠玉在前,所以小可也就斗胆续貂,简单写写关于CodeModel的
内容。
直接用Vs2008新建项目,选择“Visual Stduio外接程序”,选择使用c#
语言开发,然后一路默认,编辑器就
自动为你
生成了一个AddIn项目。该项目已经
自动生成了大部分
代码,
包括将的AddIn程序
添加到工具
菜单上等。接下来只需要将需要执行的
代码加入到Exec
函数中即可。
我们再在项目中
添加一个含文本框的窗体,作为输入界面,输入项目中要
添加工厂
方法的基类的全名(含命名空间)。接下来就是利用CodeModel寻找该类及其派生类,并
添加工厂
方法。还是
代码说话吧,相关
函数我在注释里都有说明,另外可以
查询msdn。
- /// <summary>
- /// 实现 IDTCommandTarget接口的 Exec 方法。此方法在调用该命令时调用。编辑器自动生成。
- /// </summary>
- /// <param term='commandName'>要执行的命令的名称。</param>
- /// <param term='executeOption'>描述该命令应如何运行。</param>
- /// <param term='varIn'>从调用方传递到命令处理程序的参数。</param>
- /// <param term='varOut'>从命令处理程序传递到调用方的参数。</param>
- /// <param term='handled'>通知调用方此命令是否已被处理。</param>
- /// <seealso class='Exec' />
- public void Exec(string commandName,vsCommandExecOption executeOption,
- ref object varIn,ref object varOut,ref bool handled)
- handled = false;
- if (executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
- if (commandName == "AddFactoryMethod.Connect.AddFactoryMethod")
- InputBox iBox = new InputBox();
- //程序添加一个输入窗口,输入要添加工厂方法的基类的全称(含命名空间)
- if (iBox.ShowDialog() == DialogResult.OK
- && !string.IsNullOrEmpty(iBox.Str))
- {
- //获取当前解决方案的第一个项目的CodeModel
- //注意CodeModel中集合都是以下标1开始而非0。如Projects.Item(1)
- CodeModel cm = _applicationObject.Solution.Projects.Item(1).CodeModel;
- foreach (CodeElement ce in cm.CodeElements)
- {
- //只有命名空间和类中才包含类。
- if ((ce is CodeNamespace) | (ce is CodeClass))
- {
- SetAllClass(ce,iBox.Str);
- }
- }
- }
- handled = true;
- return;
- /// 寻找基类及所有子类,并添加工厂方法。
- /// 这个函数主要演示了:
- /// 1、如何寻找一个项目中包含的所有类。
- /// 注意,所有的类应该包含在项目的命名空间和类中。
- /// 2、如何判断类是指定类的派生类。
- /// 3、CodeElement(包括CodeClass、CodeFunction……)等一系列对象的基本用法。
- /// <param name="ct">类或者命名空间</param>
- /// <param name="str">基类的全称(含命名空间)</param>
- private void SetAllClass(CodeElement ct,string str)
- CodeClass cc = ct as CodeClass;
- if (cc != null && cc.get_IsDerivedFrom(str))
- //cc.get_IsDerivedFrom(str)函数用来判断类是否由全名为str的类派生得来。
- //注意!自身也是自身的派生类,即cc.get_IsDerivedFrom(cc.FullName)将返回true。
- //为基类和派生类添加工厂方法
- AddFactoryMethod(cc,str);
- CodeElements elements;
- if (ct is CodeNamespace)
- elements = (ct as CodeNamespace).Members;
- else
- elements = (ct as CodeClass).Members;
- foreach (var ce in elements)
- if ((ce is CodeNamespace) | (ce is CodeClass))
- //寻找嵌套的类
- SetAllClass(ce as CodeElement,serif; font-size: 12px; ">/// 为类添加工厂方法。
- /// 1、如何用AddFunction方法为类添加方法。
- /// 2、CodeModel和文档操作之间的结合。
- /// code***.GetStartPoint/GetEndPoint可以获得TextPoint;
- /// TextPoint.CodeElement可获得对应的codeElement。
- /// 3、为不同的编程语言(VB.NET,c#)提供插件。
- /// <param name="cc">要添加方法的类</param>
- /// <param name="fullname">基类的全称</param>
- private void AddFactoryMethod(CodeClass cc,string fullname)
- string str1 = "";
- string str2 = "";
- if (cc.Language == CodeModelLanguageConstants.vsCMLanguageCSharp)
- str1 = string.Format("return new {0}();\n",cc.Name);
- if (cc.FullName == fullname)
- //基类
- str2 = "public virtual";
- else//子类
- str2 = "public override";
- else if (cc.Language == CodeModelLanguageConstants.vsCMLanguageVB)
- str1 = string.Format("return new {0}()\n",serif; font-size: 12px; "> str2 = "Public Overridable";
- str2 = "Public Overrides";
- //添加函数
- CodeFunction cf = cc.AddFunction("GetInstance",vsCMFunction.vsCMFunctionFunction,serif; font-size: 12px; "> fullname,-1,vsCMAccess.vsCMAccessPublic,null);
- //为函数添加文档注释
- cf.DocComment = "<summary>\n工厂方法生成一个实例。\n</summary>";
- EditPoint ep = cf.GetEndPoint(vsCMPart.vsCMPartBody).CreateEditPoint();
- //添加函数体
- ep.Insert(str1);
- ep = cf.GetStartPoint(vsCMPart.vsCMPartHeader).CreateEditPoint();
- ep.ReplaceText(6,str2,0);
- private DTE2 _applicationObject;
复制代码
完整
代码可以在这里下载:
附件:
AddFactoryMethod.rar(下载
0次)。
3、一些问题
3.1 CodeClass的
属性DerivedTypes没有实现。否则寻找指定类的所有派生类将更为简化,程序更简单。
3.2 CodeClass.AddFunction
方法,第2和第5个参数,使用示例
代码以外的参数值,均会出现异常。Bug??所以示例最后被迫使用ReplaceText来完成加入虚
函数的任务。希望有高手指点一下。
3.3 CodeModel中大部分集合,特别是有item
属性但item非默认
属性者,使用下标索引时,从1而非0开始。EnvDTE命名空间中其他集合大概也是。或者,推而广之,涉及到VSTA开发的(如VSTO),集合一般下标从1开始。毕竟这些领域还是VB为主。
3.4 和本文无关——
后台编辑器在编辑
代码的时候很不好用啊。尤其选择折叠
代码的时候,居然不能正常
显示。。。我浏览器的问题麽?
4、参考文献
1、《
Dynamic Objects,and Runtime Machines to Boost Performance》 by Philip Liebscher
2、《
Creating a C# Class using the CodeModel Object》 MSDN
3、《
如何:使用 CodeModel 对象分析 Visual Basic 代码》 MSDN
4、关于Vs扩展的系列
文章:
Visual Studio 2008 可扩展性开发(九):总结篇