VBA-字符串处理-语言层级bug

一. 前言

最近碰到一个比较有趣的vba编写的错误, 处理该问题时, 突然想起很早之前的一个vba语言层的bug.

相关内容见本人在这个帖子上的描述, @liucqa你的问题暂时解决了_VBA字符串/字符编码/字符集谜题-Excel VBA程序开发-ExcelHome技术论坛 -.

二. 问题

该问题为语言层级的bug所导致.

该问题在百度上能够检索到的相关页面最早见于可能是在2010版本的office上出现.

问题很简单, 当使用instr, split函数处理字符串时, 部分的字符串上会出现异常错误.

Sub test()
	Debug.Print InStr(1, ChrW$(12500), "a", vbTextCompare)
End Sub
' vbTextCompare, 进行文本模式比较
pCC2jg0.png
Option Explicit

Sub test()
    Dim s As String
    Dim i As Long, k As Long, ic As Long
    Dim arr() As Long

    On Error Resume Next
    ic = 0
    For i = 1 To 65535
        s = ChrW$(i)
        k = InStr(1, s, "a", vbTextCompare)
        If Err.Number > 0 Then
            ReDim Preserve arr(ic)
            arr(ic) = i
            ic = ic + 1
            Err.Clear
        End If
    Next
    If ic > 0 Then Cells(2, 1).Resize(ic, 1).Value = Application.Transpose(arr)
End Sub

通过上述代码得到以下可以导致异常错误字符串的Unicode, 使用excelUNICHAR函数生成对应字符.

异常unicode 异常字符
12460
12462
12464
12466
12468
12470
12472
12474
12476
12478
12480
12482
12485
12487
12489
12496
12497
12499
12500
12502
12503
12505
12506
12508
12509
12532
12535
12536
12537
12538
12542

可以看到这些导致异常的字符均为日文, 呈现一定的规律性, 间隔多为1 - 3之间.

三. 小结

理论上, 32位和64位应该是一样的, 32位只测试了2013, 2016测试了32位, 64位, 其余的只测试64位.

以下均为office中文版(简体), 专业增强版本.

win8.1, win10下进行的测试.

经过实际测试, 结果如下:

office版本 是否出现bug
2013 yes
2016 yes
2019 no
2021 no

在二进制(vbBinaryCompare)下的比较并未出现任何异常, 为默认状态下instr函数使用参数, 即不区分大小写.

Option Explicit

Sub test()
    Dim s As String
    Dim i As Long, k As Long, ic As Long
    Dim arr() As Long

    On Error Resume Next
    ic = 0
    For i = 1 To 65535
        s = ChrW$(i)
        k = InStr(1, s, "a", vbBinaryCompare)
        If Err.Number > 0 Then
            ReDim Preserve arr(ic)
            arr(ic) = i
            ic = ic + 1
            Err.Clear
        End If
    Next
    If ic > 0 Then Cells(2, 1).Resize(ic, 1).Value = Application.Transpose(arr)
End Sub

虽然微修复该bug前后间隔将近十年, 也许微软还不希望vba彻底入土为安(微软自VBE7发布之后已经基本没有再为VBA显著添加些什么).

这种涉及到字符(编码)的问题, 对于vba这个不在更新的古老语言而言, 基本是等同于癌症, 只能化疗维持下去.

四. 延展

判断某个字符串是否出现在另一个字符串上, 除了instr函数, 也可以使用StrStr系列api来实现(使用api需要谨慎, 非熟(高)手不建议在业务中的vba代码使用api, 导致的异常错误可能导致文件彻底被破坏, 信息不可找回).

PCWSTR StrStrNW(
  [in] PCWSTR pszFirst,
  [in] PCWSTR pszSrch,
       UINT   cchMax
);
' 32位
Private Declare Function uStrStrNW Lib "Shlwapi.dll" Alias "StrStrNW" (ByVal sText As Long, ByVal sFind As Long, ByVal iLength As Long) As Long '大小写不明感
Private Declare Function uStrStrW Lib "Shlwapi.dll" Alias "StrStrW" (ByVal sText As Long, ByVal sFind As Long) As Long
' 64位
Private Declare PtrSafe Function uStrStrNW Lib "Shlwapi.dll" Alias "StrStrNW" (ByVal sText As LongLong, ByVal sFind As LongLong, ByVal iLength As Long) As Long '大小写不明感
Private Declare PtrSafe Function uStrStrW Lib "Shlwapi.dll" Alias "StrStrW" (ByVal sText As LongLong, ByVal sFind As LongLong) As Long

Sub test()
    Dim a As String
    Dim b As String
    a = ChrW$(12500)
    b = "a"
    Debug.Print uStrStrW(StrPtr(a), StrPtr(b))
End Sub

但是需要注意返回的是匹配字符的内存地址(不匹配返回0), 不是字符串出现的位置, 上述函数可以判断是否有出现的字符串, 但是不能判断位置.

Returns the address of the first occurrence of the matching substring if successful, or NULL otherwise.

鉴于操作内存是极为危险的, 微软并未官方提供相关文档Unofficial Documentation for VarPtr, StrPtr, and ObjPtr (classicvb.net)的支持, StrPtr返回的是字符串在内存中的地址. 在64位下, 需要使用longlong数据类型, longlong类型数据的支持和VBE7是微软最后为vba留下的不多的遗产了.

操作内存通常在大型文本数据的处理上, 如在数百兆大小文本中查找特定的字符串, 对大型数组的文本进行排序等, 通过直接访问内存, 可以极大的提高数据处理的速度.