字符串指南

大多数 Mozilla 代码使用 C++ 类层次结构来传递字符串数据,而不是使用原始指针。本指南介绍了 Mozilla 代码库(链接到 libxul 的代码)中可见的字符串类。

简介

字符串类是一个 C++ 类库,用于管理宽(16 位)和窄(8 位)字符字符串的缓冲区。头文件和实现位于 xpcom/string 目录中。所有字符串都存储为单个连续的字符缓冲区。

8 位和 16 位字符串类具有完全独立的基类,但共享相同的 API。因此,您不能在没有某种转换辅助类或例程的情况下将 8 位字符串赋值给 16 位字符串。出于本文档的目的,我们将在类文档中引用 16 位字符串类。每个 16 位类都有一个等效的 8 位类

nsAString

nsACString

nsString

nsCString

nsAutoString

nsAutoCString

nsDependentString

nsDependentCString

字符串类在其类型层次结构中区分必须在其缓冲区末尾具有空终止符 (ns[C]String) 的字符串和不需要具有空终止符 (nsA[C]String) 的字符串。nsA[C]String 是字符串类的基类(因为它施加的限制较少),而 ns[C]String 是从它派生的类。获取字符串作为参数的函数通常应采用这四种类型之一。

为了避免不必要的字符串数据复制(这可能会有很大的性能成本),字符串类支持不同的所有权模型。所有字符串类都动态支持以下三种所有权模型

  • 引用计数、写时复制、缓冲区(默认)

  • 采用缓冲区(字符串类拥有的缓冲区,但未进行引用计数,因为它来自其他地方)

  • 依赖缓冲区,即字符串类不拥有的底层缓冲区,但构造字符串的调用方保证该缓冲区将在字符串实例的生命周期内存在

自动字符串将优先于其堆栈缓冲区引用计数现有的引用计数缓冲区,但在其他情况下,将使用其堆栈缓冲区来存储任何适合其中的内容。

还有许多其他字符串类

  • 主要用作其他类型的构造函数的类,特别是 nsDependent[C]StringnsDependent[C]Substring。这些类型实际上只是使用非默认所有权模式构造 nsA[C]String 的便捷表示法;不应将它们视为不同的类型。

  • nsLiteral[C]String 很少显式构造,但通常通过 ""_nsu""_ns 用户定义的字符串文字来构造。 nsLiteral[C]String 可以轻松地构造和销毁,因此在静态存储(而不是其他字符串类)中存储时不会发出构造/销毁代码。

主要的字符串类

下面的列表描述了主要的基类。熟悉它们后,请参阅描述何时使用哪个类的附录。

  • nsAString/nsACString:所有字符串的抽象基类。它提供了一个用于赋值、单个字符访问、基本字符操作和字符串比较的 API。此类对应于 XPIDL AStringACString 参数类型。 nsA[C]String 不一定是空终止的。

  • nsString/nsCString:通过保证空终止存储在 nsA[C]String 上构建。这允许使用一种方法 (.get()) 来访问底层字符缓冲区。

其余的字符串类从 nsA[C]Stringns[C]String 继承。因此,每个字符串类都与 nsA[C]String 兼容。

注意

在针对字符串宽度的泛型代码中,nsA[C]String 有时被称为 nsTSubstring<CharT>nsAStringnsTSubstring<char16_t> 的类型别名,而 nsACStringnsTSubstring<char> 的类型别名。

注意

类型 nsLiteral[C]String 从技术上讲不继承自 nsA[C]String,而是继承自 nsStringRepr<CharT>。这使得该类型在存储在静态存储中时不会生成析构函数。

它可以隐式强制转换为 const ns[C]String&(尽管永远无法以可变方式访问)并且在大多数情况下通常表现得好像它是 ns[C]String 的子类。

由于每个字符串都派生自 nsAString(或 nsACString),因此它们都共享一个简单的 API。常见的只读方法包括

  • .Length() - 字符串中的代码单元数(对于 8 位字符串类为字节,对于 16 位字符串类为 char16_t)。

  • .IsEmpty() - 确定字符串是否有任何值的最快方法。使用此方法代替测试 string.Length() == 0

  • .Equals(string) - 如果给定字符串的值与当前字符串相同,则为 true。与 operator== 几乎相同。

修改字符串的常用方法

  • .Assign(string) - 为字符串分配新值。与 operator= 几乎相同。

  • .Append(string) - 将值附加到字符串。

  • .Insert(string, position) - 在位置处的代码单元之前插入给定字符串。

  • .Truncate(length) - 将字符串缩短到给定长度。

更完整的文档可以在 类参考 中找到。

作为函数参数

通常,使用 nsA[C]String 引用在模块之间传递字符串。例如

// when passing a string to a method, use const nsAString&
nsFoo::PrintString(const nsAString& str);

