通过


字符串的默认封送处理

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能够更改非托管代码中的字符串,并让结果反映在托管代码中。 此值仅支持用于平台调用。 这是 ByVal 字符串Visual Basic中的默认值。

此表适用于 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<T>来租用一个char[],这样可以避免重复的堆分配。 下面的代码示例演示了此模式。

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

internal static class NativeMethods
{
    [DllImport("User32.dll", CharSet = CharSet.Unicode)]
    public static extern int 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);
        try
        {
            int length = NativeMethods.GetWindowText(h, buffer, buffer.Length);
            return new string(buffer, 0, length);
        }
        finally
        {
            ArrayPool<char>.Shared.Return(buffer);
        }
    }
}
Imports System
Imports System.Buffers
Imports System.Runtime.InteropServices

Friend Class NativeMethods
    Public Declare Auto Function GetWindowText Lib "User32.dll" _
        (hWnd As IntPtr, <Out> lpString() As Char, nMaxCount As Integer) 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)
        Try
            Dim length As Integer = NativeMethods.GetWindowText(h, buffer, buffer.Length)
            Return New String(buffer, 0, length)
        Finally
            ArrayPool(Of Char).Shared.Return(buffer)
        End Try
    End Function
End Class

你还可以考虑传递StringBuilder而不是String。 当StringBuilder被封送时创建的缓冲区可由被调用方接触和修改,只要不超出StringBuilder的容量。 也可以将其初始化为固定长度。 例如,如果将缓冲区初始化 StringBuilder 为容量 N,封送器将提供大小为 (N+1) 字符的缓冲区。 +1 表示非托管字符串具有空终止符,而 StringBuilder 没有。

注意

在性能重要时避免 StringBuilder 参数。 StringBuilder封送都会创建一个本机缓冲区的副本。 从本机代码中获取字符串的典型调用可能会导致四个分配:

  1. 托管 StringBuilder 缓冲区。
  2. 在封送期间分配的本机缓冲区。
  3. 如果 [Out],本机缓冲区内容将复制到新分配的托管数组中。
  4. stringToString()分配。

在多次调用中重用同一个StringBuilder仅节省一个分配。 使用从中租用的 ArrayPool<char> 字符缓冲区效率要高得多, 它减少了对只分配的 ToString()后续调用。

此外,StringBuilder容量不包括一个隐藏的空终止符,而互操作始终考虑到这一点。 这是一个常见的错误,因为大多数 API 需要缓冲区的大小包括空值。 这可能会导致浪费或不必要的分配,并且会阻止运行时优化 StringBuilder 封送以尽量减少副本。

有关详细信息,请参阅 字符串参数CA1838: 避免用于 P/Invoke 的StringBuilder 参数

另请参阅