字符串的默认封送处理

System.StringSystem.Text.StringBuilder 类均具有类似的封送处理行为。

字符串作为 COM 样式 BSTR 类型或以 null 结尾的字符串(以 null 字符结尾的字符数组)进行封送。 字符串中的字符可以转换为 Unicode 编码(Windows 系统上的默认编码)或 ANSI 编码。

接口中使用的字符串

下表显示以非托管代码的方法参数封送字符串数据类型时的封送处理选项。 该 MarshalAsAttribute 属性提供多个 UnmanagedType 枚举值,用于将字符串封送给 COM 接口。

枚举类型 非托管格式说明
UnmanagedType.BStr(默认值) 具有预先固定长度和 Unicode 字符的 COM 样式 BSTR
UnmanagedType.LPStr 指向以 null 结尾的 ANSI 字符数组的指针。
UnmanagedType.LPWStr 指向以 null 结尾的 Unicode 字符数组的指针。

此表适用于 String. 对于 StringBuilder,唯一允许的选项是 UnmanagedType.LPStrUnmanagedType.LPWStr

以下示例显示了在接口中声明的 IStringWorker 字符串。

public interface IStringWorker
{
    void PassString1(string s);
    void PassString2([MarshalAs(UnmanagedType.BStr)] string s);
    void PassString3([MarshalAs(UnmanagedType.LPStr)] string s);
    void PassString4([MarshalAs(UnmanagedType.LPWStr)] string s);
    void PassStringRef1(ref string s);
    void PassStringRef2([MarshalAs(UnmanagedType.BStr)] ref string s);
    void PassStringRef3([MarshalAs(UnmanagedType.LPStr)] ref string s);
    void PassStringRef4([MarshalAs(UnmanagedType.LPWStr)] ref string s);
}
Public Interface IStringWorker
    Sub PassString1(s As String)
    Sub PassString2(<MarshalAs(UnmanagedType.BStr)> s As String)
    Sub PassString3(<MarshalAs(UnmanagedType.LPStr)> s As String)
    Sub PassString4(<MarshalAs(UnmanagedType.LPWStr)> s As String)
    Sub PassStringRef1(ByRef s As String)
    Sub PassStringRef2(<MarshalAs(UnmanagedType.BStr)> ByRef s As String)
    Sub PassStringRef3(<MarshalAs(UnmanagedType.LPStr)> ByRef s As String)
    Sub PassStringRef4(<MarshalAs(UnmanagedType.LPWStr)> ByRef s As String)
End Interface

以下示例显示了类型库中介绍的相应接口。

interface IStringWorker : IDispatch
{
    HRESULT PassString1([in] BSTR s);
    HRESULT PassString2([in] BSTR s);
    HRESULT PassString3([in] LPStr s);
    HRESULT PassString4([in] LPWStr s);
    HRESULT PassStringRef1([in, out] BSTR *s);
    HRESULT PassStringRef2([in, out] BSTR *s);
    HRESULT PassStringRef3([in, out] LPStr *s);
    HRESULT PassStringRef4([in, out] LPWStr *s);
};

平台调用中使用的字符串

当 CharSet 为 Unicode 或字符串参数被显式标记为 [MarshalAs(UnmanagedType.LPWSTR)] 并且字符串通过值(不是 refout)传递时,字符串将直接由本机代码固定并使用。 否则,平台调用会复制字符串参数,将其从.NET Framework 格式 (Unicode) 转换为平台非托管格式。 字符串是不可变的,在调用返回时不会从非托管内存复制回托管内存。

本机代码仅负责在通过引用传递字符串时释放内存,并分配新值。 否则,.NET 运行时拥有内存,并在调用后释放它。

下表列出了作为平台调用的方法参数进行封送时字符串的封送选项。 MarshalAsAttribute 特性向封送字符串提供若干个 UnmanagedType 枚举值。