// when getting a string from a method, use nsAString&
nsFoo::GetString(nsAString& result);

具体类 - 何时使用哪些类

具体类用于实际需要存储字符串数据的代码中。具体类的最常见用途是作为局部变量以及类或结构体中的成员。

digraph concreteclasses {
node [shape=rectangle]

"nsA[C]String" -> "ns[C]String";
"ns[C]String" -> "nsDependent[C]String";
"nsA[C]String" -> "nsDependent[C]Substring";
"nsA[C]String" -> "ns[C]SubstringTuple";
"ns[C]String" -> "nsAuto[C]StringN";
"ns[C]String" -> "nsLiteral[C]String" [style=dashed];
"nsAuto[C]StringN" -> "nsPromiseFlat[C]String";
"nsAuto[C]StringN" -> "nsPrintfCString";
"nsAuto[C]StringN" -> "nsFmtCString";
}

以下是最常见的具体类的列表。熟悉它们后,请参阅描述何时使用哪个类的附录。

  • ns[C]String - 一个空终止字符串,其缓冲区在堆上分配。在字符串对象消失时销毁其缓冲区。

  • nsAuto[C]String - 从 nsString 派生,一个拥有与字符串本身相同的存储空间中 64 个代码单元缓冲区的字符串。如果将小于 64 个代码单元的字符串分配给 nsAutoString,则不会分配额外的存储空间。对于较大的字符串,会在堆上分配一个新的缓冲区。

    如果想要其他数字而不是 64,请使用模板类型 nsAutoStringN / nsAutoCStringN。(nsAutoStringnsAutoCString 分别只是 nsAutoStringN<64>nsAutoCStringN<64> 的类型定义。)

  • nsDependent[C]String - 从 nsString 派生,此字符串不拥有其缓冲区。它可用于将原始字符串指针 (const char16_t*const char*) 转换为 nsAString 类型的类。请注意,您必须将 nsDependentString 使用的缓冲区空终止。如果您不想或不能空终止缓冲区,请使用 nsDependentSubstring

  • nsPrintfCString - 从 nsCString 派生,此字符串的行为类似于 nsAutoCString。构造函数采用参数,允许它从 printf 样式格式字符串和参数列表构造 8 位字符串。

  • nsFmtCString - 从 nsCString 派生,此字符串的行为类似于 nsAutoCString。构造函数采用参数,允许它从 {fmt} 样式格式字符串和参数列表构造 8 位字符串。

还有一些作为辅助例程等的副作用创建的具体类。您应该避免直接使用这些类。让字符串库为您创建类。

  • ns[C]SubstringTuple - 通过字符串连接创建

  • nsDependent[C]Substring - 通过 Substring() 创建

  • nsPromiseFlat[C]String - 通过 PromiseFlatString() 创建

  • nsLiteral[C]String - 通过 ""_nsu""_ns 用户定义的文字创建

当然,有时需要在代码中引用这些字符串类,但通常应避免。

迭代器

因为 Mozilla 字符串始终是单个缓冲区,所以对字符串中字符的迭代是使用原始指针完成的

/**
 * Find whether there is a tab character in `data`
 */
bool HasTab(const nsAString& data) {
  const char16_t* cur = data.BeginReading();
  const char16_t* end = data.EndReading();

  for (; cur < end; ++cur) {
    if (char16_t('\t') == *cur) {
      return true;
    }
  }
  return false;
}

请注意,end 指向字符串缓冲区末尾后的字符。它永远不应该被取消引用。

写入可变字符串也很简单

/**
* Replace every tab character in `data` with a space.
*/
void ReplaceTabs(nsAString& data) {
  char16_t* cur = data.BeginWriting();
  char16_t* end = data.EndWriting();

  for (; cur < end; ++cur) {
    if (char16_t('\t') == *cur) {
      *cur = char16_t(' ');
    }
  }
}

您可以通过 SetLength() 更改字符串的长度。请注意,在更改字符串长度后,迭代器将失效。如果在写入字符串缓冲区时缓冲区变小,请使用 SetLength 通知字符串类新的大小

/**
 * Remove every tab character from `data`
 */
void RemoveTabs(nsAString& data) {
  int len = data.Length();
  char16_t* cur = data.BeginWriting();
  char16_t* end = data.EndWriting();

  while (cur < end) {
    if (char16_t('\t') == *cur) {
      len -= 1;
      end -= 1;
      if (cur < end)
        memmove(cur, cur + 1, (end - cur) * sizeof(char16_t));
    } else {
      cur += 1;
    }
  }

  data.SetLength(len);
}

请注意,使用BeginWriting()来延长字符串是不允许的。 BeginWriting()不能用于写入超过由EndWriting()Length()指示的字符串逻辑长度的内容。在BeginWriting()之前调用SetCapacity()不会影响上一句话所述内容。要延长字符串,请在BeginWriting()之前调用SetLength()或使用下面描述的BulkWrite() API。

