给.Net程序员的PInvoke Tips [2]: Are Strings Immutable?
Posted on 2006-06-22 10:56 smalldust 阅读(825) 评论(0) 编辑 收藏 举报
早在Java到来之际,程序员们都已逐渐接受并乐于接受String的这一特性:immutable。
从C/C++转到.Net/C#的程序员们,在最初可能非常不适应把char[]和string分开对待,但是一旦习惯了就会觉得非常方便,尤其是直接以+进行连接,以及支持switch...case等。
这个时候问题来了,string真的是immutable的吗?
cbrumme的blog上给出了一个例子:
该程序的执行结果也许正在你的预料之中,输出的是类似
MYCOMPUTERNAMElways immutable
之类的字符串,也就是说原字符串的前面一部分被计算机名覆盖掉了。
对上面的程序,我们可以做出如下分析:
1,computerName和otherString 的文本相同,因此由于编译器的Interning的结果,二者其实指向同一个字符串,用Object.ReferenceEquals()可以验证其相等。
2,红色部分标出的Marshal指令,使得该string被marshal为一个unmanaged pointer(LPWSTR)传递给了GetComputerName函数;
3,GetComputerName函数直接改写了computerName指向的缓冲区,string的immutable特性即被破坏。
由此我们可以看到,在与Unamanaged代码进行交互操作时必须额外小心,因为从某种意义上来说Unmanaged代码权限更大,破坏力也就更大,也就更容易引起意想不到的问题。
因此,上面那段使用GetComputerName的代码中,对该函数的包装要如何改进呢?
首先,在使用一个API之前应该注意其各个参数的in, out性质,例如关于GetComputerName,MSDN上有如下一段:
BOOL GetComputerName( LPTSTR lpBuffer, LPDWORD lpnSize );
很显然,lpBuffer应该是用来输出的缓冲区,因此不应该用string,而是用byte[],StringBuilder之类的类型与之对应;
即便一定要用String,也绝对不能Marshal为LPWSTR/LPTSTR,而是Marshal为VBByRefStr,以确保Managed代码侧string的immutable性质。
※此外,使用unsafe代码也可以打破String的immutable,由于不在本文范围之内,就不进行说明了。
从C/C++转到.Net/C#的程序员们,在最初可能非常不适应把char[]和string分开对待,但是一旦习惯了就会觉得非常方便,尤其是直接以+进行连接,以及支持switch...case等。
这个时候问题来了,string真的是immutable的吗?
cbrumme的blog上给出了一个例子:
using System;
using System.Runtime.InteropServices;
public class Class1
{
static void Main(string[] args)
{
String computerName = "strings are always immutable";
String otherString = "strings are always immutable";
int len = computerName.Length;
GetComputerName(computerName, ref len);
Console.WriteLine(otherString);
}
[DllImport("kernel32", CharSet=CharSet.Unicode)]
static extern bool GetComputerName(
[MarshalAs (UnmanagedType.LPWStr)] string name,
ref int len);
}
using System.Runtime.InteropServices;
public class Class1
{
static void Main(string[] args)
{
String computerName = "strings are always immutable";
String otherString = "strings are always immutable";
int len = computerName.Length;
GetComputerName(computerName, ref len);
Console.WriteLine(otherString);
}
[DllImport("kernel32", CharSet=CharSet.Unicode)]
static extern bool GetComputerName(
[MarshalAs (UnmanagedType.LPWStr)] string name,
ref int len);
}
该程序的执行结果也许正在你的预料之中,输出的是类似
MYCOMPUTERNAMElways immutable
之类的字符串,也就是说原字符串的前面一部分被计算机名覆盖掉了。
对上面的程序,我们可以做出如下分析:
1,computerName和otherString 的文本相同,因此由于编译器的Interning的结果,二者其实指向同一个字符串,用Object.ReferenceEquals()可以验证其相等。
2,红色部分标出的Marshal指令,使得该string被marshal为一个unmanaged pointer(LPWSTR)传递给了GetComputerName函数;
3,GetComputerName函数直接改写了computerName指向的缓冲区,string的immutable特性即被破坏。
由此我们可以看到,在与Unamanaged代码进行交互操作时必须额外小心,因为从某种意义上来说Unmanaged代码权限更大,破坏力也就更大,也就更容易引起意想不到的问题。
因此,上面那段使用GetComputerName的代码中,对该函数的包装要如何改进呢?
首先,在使用一个API之前应该注意其各个参数的in, out性质,例如关于GetComputerName,MSDN上有如下一段:
BOOL GetComputerName( LPTSTR lpBuffer, LPDWORD lpnSize );
Parameters
- lpBuffer
- [out] Pointer to a buffer that receives a null-terminated string containing the computer name or the cluster virtual server name. The buffer size should be large enough to contain MAX_COMPUTERNAME_LENGTH + 1 characters.
很显然,lpBuffer应该是用来输出的缓冲区,因此不应该用string,而是用byte[],StringBuilder之类的类型与之对应;
即便一定要用String,也绝对不能Marshal为LPWSTR/LPTSTR,而是Marshal为VBByRefStr,以确保Managed代码侧string的immutable性质。
※此外,使用unsafe代码也可以打破String的immutable,由于不在本文范围之内,就不进行说明了。