枚举类型 非托管格式说明
UnmanagedType.AnsiBStr 具有预先固定长度和 ANSI 字符的 COM 样式 BSTR
UnmanagedType.BStr 具有预先固定长度和 Unicode 字符的 COM 样式 BSTR
UnmanagedType.LPStr(默认值) 指向以 null 结尾的 ANSI 字符数组的指针。
UnmanagedType.LPTStr 指向以 null 终止的平台相关字符数组的指针。
UnmanagedType.LPUTF8Str 指向一个以 null 结尾的 UTF-8 编码字符数组的指针。
UnmanagedType.LPWStr 指向以 null 结尾的 Unicode 字符数组的指针。
UnmanagedType.TBStr 具有预先固定长度和平台相关字符的 COM 样式 BSTR
VBByRefStr 一个值,使 Visual Basic 能够更改非托管代码中的字符串,并让结果反映在托管代码中。 此值仅支持用于平台调用。 这是 Visual Basic 中字符串 ByVal 的默认值。

此表适用于 String. 对于 StringBuilder,允许的唯一选项是 LPStrLPTStr以及 LPWStr

以下类型定义显示了使用 MarshalAsAttribute 进行平台调用的正确方法。

class StringLibAPI
{
    [DllImport("StringLib.dll")]
    public static extern void PassLPStr([MarshalAs(UnmanagedType.LPStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassLPWStr([MarshalAs(UnmanagedType.LPWStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassLPTStr([MarshalAs(UnmanagedType.LPTStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassLPUTF8Str([MarshalAs(UnmanagedType.LPUTF8Str)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassBStr([MarshalAs(UnmanagedType.BStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassAnsiBStr([MarshalAs(UnmanagedType.AnsiBStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassTBStr([MarshalAs(UnmanagedType.TBStr)] string s);
}
Class StringLibAPI
    Public Declare Auto Sub PassLPStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.LPStr)> s As String)
    Public Declare Auto Sub PassLPWStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.LPWStr)> s As String)
    Public Declare Auto Sub PassLPTStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.LPTStr)> s As String)
    Public Declare Auto Sub PassLPUTF8Str Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.LPUTF8Str)> s As String)
    Public Declare Auto Sub PassBStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.BStr)> s As String)
    Public Declare Auto Sub PassAnsiBStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.AnsiBStr)> s As String)
    Public Declare Auto Sub PassTBStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.TBStr)> s As String)
End Class

结构中使用的字符串

字符串是结构的有效成员;但是, StringBuilder 缓冲区在结构中无效。 下表显示以字段形式封送 String 数据类型时的封送处理选项。 MarshalAsAttribute 特性向字段的封送字符串提供若干个 UnmanagedType 枚举值。

枚举类型 非托管格式说明
UnmanagedType.BStr 具有预先固定长度和 Unicode 字符的 COM 样式 BSTR
UnmanagedType.LPStr(默认值) 指向以 null 结尾的 ANSI 字符数组的指针。
UnmanagedType.LPTStr 指向以 null 终止的平台相关字符数组的指针。
UnmanagedType.LPUTF8Str 指向一个以 null 结尾的 UTF-8 编码字符数组的指针。
UnmanagedType.LPWStr 指向以 null 结尾的 Unicode 字符数组的指针。
UnmanagedType.ByValTStr 固定长度的字符数组;数组的类型由包含结构的字符集确定。

ByValTStr 类型用于在结构中显示的内联固定长度字符数组。 其他类型适用于包含字符串指针的结构内的字符串引用。

CharSet应用于包含结构的自变量StructLayoutAttribute决定了结构中字符串的字符格式。 以下示例结构包含字符串引用和内联字符串,以及 ANSI、Unicode 和依赖于平台的字符。 类型库中这些结构的表示形式显示在以下C++代码中:

struct StringInfoA
{
    char *  f1;
    char    f2[256];
};

struct StringInfoW
{
    WCHAR * f1;
    WCHAR   f2[256];
    BSTR    f3;
};

struct StringInfoT
{
    TCHAR * f1;
    TCHAR   f2[256];
};

以下示例演示如何使用 MarshalAsAttribute 不同格式定义相同的结构。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct StringInfoA
{
    [MarshalAs(UnmanagedType.LPStr)] public string f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string f2;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct StringInfoW
{
    [MarshalAs(UnmanagedType.LPWStr)] public string f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string f2;
    [MarshalAs(UnmanagedType.BStr)] public string f3;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct StringInfoT
{
    [MarshalAs(UnmanagedType.LPTStr)] public string f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string f2;
}
<StructLayout(LayoutKind.Sequential, CharSet := CharSet.Ansi)> _
Structure StringInfoA
    <MarshalAs(UnmanagedType.LPStr)> Public f1 As String
    <MarshalAs(UnmanagedType.ByValTStr, SizeConst := 256)> _
    Public f2 As String
End Structure

<StructLayout(LayoutKind.Sequential, CharSet := CharSet.Unicode)> _
Structure StringInfoW
    <MarshalAs(UnmanagedType.LPWStr)> Public f1 As String
    <MarshalAs(UnmanagedType.ByValTStr, SizeConst := 256)> _
    Public f2 As String
<MarshalAs(UnmanagedType.BStr)> Public f3 As String
End Structure

<StructLayout(LayoutKind.Sequential, CharSet := CharSet.Auto)> _
Structure StringInfoT
    <MarshalAs(UnmanagedType.LPTStr)> Public f1 As String
    <MarshalAs(UnmanagedType.ByValTStr, SizeConst := 256)> _
    Public f2 As String
End Structure

固定长度字符串缓冲区

在某些情况下,必须将固定长度的字符缓冲区传递到非托管代码中进行操作。 在这种情况下,仅传递字符串不起作用,因为被调用方无法修改传递缓冲区的内容。 即使字符串通过引用传递,也无法将缓冲区初始化为给定大小。

解决方案是根据预期的编码,将byte[]char[]作为参数传递,而不是传递String。 当标记有 [Out] 时,数组可以由被调用方取消引用和修改,前提是它不超过所分配数组的容量。

例如,Windows GetWindowText API 函数(在 winuser.h 中定义)要求调用方传递函数向其写入窗口文本的固定长度字符缓冲区。 参数 lpString 指向大小 nMaxCount为调用方分配的缓冲区。 调用方应分配缓冲区并将参数设置为 nMaxCount 已分配缓冲区的大小。 以下示例演示 GetWindowTextwinuser.h 中定义的函数声明。

int GetWindowText(
    HWND hWnd,        // Handle to window or control.
    LPTStr lpString,  // Text buffer.
    int nMaxCount     // Maximum number of characters to copy.
);

char[] 可以由被调用方取消引用和修改。 下面的代码示例演示如何 ArrayPool<char> 用于预分配 char[]

using System;
using System.Buffers;
using System.Runtime.InteropServices;

internal static class NativeMethods
{
    [DllImport("User32.dll", CharSet = CharSet.Unicode)]
    public static extern void GetWindowText(IntPtr hWnd, [Out] char[] lpString, int nMaxCount);
}

public class Window
{
    internal IntPtr h;        // Internal handle to Window.
    public string GetText()
    {
        char[] buffer = ArrayPool<char>.Shared.Rent(256 + 1);
        NativeMethods.GetWindowText(h, buffer, buffer.Length);
        return new string(buffer);
    }
}
Imports System
Imports System.Buffers
Imports System.Runtime.InteropServices

Friend Class NativeMethods
    Public Declare Auto Sub GetWindowText Lib "User32.dll" _
        (hWnd As IntPtr, <Out> lpString() As Char, nMaxCount As Integer)
End Class

Public Class Window
    Friend h As IntPtr ' Friend handle to Window.
    Public Function GetText() As String
        Dim buffer() As Char = ArrayPool(Of Char).Shared.Rent(256 + 1)
        NativeMethods.GetWindowText(h, buffer, buffer.Length)
        Return New String(buffer)
   End Function
End Class

另一种解决方法是传递 StringBuilder 作为参数而不是 String参数。 被调用方可以取消引用和修改封送 StringBuilder 时创建的缓冲区,前提是它不超过 StringBuilder 的容量。 也可以将其初始化为固定长度。 例如,如果将缓冲区初始化 StringBuilder 为容量 N,封送器将提供大小为 (N+1) 字符的缓冲区。 +1 解释了非托管字符串具有 null 终止符而帐户 StringBuilder 却不具有这一事实。

注释

通常,如果担心性能,不建议传递 StringBuilder 参数。 有关详细信息,请参阅 字符串参数

另请参阅