字串的預設封送處理

System.StringSystem.Text.StringBuilder 類別都有類似的編組行為。

字串會封送為 COM 樣式 BSTR 類型或以 NULL 結束的字串(以 NULL 字元結尾的字元陣列)。 字串中的字元可以編組為 Unicode(Windows 系統的預設)或 ANSI。

介面中使用的字串

下表顯示當字串資料類型作為方法參數封送到非受控代碼時的封送選項。 MarshalAsAttribute 屬性提供多個 UnmanagedType 列舉值,用於將字串封送至 COM 介面。

列舉型別 Unmanaged 格式的描述
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)] 且字串會以實際值(not refout) 傳遞時,字串會直接釘選並由原生程式碼直接使用。 否則,平台呼叫會複製字串參數,從 .NET 框架格式(Unicode)轉換成平台非管理格式。 字串是不可變的,而且當呼叫傳回時,不會從 Unmanaged 記憶體複製回 Managed 記憶體。

原生程式代碼只負責在字串以傳址方式傳遞時釋放記憶體,並指派新的值。 否則,.NET 執行時會擁有記憶體,並在呼叫後釋放它。

下表列出當字串作為平台調用方法的引數進行封送處理時的封送選項。 屬性 MarshalAsAttribute 提供數個 UnmanagedType 列舉值來封送處理字串。

列舉型別 Unmanaged 格式的描述
UnmanagedType.AnsiBStr 具有前置長度和 ANSI 字元的 COM 樣式 BSTR
UnmanagedType.BStr 具有前置長度和 Unicode 字元的 COM 樣式 BSTR
UnmanagedType.LPStr (預設值) 指向以 Null 結尾的 ANSI 字元陣列的指標。
UnmanagedType.LPTStr 指向依平台而定的字符之以 Null 結尾的陣列的指標。
UnmanagedType.LPUTF8Str 指向使用 UTF-8 編碼字元且以 Null 終止的陣列的指標。
UnmanagedType.LPWStr 以 null 終止的 Unicode 字元陣列的指標。
UnmanagedType.TBStr 具有前置長度和平臺相依字元的 COM 標準樣式 BSTR
VBByRefStr 一個讓 Visual Basic 能在非管理程式碼中更改字串,並將結果反映在受管程式碼中的值。 只有平台調用才支援此值。 這是 Visual Basic 中 ByVal 字串的預設值。

此表格適用於 String。 針對 StringBuilder,唯一允許的選項是 LPStrLPTStrLPWStr

下列類型定義顯示平台調用呼叫的正確用法 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 列舉值,以將字串封送至欄位。

列舉型別 Unmanaged 格式的描述
UnmanagedType.BStr 具有前置長度和 Unicode 字元的 COM 樣式 BSTR
UnmanagedType.LPStr (預設值) 指向以 Null 結尾的 ANSI 字元陣列的指標。
UnmanagedType.LPTStr 指向依平台而定的字符之以 Null 結尾的陣列的指標。
UnmanagedType.LPUTF8Str 指向使用 UTF-8 編碼字元且以 Null 終止的陣列的指標。
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。 緩衝區是在 a StringBuilder 被編組時建立的,只要不超過 StringBuilder,調用者可以將其取消參照並進行修改。 它也可以初始化為固定長度。 例如,如果您將 StringBuilder 緩衝區初始化為容量 N,封送程式會提供大小為 (N+1) 字元的緩衝區。 +1 則解釋了未管理字串有空終止符,而 StringBuilder 則沒有。

謹慎

當績效重要時,請避免 StringBuilder 參數。 編組 a StringBuilder總是 會產生原生緩衝區副本。 一個典型的呼叫用來從原生程式碼中取得字串,可能會產生四次配置。

  1. 一個受控 StringBuilder 緩衝區。
  2. 編組時分配的原生緩衝區。
  3. [Out],則原生緩衝區內容會被複製到新分配的受管理陣列中。
  4. ToString() 分配的 string

重複在多次調用中使用同一 StringBuilder,僅節省一次配置。 使用從 ArrayPool<char> 租用的字元緩衝區更有效率——它將後續呼叫簡化為僅涉及對 ToString() 的配置。

此外,StringBuilder容量包含隱藏的空字元終止符號,而互通處理總是會考慮到這一點。 這是常見的錯誤,因為大多數 API 都希望緩衝區大小包含空字元。 這可能導致浪費或不必要的配置,並阻礙執行時優化 StringBuilder 編組以減少複製次數。

欲了解更多資訊,請參閱 String 參數CA1838:避免 StringBuilder 用於 P/Invoke 的參數

另請參閱