批量写入

BulkWrite()允许对字符串的缓冲区进行容量感知、缓存友好的低级写入。

容量感知意味着调用者会意识到调用者请求的缓冲区容量是如何向上舍入到 mozjemalloc 桶的。当最初请求最佳缓冲区大小但尚不知道真正需要的尺寸时,这很有用。如果实际需要写入的数据大于最佳估计值,但仍在舍入后的容量范围内,则无需重新分配,即使请求了最佳容量。

缓存友好意味着在写入字符串的新内容后,会写入用于 C 兼容性的零终止符,因此结果是仅向前进行的线性写入访问模式,而不是使用SetLength()后跟BeginWriting()导致的非线性来回序列。

低级意味着可以通过原始指针进行写入,就像使用BeginWriting()一样。

BulkWrite()接受三个参数:新容量(可能向上舍入)、要保留的字符串开头处的代码单元数量(通常是旧的逻辑长度)以及一个布尔值,指示如果请求的容量适合于比当前容量小的缓冲区,是否可以重新分配较小的缓冲区。它返回一个mozilla::Result,其中包含一个可用的mozilla::BulkWriteHandle<T>(其中T是字符串的char_type)或一个nsresult,解释了为什么无法获得任何可用的句柄(可能是内存不足)。

实际写入是通过返回的mozilla::BulkWriteHandle<T>执行的。在成功的情况下,在调用句柄上的Finish()之前,您不得通过此句柄访问字符串;或者,在失败的情况下,让句柄超出范围而不调用Finish(),在这种情况下,句柄的析构函数会将字符串置于一个大部分无害但一致的状态(如果请求的容量大于 0,则包含单个替换字符;或者,在char情况下,如果替换字符的三字节 UTF-8 表示形式不适合,则为 ASCII 替换字符)。

mozilla::BulkWriteHandle<T>自动转换为可写的mozilla::Span<T>,并且还提供显式访问自身作为SpanAsSpan())或通过与Span上的一致命名的组件访问器:Elements()Length()。(后者不是字符串的逻辑长度,而是缓冲区的可写长度。)通过这些方法公开的缓冲区包括您可能请求保留的前缀。您需要跳过它,以免覆盖它。

如果在准备好调用Finish()之前需要请求不同的容量,可以在句柄上调用RestartBulkWrite()。它接受三个与BulkWrite()的前三个参数匹配的参数。它返回mozilla::Result<mozilla::Ok, nsresult>以指示成功或内存不足。调用RestartBulkWrite()会使先前获得的跨度、原始指针或长度失效。

完成写入后,调用Finish()。它接受两个参数:字符串的新逻辑长度(不得超过句柄的Length()方法返回的容量)以及一个布尔值,指示如果较小的 mozjemalloc 桶可以容纳新的逻辑长度,是否可以尝试重新分配较小的缓冲区。

辅助类和函数

转换 NSString 字符串

使用mozilla::CopyNSStringToXPCOMString()(位于mozilla/MacStringHelpers.h中)将 NSString 字符串转换为 XPCOM 字符串。

搜索字符串 - 查找子字符串、字符等

nsReadableUtils.h头文件提供了用于在可运行对象中搜索的辅助方法。

bool FindInReadable(const nsAString& pattern,
                    nsAString::const_iterator start, nsAString::const_iterator end,
                    nsStringComparator& aComparator = nsDefaultStringComparator());

要使用此方法,startend应分别指向要搜索的字符串的开头和结尾。如果找到搜索字符串,则startend将被调整为指向找到的模式的开头和结尾。返回值为truefalse,指示是否找到字符串。

一个例子

const nsAString& str = GetSomeString();
nsAString::const_iterator start, end;

str.BeginReading(start);
str.EndReading(end);

constexpr auto valuePrefix = u"value="_ns;

if (FindInReadable(valuePrefix, start, end)) {
    // end now points to the character after the pattern
    valueStart = end;
}

检查内存分配失败

与 Gecko 中的其他类型一样,字符串类默认使用永不失败的内存分配,因此在分配/调整“普通”字符串大小时,您无需检查是否成功。

大多数修改字符串的函数(Assign()SetLength()等)也有一个接受mozilla::fallible_t参数的重载版本。如果分配失败,这些重载版本将返回false而不是中止。在创建/分配可能非常大的字符串时使用它们,并且如果分配失败,程序可以从中恢复。

子字符串(字符串片段)

引用现有字符串的子字符串非常简单,而无需实际分配新的空间并将字符复制到该子字符串中。Substring()是创建此类字符串引用的首选方法。

void ProcessString(const nsAString& str) {
    const nsAString& firstFive = Substring(str, 0, 5); // from index 0, length 5
    // firstFive is now a string representing the first 5 characters
}

