当我致力于将一些古老的代码带入未来时,我发现了这个关于缓冲和私有配置文件 API 的问题。经过我自己的实验和研究,我可以确认提问者关于无法确定字符串何时恰好为 nSize - 1 或缓冲区太小之间的差异的原始陈述。
有没有更好的办法?Mike 接受的回答说没有根据文档,您应该尝试确保缓冲区足够大。Marc 说要增加缓冲区。Roman 说检查错误代码。一些随机用户说您需要提供一个足够大的缓冲区,并且与 Marc 不同的是,它继续显示一些扩展他的缓冲区的代码。
有没有更好的办法?让我们了解事实!
由于 ProfileString API 的年代久远,因为这个问题的所有标签都不涉及任何特定语言并且为了便于阅读,我决定使用 VB6 来展示我的示例。随意为您自己的目的翻译它们。
GetPrivateProfileString 文档
根据GetPrivateProfileString 文档,提供这些 Private Profile 函数只是为了与基于 Windows 的 16 位应用程序兼容。这是很好的信息,因为它使我们能够了解这些 API 函数可以做什么的限制。
16 位有符号整数的范围为 -32,768 到 32,767,无符号 16 位整数的范围为 0 到 65,535。如果这些函数真的是为在 16 位环境中使用而设计的,那么我们遇到的任何数字很可能都会被限制在这两个限制之一。
该文档指出,返回的每个字符串都将以空字符结尾,并且还说不适合提供的缓冲区的字符串将被截断并以空字符终止。因此,如果一个字符串确实适合缓冲区,则倒数第二个字符以及最后一个字符都将为空。如果只有最后一个字符为空,则提取的字符串与提供的缓冲区的长度完全相同 - 1 或缓冲区不够大以容纳字符串。
在倒数第二个字符不为空的情况下,提取的字符串的长度准确或对于缓冲区来说太大,GetLastError 将返回错误号234 ERROR_MORE_DATA (0xEA),使我们无法区分它们。
GetPrivateProfileString 接受的最大缓冲区大小是多少?
虽然文档没有说明最大缓冲区大小,但我们已经知道这个 API 是为 16 位环境设计的。经过一些实验,我可以得出结论,最大缓冲区大小为65,536. 如果文件中的字符串大于 65,535 个字符,我们会在尝试读取字符串时开始看到一些奇怪的行为。如果文件中的字符串长度为 65,536 个字符,则检索到的字符串长度为 0 个字符。如果文件中的字符串长度为 65,546 个字符,则检索到的字符串长度为 10 个字符,以空字符结尾并从文件中包含的字符串的最开头截断。API 将写入大于 65,535 个字符的字符串,但无法读取大于 65,535 个字符的任何内容。如果缓冲区长度为 65,536 且文件中的字符串长度为 65,535 个字符,则缓冲区将包含文件中的字符串,并且还以单个空字符结尾。
这为我们提供了第一个虽然不是完美的解决方案。如果您想始终确保您的第一个缓冲区足够大,请将该缓冲区设置为 65,536 个字符长。
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String
On Error GoTo iniReadError
Dim Buffer As String
Dim Result As Long
Buffer = String$(65536, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, 65536, Pathname)
If Result <> 0 Then
iniRead = Left$(Buffer, Result)
Else
iniRead = Default
End If
iniReadError:
End Function
现在我们知道了最大缓冲区大小,我们可以使用文件的大小来修改它。如果您的文件长度小于 65,535 个字符,则可能没有理由创建如此大的缓冲区。
在文档的备注部分中,它说初始化文件中的一个部分必须具有以下形式:
[部分]
键=字符串
我们可以假设每个部分都包含两个方括号和一个等号。经过一个小测试,我能够验证 API 是否可以接受节和键之间的任何类型的换行符(vbLf、vbCr 或 vbCrLf / vbNewLine)。这些细节以及节和键名的长度将允许我们缩小最大缓冲区长度,并确保文件大小足以在我们尝试读取文件之前包含一个字符串。
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String
On Error Resume Next
Dim Buffer_Size As Long
Err.Clear
Buffer_Size = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Size > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Buffer_Size = Buffer_Size - Len(Section) - Len(Key) - 4
If Buffer_Size > 65535 Then
Buffer_Size = 65536
Else
Buffer_Size = Buffer_Size + 1
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniRead = Default
iniReadError:
End Function
增加缓冲区
现在我们已经非常努力地确保第一个缓冲区足够大,并且我们已经修改了最大缓冲区大小,对我们来说,从较小的缓冲区开始并逐渐增加缓冲区的大小来创建可能仍然更有意义一个足够大的缓冲区,我们可以从文件中提取整个字符串。根据文档,API 会创建 234 错误来告诉我们还有更多可用数据。他们使用这个错误代码告诉我们用更大的缓冲区重试是很有意义的。一遍又一遍地重试的缺点是它的成本更高。文件中的字符串越长,读取它所需的尝试次数越多,花费的时间就越长。64 KB 对于今天的计算机来说并不算多,而且今天的计算机速度非常快,
我已经对 GetPrivateProfileString API 进行了相当多的搜索,我发现通常当没有广泛了解 API 的人尝试创建足够大的缓冲区以满足他们的需求时,他们选择 255 的缓冲区长度。这将允许您可以从文件中读取最多 254 个字符的字符串。我不确定为什么有人开始使用它,但我假设有人在某个地方想象这个 API 使用缓冲区长度限制为 8 位无符号数的字符串。也许这是WIN16的限制。
除非最大缓冲区长度更小,否则我将从 64 字节的低缓冲区开始,然后将数字翻两番,直到最大缓冲区长度或 65,536。将数字加倍也是可以接受的,更大的乘法意味着更少尝试读取更大字符串的文件,而相对而言,一些中等长度的字符串可能有额外的填充。
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String
On Error Resume Next
Dim Buffer_Max As Long
Err.Clear
Buffer_Max = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Max > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Dim Buffer_Size As Long
Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4
If Buffer_Max > 65535 Then
Buffer_Max = 65536
Else
Buffer_Max = Buffer_Max + 1
End If
If Buffer_Max < 64 Then
Buffer_Size = Buffer_Max
Else
Buffer_Size = 64
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
If Buffer_Max > 64 Then
Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max
Buffer_Size = Buffer_Size * 4
If Buffer_Size > Buffer_Max Then
Buffer_Size = Buffer_Max
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
Loop
End If
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniRead = Default
iniReadError:
End Function
改进的验证
根据您的实现,改进对路径名、部分和键名的验证可能会阻止您需要准备缓冲区。
根据维基百科的 INI 文件页面,他们说:
在 Windows 实现中,密钥不能包含字符等号 (=) 或分号 (;),因为它们是保留字符。该值可以包含任何字符。
和
在 Windows 实现中,该部分不能包含字符右括号 ( ] )。
对 GetPrivateProfileString API 的快速测试证明这只是部分正确。只要分号不在开头,我就可以在键名中使用分号。他们没有在文档或维基百科中提及任何其他限制,尽管可能还有更多限制。
查找 GetPrivateProfileString 接受的部分或键名的最大长度的另一个快速测试给了我 65,535 个字符的限制。使用大于 65,535 个字符的字符串的效果与我在测试最大缓冲区长度时的体验相同。另一项测试证明,此 API 将接受一个空白字符串作为部分或键名。根据 API 的功能,这是一个可接受的初始化文件:
[]
=你好世界!
根据维基百科,对空格的解释各不相同。在又一次测试之后,Profile String API 肯定会从部分和键名中去除空格,所以如果我们也这样做可能没问题。
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String
On Error Resume Next
If Len(Pathname) <> 0 Then
Key = Trim$(Key)
If InStr(1, Key, ";") <> 1 Then
Section = Trim$(Section)
If Len(Section) > 65535 Then
Section = RTrim$(Left$(Section, 65535))
End If
If InStr(1, Section, "]") = 0 Then
If Len(Key) > 65535 Then
Key = RTrim$(Left$(Key, 65535))
End If
If InStr(1, Key, "=") = 0 Then
Dim Buffer_Max As Long
Err.Clear
Buffer_Max = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Max > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Dim Buffer_Size As Long
Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4
If Buffer_Max > 65535 Then
Buffer_Max = 65536
Else
Buffer_Max = Buffer_Max + 1
End If
If Buffer_Max < 64 Then
Buffer_Size = Buffer_Max
Else
Buffer_Size = 64
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
If Buffer_Max > 64 Then
Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max
Buffer_Size = Buffer_Size * 4
If Buffer_Size > Buffer_Max Then
Buffer_Size = Buffer_Max
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
Loop
End If
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniRead = Default
End If
End If
End If
End If
iniReadError:
End Function
静态长度缓冲区
有时我们需要存储具有最大长度或静态长度的变量。用户名、电话号码、颜色代码或 IP 地址是您可能希望限制最大缓冲区长度的字符串示例。必要时这样做可以节省您的时间和精力。
在下面的代码示例中,Buffer_Max 将被限制为 Buffer_Limit + 1。如果限制大于 64,我们将从 64 开始并像以前一样扩展缓冲区。小于 64 并且我们只会使用我们的新缓冲区限制读取一次。
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String, Optional Buffer_Limit As Long = 65535) As String
On Error Resume Next
If Len(Pathname) <> 0 Then
Key = Trim$(Key)
If InStr(1, Key, ";") <> 1 Then
Section = Trim$(Section)
If Len(Section) > 65535 Then
Section = RTrim$(Left$(Section, 65535))
End If
If InStr(1, Section, "]") = 0 Then
If Len(Key) > 65535 Then
Key = RTrim$(Left$(Key, 65535))
End If
If InStr(1, Key, "=") = 0 Then
Dim Buffer_Max As Long
Err.Clear
Buffer_Max = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Max > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Dim Buffer_Size As Long
Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4
If Buffer_Limit > 65535 Then
Buffer_Limit = 65535
End If
If Buffer_Max > Buffer_Limit Then
Buffer_Max = Buffer_Limit + 1
Else
Buffer_Max = Buffer_Max + 1
End If
If Buffer_Max < 64 Then
Buffer_Size = Buffer_Max
Else
Buffer_Size = 64
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
If Buffer_Max > 64 Then
Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max
Buffer_Size = Buffer_Size * 4
If Buffer_Size > Buffer_Max Then
Buffer_Size = Buffer_Max
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
Loop
End If
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniRead = Default
End If
End If
End If
End If
iniReadError:
End Function
使用 WritePrivateProfileString
为确保使用 GetPrivateProfileString 读取字符串没有问题,请在使用 WritePrivateProfileString 之前将字符串限制为 65,535 个或更少字符。包含相同的验证也是一个好主意。
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Private Declare Function WritePrivateProfileString Lib "kernel32" Alias "WritePrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpString As Any, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String, Optional Buffer_Limit As Long = 65535) As String
On Error Resume Next
If Len(Pathname) <> 0 Then
Key = Trim$(Key)
If InStr(1, Key, ";") <> 1 Then
Section = Trim$(Section)
If Len(Section) > 65535 Then
Section = RTrim$(Left$(Section, 65535))
End If
If InStr(1, Section, "]") = 0 Then
If Len(Key) > 65535 Then
Key = RTrim$(Left$(Key, 65535))
End If
If InStr(1, Key, "=") = 0 Then
Dim Buffer_Max As Long
Err.Clear
Buffer_Max = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Max > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Dim Buffer_Size As Long
Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4
If Buffer_Limit > 65535 Then
Buffer_Limit = 65535
End If
If Buffer_Max > Buffer_Limit Then
Buffer_Max = Buffer_Limit + 1
Else
Buffer_Max = Buffer_Max + 1
End If
If Buffer_Max < 64 Then
Buffer_Size = Buffer_Max
Else
Buffer_Size = 64
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
If Buffer_Max > 64 Then
Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max
Buffer_Size = Buffer_Size * 4
If Buffer_Size > Buffer_Max Then
Buffer_Size = Buffer_Max
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
Loop
End If
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniWrite Pathname, Section, Key, Default
iniRead = Default
End If
End If
End If
End If
iniReadError:
End Function
Public Function iniWrite(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, ByVal Value As String) As Boolean
On Error GoTo iniWriteError
If Len(Pathname) <> 0 Then
Key = Trim$(Key)
If InStr(1, Key, ";") <> 1 Then
Section = Trim$(Section)
If Len(Section) > 65535 Then
Section = RTrim$(Left$(Section, 65535))
End If
If InStr(1, Section, "]") = 0 Then
If Len(Key) > 65535 Then
Key = RTrim$(Left$(Key, 65535))
End If
If InStr(1, Key, "=") = 0 Then
If Len(Value) > 65535 Then Value = Left$(Value, 65535)
iniWrite = WritePrivateProfileString(Section, Key, Value, Pathname) <> 0
End If
End If
End If
End If
iniWriteError:
End Function