表根据范围干扰VBA范围variables

Excel文件包含部署在表(VBA listobjects)中的VBA编码的用户定义函数(UDF)。 现在,出于逃避我的原因,如果UDF模块包含声明在任何子函数或函数范围之外的Rangevariables,当文件打开时,会出现非常严重的警告:“自动错误 – 灾难性故障”。

“灾难性”似乎有些夸张,因为在警告被驳回之后,文件似乎正常工作。 但是我仍然想明白这个问题是什么。 我已经设法复制这个问题与MVC的例子如下。 我在Windows 10上运行Excel 2016(更新)。

有两个表(即VBA列表对象): 表1列出了“项目”, 表2列出了“项目function”(两个表格都是通过select数据并单击“ Insert选项卡上的表格生成的)。 表2在Item_Name字段中有一个名为ITEM_NAME()的UDF,它根据项目ID返回项目名称,请参阅截图:

在这里输入图像说明

函数ITEM_NAME()实际上是常规工作表函数INDEX和MATCH的一个包装,如下面的代码所示:

 Option Explicit Dim mrngItemNumber As Range Dim mrngItemName As Range Public Function ITEM_NAME(varItemNumber As Variant) As String ' Returns Item Name as a function of Item Number. Set mrngItemNumber = Sheets(1).Range("A4:A6") Set mrngItemName = Sheets(1).Range("B4:B6") ITEM_NAME = Application.WorksheetFunction.Index(mrngItemName, _ Application.WorksheetFunction.Match(varItemNumber, mrngItemNumber)) End Function 

所以,重复一遍,用这个设置, 打开文件时会出现自动化错误。 但是,当我执行以下任一操作时,错误消失:

  1. 将声明移到函数的作用域中。 这个解决scheme没有吸引力,因为它需要更多的代码行,每个UDF都有一行代码,而且有很多。

  2. 将variablestypes从Range更改为其他值,例如Integer (所以函数显然不起作用)。

  3. 转换表2到一个普通的范围(即删除表)。 这也是一个不方便的解决scheme,因为我真的想在我的代码中使用Tablefunction用于其他目的。

  4. 从表2中删除函数ITEM_NAME() (显然没有吸引力的select..)

这是怎么回事? 为什么我会收到错误信息? 为什么尽pipe警告,文件似乎仍然正常工作? 有没有我错过的解决方法?

我怀疑它可能与表单对象和列表对象如何交互有关,但不确定。 这个答案提供了一个可能的提示来解决另一个问题:

如果你想引用一张表而不使用工作表,你可以使用一个黑客Application.Range(ListObjectName).ListObject

注意:这个hack依赖于这样的事实,即Excel总是为表的DataBodyRange创build一个与表名相同的命名范围。

其他地方( Stackoverflow和Microsoft Technet )也报告了类似的问题,但是并没有提到这个特殊的问题。 build议的解决scheme包括检查损坏的引用或在后台运行的其他进程,我已经这样做,无济于事。 我还可以补充一点:在创build表2之后,是否input了ITEM_NAME函数,而不是之前; 唯一的区别是它在这种情况下使用结构化引用 (如上面的截图所示)。

更新︰启发@ SJR的评论下面我尝试了以下代码的变化,其中声明一个ListObjectvariables来存储表“Items”。 请注意, Range声明现在在函数的范围内,并且只有ListObject声明在外。 这也会产生相同的自动化错误!

 Option Explicit Dim mloItems As ListObject Public Function ITEM_NAME(varItemNumber As Variant) As String ' Returns Item Name as a function of Item Number. Dim rngItemNumber As Range Dim rngItemName As Range Set mloItems = Sheet1.ListObjects("Items") Set rngItemNumber = mloItems.ListColumns(1).DataBodyRange Set rngItemName = mloItems.ListColumns(2).DataBodyRange ITEM_NAME = Application.WorksheetFunction.Index(rngItemName, _ Application.WorksheetFunction.Match(varItemNumber, rngItemNumber)) End Function 

更新2:现在的问题似乎已经解决了,但是我并不是很聪明,究竟是什么造成的。 由于没有人可以复制(甚至连我的朋友在不同的系统上打开同一个文件),我开始认为这是一个本地问题。 我试图修复Excel,然后甚至从头重新安装完整的Office包。 但是这个问题依然存在,无论是用我的MCV文件来创build上面的例子,还是我发现问题的原始文件。

我决定尝试创build一个新版本的MCV示例,在AndrewD的答案的启发下 ,我使用.ListObjects()来设置范围,而不是使用.Range() 。 这确实有效。 我可能会适应这个解决scheme,我的工作(但看到我的意见下AndrewD的问题,解释为什么我可能更喜欢.Range() 。)