Unicode 转换

字符串可以存储在两种基本格式中:8 位代码单元(字节/char)字符串或 16 位代码单元(char16_t)字符串。类名中带有大写“C”的任何字符串类都包含 8 位字节。这些类包括nsCStringnsDependentCString等。任何不带“C”的字符串类都包含 16 位代码单元。

8 位字符串可以采用多种字符编码,而 16 位字符串始终采用可能无效的 UTF-16。(可以通过将 16 位字符串传递给EnsureUTF16Validity()来使其成为保证有效的 UTF-16。)最常见的编码是

  • ASCII - 用于基本仅英语字符串的 7 位编码。每个 ASCII 值都存储在数组中的一个字节中,最高有效第 8 位设置为零。

  • UCS2 - Unicode 的一个子集BMP的 16 位编码。存储在 UCS2 中的字符的 Unicode 值存储在字符串类中的一个 16 位char16_t中。

  • UTF-8 - Unicode 字符的 8 位编码。每个 Unicode 字符都存储在字符串类中的最多 4 个字节中。UTF-8 能够表示整个 Unicode 字符集,并且它有效地映射到UTF-32。(Gtk 和 Rust 本身使用 UTF-8。)

  • UTF-16 - 用于 Unicode 存储的 16 位编码,与 UCS2 向后兼容。存储在 UTF-16 中的字符的 Unicode 值可能需要字符串类中的一个或两个 16 位char16_tnsAString的内容始终必须被视为采用此编码而不是 UCS2。UTF-16 能够表示整个 Unicode 字符集,并且它有效地映射到 UTF-32。(Win32 W API 和 Mac OS X 本身使用 UTF-16。)

  • Latin1 - 前 256 个 Unicode 代码点的 8 位编码。用于 HTTP 标头以及在文本节点和 SpiderMonkey 字符串中进行尺寸优化的存储。Latin1 通过将每个字节零扩展为 16 位代码单元来转换为 UTF-16。请注意,这种“Latin1”不能用于编码 HTML、CSS、JS 等。指定charset=latin1charset=windows-1252含义相同。Windows-1252 是一种类似但不同的编码,用于交换。

此外,还存在多种其他(旧版)编码。与 Web 相关的编码在编码标准中定义。从这些编码到 UTF-8 和 UTF-16 的转换由mozilla::Encoding提供。此外,在 Windows 上,有一些罕见的情况(例如拖放),其中需要使用以 Windows 区域设置相关的旧版编码而不是 UTF-16 编码的数据来调用系统 API。在这些罕见情况下,请使用 kernel32.dll 中的MultiByteToWideChar/WideCharToMultiByte。请勿在 *nix 上使用iconv。我们仅在 Windows 之外的 *nix 上支持 UTF-8 编码的文件路径,非路径 Gtk 字符串始终为 UTF-8,Cocoa 和 Java 字符串始终为 UTF-16。

处理现有代码时,务必检查正在操作的字符串的当前用法,以确定正确的转换机制。

编写新代码时,可能难以确定哪种存储类和编码最合适。这个问题没有唯一的答案,但要点是

  • **令人惊讶的是,许多字符串通常只是 ASCII。**ASCII 是 UTF-8 的一个子集,因此可以有效地表示为 UTF-8。将 ASCII 表示为 UTF-16 对内存使用和缓存局部性都不利。

  • **Rust 非常喜欢 UTF-8。**如果您的 C++ 代码与 Rust 代码交互,则在nsACString中使用 UTF-8,并在转换为 Rust 字符串时仅对其进行验证,比在 C++ 端使用nsAString更有效。

  • **网络代码更喜欢 8 位字符串。**网络代码倾向于使用 8 位字符串:使用 UTF-8 或 Latin1(字节值是 Unicode 标量值)语义。

  • **JS 和 DOM 更喜欢 UTF-16。**大多数 Gecko 代码使用 UTF-16 以与 JS 字符串和 DOM 字符串兼容,这些字符串可能是无效的 UTF-16。但是,DOM 文本节点和 JS 字符串都存储仅包含低于 U+0100 的代码点的字符串作为 Latin1(字节值是 Unicode 标量值)。

  • **Windows 和 Cocoa 使用 UTF-16。**Windows 系统 API 使用 UTF-16。Cocoa NSString 是 UTF-16。

  • **Gtk 使用 UTF-8。**Gtk API 使用 UTF-8 表示非文件路径。在 Gecko 的情况下,我们仅支持 Windows 之外的 UTF-8 编码的文件路径,因此出于我们的目的,所有 Gtk 字符串都是 UTF-8,尽管从 Gtk 收到的文件路径可能不是有效的 UTF-8。

为了帮助进行 ASCII、Latin1、UTF-8 和 UTF-16 转换,提供了一些辅助方法和类。其中一些类看起来像函数,因为它们最常在堆栈上用作临时对象。

