Excel VBA 学习总结 - 代码优化之道
每个问题都可以用不同的解决方案解决,不同的方案执行的效率却是不同的。同样一个问题,不同的程序执行的时间可能相差数十,甚至数百倍。所以学习一门语言,必须要了解其优化方法。下面从几个方面总结一下VBA中的代码优化之道。
一、运行环境的优化
VBA是需要运行在运行环境中的,运行环境直接影响了VBA程序的执行效率,所以第一个方面,我们从干扰程序执行的因素,讨论运行环境的优化。
若要提高VBA程序的性能,明确禁用代码执行时不需要的功能是极其有效的优化方法。通常,只需在代码运行后重新计算或重绘一次,这样做可以提高性能。代码执行后,将功能恢复到其原始状态即可。
1.关闭屏幕更新
关闭屏幕更新是提高VBA程序运行速度的很有效的方法,能大幅缩短运行时间。原因很简单,后台的操作不需要反映到屏幕上了,当然就省去了很多的操作,所以可以节省很多时间。当然了,这个操作是可以控制粒度的,比如每次VBA程序的开始于结束可以设置屏幕更新,也可以在每个循环的开始与结束时设置,这个通常取决于用户的需要。
Application.ScreenUpdate = False
'程序执行过程
'...
'请不要忘记VBA程序运行结束时再将该值设回来
Application.ScreenUpdate = True
2.关闭自动计算
函数的自动计算对Excel运行影响很大,尤其是有大量数组函数和易失性函数时,影响更为明显。这个方面优化的最简单的方法就是关闭自动重算,启用手动重算。通过减少重算量提高Excel速度。可以在进入主程序运行前,将计算模式设置为手动。Calculation 属性是对所有工作簿进行的设置,您也可以用工作表的EnableCalculation属性来设置对某个工作表是否进行重新计算。
Application.Calculation = xlCalculationManual
'程序执行过程
'...
'恢复为自动计算
Application.Calculation = xlCalculationAutomatic
3.禁用事件
禁用事件可以避免事件触发时的连锁反应。例如在工作表的Change事件中,一个单元格的值改变影响两个以上的单元格值改变,因为事件的连续触发可以造成CPU耗尽。
Application.EnableEvents = False
'程序执行过程
'...
'请不要忘记VBA程序运行结束时再将该值恢复
Application.EnableEvents = True
4.禁用状态栏
状态栏设置与屏幕更新设置是分开的,这样即使屏幕不更新,您仍可以显示当前操作的状态。但是,如果您不需要显示每个操作的状态,则在代码运行时禁用状态栏也可以提高性能。
Application.DisplayStatusBar=False,
'程序执行过程
'...
'恢复显示状态栏
Application.DisplayStatusBar=False,
5.禁用分页符
如果 ActiveSheet.DisplayPageBreaks 设置为False,则 Excel 不显示分页符。代码运行时不需要重新计算分页符,在代码执行后计算分页符可以提高性能。
6.避免在频繁的事件中写代码
这个道理很简单,例如频繁的Activate事件。
二、算法的优化
算法代表解决问题的步骤,它直接影响了程序的执行效率,所以算法优化基本是所有语言优化代码最主要的过程;这是一个不断试验,不断总结,不断优化的过程。这个不是这里的重点,所以只是简单分享一下我知道的方式。
深刻挖掘和应用数学模型的特性
基本上都是缩小问题的规模,递推总结出数学模型的规律,然后用程序实现就可以了。例如下面的经典问题:
上台阶问题:http://www.cnblogs.com/flowerszhong/archive/2011/09/14/2176374.html。
如果空间允许的话,可以拿空间换时间
常见空间换时间算法:http://bbs.pfan.cn/post-227818.html
保存可以利用的中间结果
动态规划中用的最多了:http://www.cs.pitt.edu/~ztliu/wordpress/2011/04/algo-dynamic-programming/
采用合适的排序算法
海量数据查找:http://blog.csdn.net/lanphaday/article/details/3547776
常见排序算法:http://blog.csdn.net/ctang/article/details/37914
经常查询的集合数据,先排序
通常对于不怎么变化,但是又经常查询的数据,先排序是非常合算的,下面这个介绍了最快排序与最快搜索:http://blog.csdn.net/shendl/article/details/4053853
三、实现方式的优化
算法确定以后,就是使用具体的代码实现算法,而对于同样的算法,使用不同的函数去完成,也会有不小的差异,这方面其实基本都是从减少内存使用量,加速编译器的执行的速度这两个方面优化代码。
1、合理使用变量与常量
(1)始终声明和使用大小合适的变量类型,这个原因很明显,可以节省内存,加速运行速度。
(2)除非确实需要,应避免使用浮点数据类型。尽管Currency数据类型更大,但它比 Single 数据类型快,因为Currency数据类型不使用浮点处理器。
(3)如果在一个过程中多次引用一个对象,可以创建对象变量,并将对给对象的引用指派给它。因为对象变量存储对象在内存中的位置,VBA将不必再次查找其位置。
(4)尽可能使用早期绑定,这样不仅方便编码,更方便编译器查找成员。
绑定是指将程序调用与实际代码相匹配。为了实现早期绑定,先应创建对对象库的引用(这个我们在前面的COM对象使用中见得太多了)。早期绑定可以在代码中使用定义在对象库中的常量,可以自动列出对象的方法和属性。但早期绑定只有在所控制的对象拥有独立的类型库或对象库文件才适用且还需要已安装了特定的库。而后期绑定则只是在运行时才知道对象的类型并对对象进行引用,因此不具备上述特点。
使用早期绑定创建对象通常更有效率,使代码能获得更好的性能。因为对象的早期绑定引用在编译时可以通过VBE的解析,而不是通过运行时模块解析,因此早期绑定的性能要好得多。虽然在程序设计时不可能总是使用早期绑定,但应该尽可能使用它。
(5)多次使用的数值尽量定义成常量,易于修改,易于查找。
(6)减少变量的作用范围并及时释放变量(特别是对象实例,对于Recordset的使用,我们已经见过多次了)
'使用对象...
Set AnObj=Nothing ‘释放对象变量
2、尽量使用VBA内置函数与工作表函数
充分利用VBA内置函数与WorksheetFunction中的函数是提高程序运行速度的极度有效的方法。
如求平均工资的例子:
TotalValue = TotalValue + c.Value
Next
AverageValue = TotalValue / Worksheet(1).Range(″A1:A1000″).Rows.Count
而下面代码程序比上面例子快得多:
其它函数如Count,Counta,Countif,Match,Lookup等等,都能代替相同功能的VBA程序代码,提高程序的运行速度。
3、尽量使用Range对象的SpecialCells,AutoFill,Formula等方式,替换循环单元格的做法
例如快速填充空行:
Selection.SpecialCells(xlCellTypeBlanks) = "=r[-1]c"
End Sub
4、尽可能使用For Each…Next循环集合
可以使用For Each…Next循环来保证程序代码更快地执行。在使用For Each…Next循环时,对于存储在集合或数组中的每个对象执行一组语句,程序更简洁,也更容易阅读、调试和维护。当For Each…Next语句迭代集合时,自动指定一个对集合当前成员的引用,然后在到达集合的尾部时跳出循环语句。
5、尽可能在执行循环时节省资源
(1)把与循环无关的操作拿出去。例如,是否可以在循环外(而不是在循环中)设置某些变量?每次都通过循环执行的转换过程是否可以在循环之外执行?
(2)考虑尽早退出循环。例如,假设正在对一个不应该包含数字字符的字符串进行数据验证。如果循环要检查字符串中的每个字符以确定其中是否包含数字字符,那么您可以在找到第一个数字字符时立即退出循环。
(3)如果必须在循环中引用数组的元素,可以创建一个临时变量存储该元素的值,而不是引用数组中的值。从数组中检索值比从相同类型的变量读取值要慢的多。
(4)有可能的话,减少循环的步长,也就是减少循环的次数。
当使用有针对性的For循环,即仅仅需要对循环对象中的部分对象进行操作时,应该调整循环的步长来减少循环的次数。
对比下面两个循环:
If i Mod 2 = 1 Then Cells(i, 1).EntireRow.Interior.ColorIndex = 23
Next i
For i = 1 To 10000 Step 2
Cells(i, 1).EntireRow.Interior.ColorIndex = 23
Next i
实现同样的功能,但却循环的次数有差异,明显是第二个循环效率更高。
6、一次性完成赋值与粘贴
在使用Copy方法时,可以在一个语句中指定复制的内容及要复制到的目的地。
例如:
Selection.Copy
Range("B8").Select
ActiveSheet.Paste
经修改后的最佳代码是:
7、选用合适的操作符,加速对数字的运算
(1)当对整数进行除法时,您可以使用整型除法运算符(\)而不是浮点除法运算符(/),因为无论参与除法运算的数值类型如何,浮点除法运算符总会返回Double类型的值。
(2)在任何具有整数值的算术表达式中使用Single或Double值时,整数均将被转换成Single或Double值,最后的结果将是Single或Double值。如果要对作为算术运算结果的数字执行多次操作,可能需要明确地将该数字转换为较小的数据类型。
8、提高字符串操作的性能
(1)尽可能少使用连接操作。
连接操作符很多时候可以使用Replace,Mid函数代替。例如,可以在等号左边使用Mid函数替换字符串中的字符,而不是将它们连接在一起。注意,使用Mid 函数的缺点是替换字符串必须与要替换的子字符串的长度相同。
strText = "this is a test"
Mid(strText, 11, 4) = "tent"
(2)VBA提供许多可用来替换函数调用的内部字符串常量。例如,可以使用vbCrLf常量来表示字符串中的回车/换行组合,而不是使用Chr(13) & Chr(10)。
(3)字符串比较操作的执行速度很慢。
在VBA 中,可以使用Chr$()函数把数转换成字符,并确定ANSI的值,也可以使用Asc()函数把字符串转换成数值,然后确定它的ANSI值。如果需要进行有限次数的这种检验,对程序代码的效率可能不会产生很大影响,但是,如果需要在多个循环内进行这种检验时,这将节省处理时间并且有助于程序代码更快地执行。 例如,下列代码会检查字符串中的第一个字符是否为空格:
上面的代码会比以下代码更快:
(4)、使用Len()检验空串
尽管有多种方法可检验空串,但首选的是使用Len()函数。为了测试零长度的串,可以选择把串与””相比较,或者比较串的长度是否为0,但这些方法比用Len()函数要用更多的执行时间。当对字符串应用Len()函数并且函数返回0值时,说明该字符串是空的或者是零长度的字符串。 并且,因为在If语句内非零值被认为是True,所以直接使用Len()函数而不必与””或0比较,减少了处理时间,因此执行更快。
(5)、善用带$的字符串处理函数
在VBA中,有两套字符串处理函数,包含带"$"和不带"$"的函数,例如mid 和mid$,Left 和Left$,Right 和Right$。如果不使用带"$"符号的函数计算字符串,那么VBA将字符串作为Variant类型来进行计算,而使用带"$"的函数时,则将字符串当作string类型来进行计算,显示Variant型数据在计算时需要更多的内存空间。
如下面两句代码:
Str=mid$("Wise",2)
第二句在执行效率上会占优势。
9、只要有可能就使用集合索引值
我们能在集合中使用名称或者数字来指定某个单一的对象,但使用对象的索引值通常是更快的。如果您使用对象的名字,VBA必须解析名字成为索引值;但如果您使用索引值,就能避免这个额外的步骤。
但另一方面,我们要注意到在集合中通过名称指定对象有很多优点。使用对象名称能使您的代码更容易阅读和调试。此外,通过名称指定一个对象比通过索引值更安全,因为当您的代码运行时该对象的索引值可能变化。例如,某菜单的索引值表示它在菜单栏中的位置,但是如果在菜单栏中添加了菜单或者删除了菜单,该菜单的索引值会变化。这样,您就不应该考虑代码的速度,而应保证代码运行可靠。您使用索引值加快代码速度之前,应该确保该索引值在代码运行过程中或使用应用程序时不会改变。
分享一个代码优化的经典合集:http://club.excelhome.net/thread-509998-1-1.html
四、代码规范的优化
优化了算法,采用了合适的函数和对象实现了算法后,代码其实还是有优化的空间,比如编程习惯,代码风格等,下面从这些方面总结一下。
1、尽量减少无用的操作,如对象的激活和选择
事实上大多数情况下激活和选择操作都只是有一点视觉效果,但很遗憾这对于VBA来说不是必需的。例如:
Range(″A1″).Value = 100
Range(″A2″).Value = 200
可改为:
.Range(″A1″).Value = 100
.Range(″A2″).Value = 200
End With
2、尽量减少使用对象引用,即减少“.”的使用,尤其在循环中
每一个Excel对象的属性、方法的调用都需要通过OLE接口的一个或多个调用,这些OLE调用都是需要时间的,减少使用对象引用能加快VBA代码的运行。例如
(1)使用With语句。
Workbooks(1).Sheets(1).Range(″A1:A1000″).Font.FontStyle=″Bold″
则以下语句比上面的快:
.Name = ″Pay″
.FontStyle = ″Bold″
End With
(2)使用对象变量。
如果你发现一个对象引用被多次使用,则你可以将此对象用Set 设置为对象变量,以减少对对象的访问。如:
Workbooks(1).Sheets(1).Range(″A2″).Value = 200
则以下代码比上面的要快:
MySheet.Range(″A1″).Value = 100
MySheet.Range(″A2″).Value = 200
(3)在循环中要尽量减少对象的访问。
Sheets(″Sheet1″).Select
Cells(k,1).Value = Cells(1,1).Value
Next k
则以下代码比上面的要快:
Sheets(″Sheet1″).Select
For k = 1 To 1000
Cells(k,1).Value = TheValue
Next k
3、有效地使用数组与变量,尽量少使用单元格直接参与计算
通常单元格的操作都比较慢,可以先将单元格的值读入变量或数组变量,对变量进行运算,这样可以提高处理的速度。处理结束以后,只要用一个语句就可以将数组中的数据传递回单元格区域中。
在创建已知元素的确定数组时,使用Array函数对于节约空间和时间以及写出更具效率的代码是非常理想的。例如:
Names=Array(“Fan”,“Yang”,“Wu”,“Shen”)
此外,应该尽量使用固定大小的数组。如果确实选择使用了动态数组,应该避免数组每增加一个元素就改变一次数组的大小,最好是每次增加一定数量的元素。
下面是一些扩展读物,有兴趣的同学可以试试看:
一个遍历大范围Range的各种方法的比较:http://www.vbafan.com/2009/01/22/what-is-the-fastest-way-to-scan-a-large-range-in-excel/。
Excel2010性能的改进:http://msdn.microsoft.com/zh-cn/library/ff700514.aspx。
提高Excel2010的计算性能:http://msdn.microsoft.com/zh-cn/library/ff700515.aspx。
Excel2010性能优化提示:http://msdn.microsoft.com/zh-cn/library/ff726673.aspx。