为了仔细检查这个解决scheme的工作,我着手创build两个新的文件,一个复制我自己的例子,如上所述,唯一的区别是切换到ListObjects() 。 在这个过程中,我注意到我实际上在原始文件的代码开始处缩进了Range声明,如下所示:

 Option Explicit Dim mrngItemNumber As Range Dim mrngItemName As Range Public Function ITEM_NAME(... 

没有多less思考,我创build了新的文件,但没有缩进。 所以这将是以前的文件(和上面给出的例子)的精确副本,但没有缩进。 但是,看这个文件,我无法复制自动化错误! 在检查这两个文件之后,我注意到唯一的区别确实是缩进,所以我把缩进放回到新文件中,期望它再次生成自动化错误。 但问题没有再出现。 所以然后我删除了第一个文件(用于创build上面的示例)的缩进,现在自动化错误也从该文件中消失。 有了这个观察,我回到了我真正的文件,我第一次发现这个问题,并简单地删除了那里的缩进。 它的工作。

因此,总结一下,删除Range声明的缩进后,我无法重新生成之前生成的三个文件中的任何一个的自动化错误。 而且,即使我再次把缩进放回原处,问题也不会再出现。 但是我仍然不明白为什么。

谢谢大家花时间看这个,分享宝贵意见。

声明模块级别的variables只是为了保存每个UDF中的两行,否则这将是不好的编码习惯。 但是,如果这是你的想法,为什么不去所有的方式,并保存每个UDF四行,避免每个都设置它们!

您可以通过使用伪常量函数来执行此操作,如下面的代码所示:

 Option Explicit Private Function rng_ItemNumber() As Range Set rng_ItemNumber = Sheet1.Range("A4:A6") End Function Private Function rng_ItemName() As Range Set rng_ItemName = Sheet1.Range("B4:B6") End Function Public Function ITEM_NAME(varItemNumber As Variant) As String ' Returns Item Name as a function of Item Number. With Application.WorksheetFunction ITEM_NAME = .Index(rng_ItemName, .Match(varItemNumber, rng_ItemNumber)) End With End Function 

成本当然是函数调用的开销。


如果您打算将ListObject类用于最终devise,那么现在为什么不使用它, 并且使用dynamic命名范围(示例中的硬编码范围在那里,所以它实际上按原样工作 – 应该用命名的范围):

 Option Explicit Private Function str_Table1() As String Static sstrTable1 As String If sstrTable1 = vbNullString Then sstrTable1 = Sheet1.Range("A4:B6").ListObject.Name End If str_Table1 = sstrTable1 End Function Private Function str_ItemNumber() As String Static sstrItemNumber As String If sstrItemNumber = vbNullString Then sstrItemNumber = Sheet1.Range("A4:A6").Offset(-1).Resize(1).Value2 End If str_ItemNumber = sstrItemNumber End Function Private Function str_ItemName() As String Static sstrItemName As String If sstrItemName = vbNullString Then sstrItemName = Sheet1.Range("B4:B6").Offset(-1).Resize(1).Value2 End If str_ItemName = sstrItemName End Function Public Function ITEM_NAME(varItemNumber As Variant) As String 'Returns Item Name as a function of Item Number. Dim ƒ As WorksheetFunction: Set ƒ = WorksheetFunction With Sheet1.ListObjects(str_Table1) ITEM_NAME _ = ƒ.Index _ ( _ .ListColumns(str_ItemName).DataBodyRange _ , ƒ.Match(varItemNumber, .ListColumns(str_ItemNumber).DataBodyRange) _ ) End With End Function 

一旦逻辑/devise准备就绪,如果速度非常关键,则可以用具有相同名称的模块级常量replace函数,并且需要回收函数调用开销。 否则,你可以留下一切。

请注意,不需要使用静态variables,但应该减less执行时间。 (静态variables也可以在第一个例子中使用,但是我把它们放在一边以保持简短。)

可能并不是真的有必要将表名抽出为伪常量,但为了完整起见,我已经这样做了。


编辑: (v2)

跟随Egalth的两个精彩的build议,导致下面的代码,避免命名范围的需要, 甚至硬编码单元格地址 ,因为我们利用ListObject表本身的内部dynamic。

我还更改了参数名称以匹配*相关的列标题名称,因此当用户按下Ctrl + Shift + A提示要使用哪个列时。 (这个技巧,如果需要的话,关于如何添加Intellisense工具提示和/或获取描述出现在函数参数对话框中的更多信息可以在这里看到。)

 Option Explicit Private Function str_Table1() As String Static sstrTable1 As String If sstrTable1 = vbNullString Then sstrTable1 = Sheet1.ListObjects(1).Name ' or .ListObjects("Table1").Name str_Table1 = sstrTable1 End Function Private Function str_ItemNumber() As String Static sstrItemNumber As String If sstrItemNumber = vbNullString Then sstrItemNumber = Sheet1.ListObjects(str_Table1).HeaderRowRange(1).Value2 End If str_ItemNumber = sstrItemNumber End Function Private Function str_ItemName() As String Static sstrItemName As String If sstrItemName = vbNullString Then sstrItemName = Sheet1.ListObjects(str_Table1).HeaderRowRange(2).Value2 End If str_ItemName = sstrItemName End Function Public Function ITEM_NAME(ByRef Item_ID As Variant) As String 'Returns Item Name as a function of Item Number. Dim ƒ As WorksheetFunction: Set ƒ = WorksheetFunction With Sheet1.ListObjects(str_Table1) ITEM_NAME _ = ƒ.Index _ ( _ .ListColumns(str_ItemName).DataBodyRange _ , ƒ.Match(Item_ID, .ListColumns(str_ItemNumber).DataBodyRange) _ ) End With End Function 

请注意.Value2的用法。 自从我发现了使用.Value (或依赖它作为默认属性)时执行的隐式types转换导致的性能拖延和其他问题以来,我一直使用.Value2

*当项目的逻辑/devise完成时,确保更新代码中的列标题名称。


编辑:(重新启动)

重新阅读你自己的意见,你发布的问题,我注意到这一个 :

我可能最终会采用这种方法,但是我仍然在devise过程中,并且将列移动很多,所以索引号也可能会改变

尽pipe上面的最后一个示例允许dynamic更改标题名称,但移动/插入列会更改索引,需要修改代码。

看起来我们回到使用命名的范围。 但是,这次我们只需要指向列标题的静态指针。

这也certificate,对于这个新的情况,静态variables在devise阶段是一个主意。 由于列索引被caching,因此插入一个新的列会破坏UDF,直到项目被重置。

我还在您发布的问题中引用了无引纸表格引用hack的缩写版本:

 Option Explicit Private Function str_Table1() As String str_Table1 = Sheet1.ListObjects(1).Name End Function Private Function str_ItemNumber() As String With Range(str_Table1).ListObject str_ItemNumber = .HeaderRowRange(.Parent.Range("A3").Column - .HeaderRowRange.Column + 1).Value2 End With End Function Private Function str_ItemName() As String With Range(str_Table1).ListObject str_ItemName = .HeaderRowRange(.Parent.Range("B3").Column - .HeaderRowRange.Column + 1).Value2 End With End Function Public Function ITEM_NAME(ByRef Item_ID As Variant) As String 'Returns Item Name as a function of Item Number. Dim ƒ As WorksheetFunction: Set ƒ = WorksheetFunction With Range(str_Table1).ListObject ITEM_NAME _ = ƒ.Index _ ( _ .ListColumns(str_ItemName).DataBodyRange _ , ƒ.Match(Item_ID, .ListColumns(str_ItemNumber).DataBodyRange) _ ) End With End Function 

请注意,您不能使用Item_name作为其中一个命名范围,因为它与UDF(忽略大小写)相同。 我build议使用尾部下划线,例如Item_name_ ,作为您的命名范围。


以上所有方法也都解决了你原来的问题。 我正在等待最后一些信息,以便作出一个有根据的猜测,为什么这个问题发生在第一位。

好。 此解决方法应该可以工作。

如果这样做,有几个问题和注意事项来解决。

我也会发表解释。

将代码安装在ThisWorkbook模块中。

码:

 Private Sub Workbook_BeforeClose(Cancel As Boolean) Dim rngCell As Range For Each rngCell In ActiveSheet.UsedRange.SpecialCells(xlCellTypeFormulas) With rngCell If .FormulaR1C1 Like "*ITEM_NAME*" _ And Left$(.FormulaR1C1, 4) <> "=T(""" _ Then .Value = "=T(""" & .FormulaR1C1 & """)" End If End With Next rngCell End Sub Private Sub Workbook_Open() Dim rngCell As Range For Each rngCell In ActiveSheet.UsedRange.SpecialCells(xlCellTypeFormulas) With rngCell If .FormulaR1C1 Like "*ITEM_NAME*" _ And Left$(.FormulaR1C1, 4) = "=T(""" _ Then .FormulaR1C1 = .Value End If End With Next rngCell End Sub 

在纯粹的代码级别,为什么声明模块级别的variables来存储范围,当你设置他们每一次? 如果你caching的引用,只设置它们,如果没有我能理解…但是,然后你会使用静态来减less范围。

我最好不要打扰模块化(或本地/静态)variables,将Worksheet.Name引用replace为Worksheet.CodeName (不太可能被改变,如果你在重命名后编译,你会得到一个错误),并参考该表的范围通过ListObjectListColumns (万一表的大小发生变化)。

 ' Returns the item name for the requested item ID. Public Function ITEM_NAME(ByVal ItemID As Variant) As String ITEM_NAME = Application.WorksheetFunction.Index( _ Sheet1.ListObjects("Table1").ListColumns("Item_name").DataBodyRange _ , Application.WorksheetFunction.Match( _ ItemID _ , Sheet1.ListObjects("Table1").ListColumns("Item_ID").DataBodyRange _ ) _ ) End Function 

但是最强大的解决scheme是避免使用UDF,并使用=INDEX(Table1[Item_name],MATCH([@[Item_ID]],Table1[Item_ID]‌​)) (VLOOKUP可能稍快,但INDEX + MATCH更多强大的)。