短的以零结尾的 ASCII 字符串

如果您有一个短的以零结尾的字符串,并且确定它始终是 ASCII,请使用这些特殊情况方法,而不是后面部分中描述的转换。

  • 如果将 ASCII 字面量分配给nsACString,请使用AssignLiteral()

  • 如果将字面量分配给nsAString,请使用AssignLiteral()并将字面量设为u""字面量。如果字面量必须是""字面量(而不是u"")并且是 ASCII,则仍然使用AppendLiteral(),但请注意,这会涉及运行时膨胀。

  • 如果要分配一个以零结尾的 ASCII 字符串,该字符串在调用点处的编译器看来不是字面量,并且您也不知道字符串的长度(例如,因为它是从长度不同的字面量数组中查找的),请使用AssignASCII()

UTF-8/UTF-16 转换

NS_ConvertUTF8toUTF16(const nsACString&)

一个 nsAutoString 子类,用于将 UTF-8 编码的 nsACStringconst char* 转换为 16 位 UTF-16 字符串。如果您需要一个 const char16_t* 缓冲区,可以使用 .get() 方法。例如

/* signature: void HandleUnicodeString(const nsAString& str); */
object->HandleUnicodeString(NS_ConvertUTF8toUTF16(utf8String));

/* signature: void HandleUnicodeBuffer(const char16_t* str); */
object->HandleUnicodeBuffer(NS_ConvertUTF8toUTF16(utf8String).get());
NS_ConvertUTF16toUTF8(const nsAString&)

一个 nsAutoCString,用于将 16 位 UTF-16 字符串(nsAString)转换为 UTF-8 编码的字符串。与上面一样,您可以使用 .get() 访问 const char* 缓冲区。

/* signature: void HandleUTF8String(const nsACString& str); */
object->HandleUTF8String(NS_ConvertUTF16toUTF8(utf16String));

/* signature: void HandleUTF8Buffer(const char* str); */
object->HandleUTF8Buffer(NS_ConvertUTF16toUTF8(utf16String).get());
CopyUTF8toUTF16(const nsACString&, nsAString&)

转换并复制

// return a UTF-16 value
void Foo::GetUnicodeValue(nsAString& result) {
  CopyUTF8toUTF16(mLocalUTF8Value, result);
}
AppendUTF8toUTF16(const nsACString&, nsAString&)

转换并追加

// return a UTF-16 value
void Foo::GetUnicodeValue(nsAString& result) {
  result.AssignLiteral("prefix:");
  AppendUTF8toUTF16(mLocalUTF8Value, result);
}
CopyUTF16toUTF8(const nsAString&, nsACString&)

转换并复制

// return a UTF-8 value
void Foo::GetUTF8Value(nsACString& result) {
  CopyUTF16toUTF8(mLocalUTF16Value, result);
}
AppendUTF16toUTF8(const nsAString&, nsACString&)

转换并追加

// return a UTF-8 value
void Foo::GetUnicodeValue(nsACString& result) {
  result.AssignLiteral("prefix:");
  AppendUTF16toUTF8(mLocalUTF16Value, result);
}

Latin1 / UTF-16 转换

以下内容仅在您可以保证原始字符串为 ASCII 或 Latin1(指字节值与 Unicode 标量值相同;而不是 Windows-1252 意义上的)时使用。这些辅助函数与上述 UTF-8 / UTF-16 转换辅助函数非常相似。

UTF-16 到 Latin1 转换器

这些转换器**非常危险**,因为它们在转换过程中会**丢失信息**。除非您的字符串保证为 Latin1 或 ASCII,否则应**避免使用 UTF-16 到 Latin1 的转换**。(将来,这些转换可能会在调试版本中开始断言其输入在允许的范围内。)如果输入实际上在 Latin1 范围内,则每个 16 位代码单元通过删除高半部分缩小为 8 位字节。高于 U+00FF 的 Unicode 代码点会导致垃圾,其性质不得依赖。(将来,垃圾的性质将取决于 CPU 架构。)如果您想 printf() 某些内容并且不关心非 ASCII 字符会发生什么,请改为转换为 UTF-8。

NS_LossyConvertUTF16toASCII(const nsAString&)

一个 nsAutoCString,其中包含一个临时缓冲区,该缓冲区包含字符串的 Latin1 值。

void LossyCopyUTF16toASCII(Span<const char16_t>, nsACString&)

对 UTF-16 到 Latin1 字符串对象进行就地转换。

void LossyAppendUTF16toASCII(Span<const char16_t>, nsACString&)

将 UTF-16 字符串追加到 Latin1 字符串。

Latin1 到 UTF-16 转换器

这些转换器非常危险,因为它们会**对非 ASCII UTF-8 或 Windows-1252 输入产生错误的结果**,将其转换为毫无意义的 UTF-16 字符串。除非您的字符串保证为 ASCII 或 Latin1(指字节值与 Unicode 标量值相同),否则应**避免使用 ASCII 到 UTF-16 的转换**。每个字节都扩展为 16 位代码单元。

在大多数 HTTP 头值上使用这些转换是正确的,但**在 HTTP 响应正文上使用这些转换始终是错误的!**(使用 mozilla::Encoding 处理响应正文。)

NS_ConvertASCIItoUTF16(const nsACString&)

一个 nsAutoString,其中包含一个临时缓冲区,该缓冲区包含 Latin1 到 UTF-16 转换的值。

void CopyASCIItoUTF16(Span<const char>, nsAString&)

对 Latin1 到 UTF-16 进行就地转换。

void AppendASCIItoUTF16(Span<const char>, nsAString&)

将 Latin1 字符串追加到 UTF-16 字符串。

比较 ns*Strings 与 C 字符串

您可以通过将 ns*Strings 转换为 C 字符串或直接与 C 字符串进行比较来比较 ns*Strings 与 C 字符串。

bool nsAString::EqualsASCII(const char*)

与 ASCII C 字符串进行比较。

bool nsAString::EqualsLiteral(...)

与字符串字面量进行比较。

常见模式

字符串字面量

字符串字面量是在某些 C++ 代码中编写的原始字符串值。例如,在语句 printf("Hello World\n"); 中,值 "Hello World\n" 是一个字符串字面量。当需要 nsAStringnsACString 时,通常需要插入字符串字面量值。提供了两个用户定义的字面量,它们隐式转换为 const nsString&const nsCString&

  • ""_ns 用于 8 位字面量,隐式转换为 const nsCString&

  • u""_ns 用于 16 位字面量,隐式转换为 const nsString&

鉴于 nsDependentCString 也会将字符串值包装在 nsCString 中,用户定义的字面量的优势可能不清楚。用户定义的字面量的优势有两个方面。

  • 这些字符串的长度在编译时计算,因此不需要在运行时扫描字符串以确定其长度。

  • 字符串字面量在二进制文件的整个生命周期内存在,并且可以在 ns[C]String 类之间移动,而无需复制或释放。

以下是一些正确使用字面量(标准和用户定义)的示例

// call Init(const nsLiteralString&) - enforces that it's only called with literals
Init(u"start value"_ns);

// call Init(const nsAString&)
Init(u"start value"_ns);

// call Init(const nsACString&)
Init("start value"_ns);

如果字面量通过宏定义,则可以使用其构造函数将其转换为 nsLiteralStringnsLiteralCString。您可以考虑根本不使用宏,而是使用命名的 constexpr 常量。

在某些情况下,8 位字面量通过宏定义(在代码中或来自环境),但它无法更改或既用作 8 位字符串也用作 16 位字符串。在这些情况下,您可以使用 NS_LITERAL_STRING_FROM_CSTRING 宏构造 nsLiteralString 并在编译时进行转换。

字符串连接

可以使用 + 运算符将字符串连接在一起。生成的字符串是一个 const nsSubstringTuple 对象。生成的可以类似于 nsAString 对象进行处理和引用。连接**不会复制子字符串**。只有当连接分配到另一个字符串对象时,才会复制字符串。 nsSubstringTuple 对象保存指向原始字符串的指针。因此, nsSubstringTuple 对象依赖于其所有子字符串,这意味着其生命周期必须至少与 nsSubstringTuple 对象一样长。

例如,您可以使用两个字符串的值,并将它们的连接传递给另一个函数,该函数接受 const nsAString&

void HandleTwoStrings(const nsAString& one, const nsAString& two) {
  // call HandleString(const nsAString&)
  HandleString(one + two);
}

注意:在这种情况下,两个字符串隐式地组合成一个临时 nsString,并将临时字符串传递给 HandleString。如果 HandleString 将其输入分配到另一个 nsString,则在这种情况下字符串缓冲区将被共享,从而抵消中间临时变量的成本。您可以连接 N 个字符串并将结果存储在临时变量中

constexpr auto start = u"start "_ns;
constexpr auto middle = u"middle "_ns;
constexpr auto end = u"end"_ns;
// create a string with 3 dependent fragments - no copying involved!
nsString combinedString = start + middle + end;

// call void HandleString(const nsAString&);
HandleString(combinedString);

连接用户定义的字面量是安全的,因为临时 nsLiteral[C]String 对象将与临时连接对象(类型为 nsSubstringTuple)的生命周期一样长。

// call HandlePage(const nsAString&);
// safe because the concatenated-string will live as long as its substrings
HandlePage(u"start "_ns + u"end"_ns);

局部变量

函数内部的局部变量通常存储在栈上。 nsAutoString/nsAutoCString 类是 nsString/nsCString 类的子类。它们拥有一个 64 字符的缓冲区,该缓冲区分配在与字符串本身相同的存储空间中。如果 nsAutoString 在栈上分配,那么它可以使用一个 64 字符的栈缓冲区。这使得实现能够在处理小字符串时避免分配额外的内存。 nsAutoStringN/nsAutoCStringN 是更通用的替代方案,允许您选择内联缓冲区中的字符数。

...
nsAutoString value;
GetValue(value); // if the result is less than 64 code units,
                // then this just saved us an allocation
...

成员变量

通常,您应该对成员变量使用具体类 nsStringnsCString

class Foo {
  ...
  // these store UTF-8 and UTF-16 values respectively
  nsCString mLocalName;
  nsString mTitle;
};

一种常见的错误模式是将 nsAutoString/nsAutoCString 用于成员变量。如局部变量中所述,这些类具有内置缓冲区,这使得它们非常大。这意味着如果您将它们包含在类中,它们会使类膨胀 64 字节(nsAutoCString)或 128 字节(nsAutoString)。

原始字符指针

PromiseFlatString()PromiseFlatCString() 可用于创建临时缓冲区,该缓冲区包含与源字符串相同值的以 null 结尾的缓冲区。 PromiseFlatString() 将在必要时创建临时缓冲区。这最常用于将 nsAString 传递给需要以 null 结尾的字符串的 API。

在以下示例中,nsAString 与字面量字符串组合,并将结果传递给需要简单字符缓冲区的 API。

// Modify the URL and pass to AddPage(const char16_t* url)
void AddModifiedPage(const nsAString& url) {
  constexpr auto httpPrefix = u"http://"_ns;
  const nsAString& modifiedURL = httpPrefix + url;

  // creates a temporary buffer
  AddPage(PromiseFlatString(modifiedURL).get());
}

PromiseFlatString() 在处理已以 null 结尾的字符串时非常智能。在这种情况下,它避免创建临时缓冲区。

// Modify the URL and pass to AddPage(const char16_t* url)
void AddModifiedPage(const nsAString& url, PRBool addPrefix) {
    if (addPrefix) {
        // MUST create a temporary buffer - string is multi-fragmented
        constexpr auto httpPrefix = u"http://"_ns;
        AddPage(PromiseFlatString(httpPrefix + modifiedURL));
    } else {
        // MIGHT create a temporary buffer, does a runtime check
        AddPage(PromiseFlatString(url).get());
    }
}

注意

由于 COW 优化,无法有效地将字符串类的内部缓冲区的拥有权转移到其他组件可以安全释放的已拥有 char* 中。

如果使用需要使用 malloc 分配的 char* 缓冲区的旧版 API,请优先使用 ToNewUnicodeToNewCStringToNewUTF8String 而不是 strdup 来创建已拥有的 char* 指针。

printf 和 UTF-16 字符串

为了进行调试,将 UTF-16 字符串(nsStringnsAutoString 等)printf 很有用。为此通常需要将其转换为 8 位字符串,因为这是 printf 所期望的。使用

printf("%s\n", NS_ConvertUTF16toUTF8(yourString).get());

不重新分配的追加序列

SetCapacity() 允许您向字符串提供一个关于追加序列(不包括在 UTF-16 和 UTF-8 之间进行转换的追加)导致的未来字符串长度的提示,以便在追加序列期间避免多次分配。但是,XPCOM 字符串的其他避免分配功能与 SetCapacity() 存在不良交互,使其成为一种危险的操作。

SetCapacity() 适用于在以下列表中的多个操作序列之前使用(在 SetCapacity() 调用和列表中的操作之间没有列表外的操作)

  • Append()

  • AppendASCII()

  • AppendLiteral()

  • AppendPrintf()

  • AppendFmt()

  • AppendInt()

  • AppendFloat()

  • LossyAppendUTF16toASCII()

  • AppendASCIItoUTF16()

**不要**在字符串上的后续操作不满足上述条件时调用 SetCapacity()。撤消 SetCapacity() 好处的一些操作包括但不限于

  • SetLength()

  • Truncate()

  • Assign()

  • AssignLiteral()

  • Adopt()

  • CopyASCIItoUTF16()

  • LossyCopyUTF16toASCII()

  • AppendUTF16toUTF8()

  • AppendUTF8toUTF16()

  • CopyUTF16toUTF8()

  • CopyUTF8toUTF16()

如果您的字符串是 nsAuto[C]String,并且您使用常量 N 调用 SetCapacity(),请改为将字符串声明为 nsAuto[C]StringN<N+1> 而不调用 SetCapacity()(同时注意不要使用过大的 N 导致运行时栈溢出)。

无需包含 null 终止符的空间:这是字符串类的工作。

注意:调用 SetCapacity() 并不允许您使用从 BeginWriting() 获得的指针写入超过字符串当前长度(由 Length() 返回)的部分。请改用 BulkWrite()SetLength()

XPIDL

字符串库也可以通过 IDL 使用。通过使用特殊定义的 IDL 类型声明属性和方法,字符串类用作相应方法的参数。

XPIDL 字符串类型

C++ 签名遵循上面描述的抽象类型约定,这样所有方法参数都基于抽象类。下表描述了 IDL 中每种字符串类型的用途。

XPIDL 类型

C++ 类型

用途

string

char*

指向 ASCII(7 位)字符串的原始字符指针,未使用字符串类。

在 XPConnect 边界之间不保证高位。

wstring

char16_t*

指向 UTF-16 字符串的原始字符指针,未使用字符串类。

AString

nsAString

UTF-16 字符串。

ACString

nsACString

8 位字符串。所有位在 XPConnect 边界之间都保留。

AUTF8String

nsACString

UTF-8 字符串。

在 XPConnect 边界之间使用值时,根据需要转换为 UTF-16。

在几乎所有情况下,调用者都应优先使用字符串类 AStringACStringAUTF8String 而不是原始指针类型 stringwstring

C++ 签名

在 XPIDL 中,in 参数是只读的,并且 *String 参数的 C++ 签名遵循上述指南,对这些参数使用 const nsAString&outinout 参数简单地定义为 nsAString&,以便被调用方可以写入它们。

interface nsIFoo : nsISupports {
    attribute AString utf16String;
    AUTF8String getValue(in ACString key);
};
class nsIFoo : public nsISupports {
  NS_IMETHOD GetUtf16String(nsAString& aResult) = 0;
  NS_IMETHOD SetUtf16String(const nsAString& aValue) = 0;
  NS_IMETHOD GetValue(const nsACString& aKey, nsACString& aResult) = 0;
};

在上面的示例中,utf16String 被视为 UTF-16 字符串。 GetUtf16String() 的实现将使用 aResult.Assign 来“返回”该值。在 SetUtf16String() 中,可以通过多种方法使用字符串的值,包括迭代器PromiseFlatString 和分配给其他字符串。

GetValue() 中,第一个参数 aKey 被视为 8 位值的原始序列。 aKey 中的任何非 ASCII 字符在跨越 XPConnect 边界时都会保留。 GetValue() 的实现将 UTF-8 编码的 8 位字符串分配到 aResult 中。如果跨 XPConnect 边界(例如从脚本)调用此方法,则结果将从 UTF-8 解码为 UTF-16 并用作 Unicode 值。

字符串指南

在您的代码中遵循这些简单的规则,以保持您的开发人员、审阅者和用户满意。

  • 使用尽可能抽象的字符串类。通常是:* 函数参数使用 nsAString * 成员变量使用 nsString * 局部(基于栈的)变量使用 nsAutoString

  • 使用 ""_nsu""_ns 用户定义的文字来表示字面量字符串(例如 "foo"_ns)作为与 nsAString 兼容的对象。

  • 组合字符串时使用字符串连接(即“+”运算符)。

  • 当您需要将原始字符指针转换为与 nsAString 兼容的字符串时,使用 nsDependentString

  • 使用 Substring() 提取现有字符串的片段。

  • 使用迭代器解析和提取字符串片段。

类参考

template<T>
class nsTSubstring<T>

注意

nsTSubstring<char_type> 类通常写成 nsAStringnsACString

size_type Length() const
bool IsEmpty() const
bool IsVoid() const
const char_type *BeginReading() const
const char_type *EndReading() const
bool Equals(const self_type&, comparator_type) const
char_type First() const
char_type Last() const
size_type CountChar(char_type) const
int32_t FindChar(char_type, index_type aOffset = 0) const
void Assign(const self_type&)
void Append(const self_type&)
void Insert(const self_type&, index_type aPos)
void Cut(index_type aCutStart, size_type aCutLength)
void Replace(index_type aCutStart, size_type aCutLength, const self_type &aStr)
void Truncate(size_type aLength)
void SetIsVoid(bool)

将其设为空。XPConnect 和 WebIDL 会将空 nsAStrings 转换为 JavaScript null

char_type *BeginWriting()
char_type *EndWriting()
void SetCapacity(size_type)

在连续调用 Append() 或进行在 UTF-16 和 Latin1 之间相互转换的追加操作之前,通知字符串所需的缓冲区大小。(如果使用在 UTF-16 和 UTF-8 之间相互转换的追加操作,则不要使用此方法。)调用此方法并不允许您使用 BeginWriting() 将写入内容超过字符串的逻辑长度。请根据需要使用 SetLength()BulkWrite()

void SetLength(size_type)
Result<BulkWriteHandle<char_type>, nsresult> BulkWrite(size_type aCapacity, size_type aPrefixToPreserve, bool aAllowShrinking)