活动
Utf8 字符串字面量
备注
本文是特性规范。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。
功能规范与已完成的实现之间可能存在一些差异。 那些差异已记录在相关的 语言设计会议(LDM)说明中。
可以在有关规范的文章中了解更多有关将功能规范子块纳入 C# 语言标准的过程。
支持者问题:https://github.com/dotnet/csharplang/issues/184
此建议增加了在 C# 中编写 UTF8 字符串字面量的功能,并将其自动编码为其 UTF-8 byte
表示形式。
UTF8 是 Web 的语言,它在 .NET 堆栈的重要部分中是必需的。 虽然大部分数据以 byte[]
的形式从网络堆栈中出来,但代码中仍然大量使用常量。 例如,网络堆栈通常必须写入像 "HTTP/1.0\r\n"
、" AUTH"
或这样的常量。 "Content-Length: "
。
目前没有有效的语法可以执行此操作,因为 C# 使用 UTF16 编码表示所有字符串。 这意味着开发人员必须在以下两种方式中做出选择:一种是在运行时进行编码,这将产生开销,包括启动时实际执行编码操作所花费的时间(如果针对的类型实际上不需要编码,还需要分配时间);另一种是手动转换字节并将其存储在 byte[]
中。
// Efficient but verbose and error prone
static ReadOnlySpan<byte> AuthWithTrailingSpace => new byte[] { 0x41, 0x55, 0x54, 0x48, 0x20 };
WriteBytes(AuthWithTrailingSpace);
// Incurs allocation and startup costs performing an encoding that could have been done at compile-time
static readonly byte[] s_authWithTrailingSpace = Encoding.UTF8.GetBytes("AUTH ");
WriteBytes(s_authWithTrailingSpace);
// Simplest / most convenient but terribly inefficient
WriteBytes(Encoding.UTF8.GetBytes("AUTH "));
对于我们在运行时、ASP.NET 和 Azure 方面的合作伙伴而言,这种权衡是一个经常出现的痛点。 很多时候,由于他们不愿意手工编写 byte[]
编码,因此会导致性能降低。
若要解决此问题,我们将允许该语言中的 UTF8 字面值,并在编译时将其编码为 UTF8 byte[]
。
该语言将在字符串字面量上提供 u8
后缀,以强制类型为 UTF8。
后缀不区分大小写,U8
后缀将受支持,其含义与 u8
后缀相同。
在使用 u8
后缀时,字面量的为 ReadOnlySpan<byte>
,其中包含字符串的 UTF-8 字节表示形式。
null 终止符放置在内存中最后一个字节之后(ReadOnlySpan<byte>
的长度之外),以便处理调用需要 null 终止字符串的一些互操作方案。
string s1 = "hello"u8; // Error
var s2 = "hello"u8; // Okay and type is ReadOnlySpan<byte>
ReadOnlySpan<byte> s3 = "hello"u8; // Okay.
byte[] s4 = "hello"u8; // Error - Cannot implicitly convert type 'System.ReadOnlySpan<byte>' to 'byte[]'.
byte[] s5 = "hello"u8.ToArray(); // Okay.
Span<byte> s6 = "hello"u8; // Error - Cannot implicitly convert type 'System.ReadOnlySpan<byte>' to 'System.Span<byte>'.
由于字面量将作为全局常量分配,因此生成的 ReadOnlySpan<byte>
的生存期不会阻止它被返回或传递到其他地方。 但在某些情况下,特别是在异步函数中,不允许 ref 结构类型的局部变量,因此在这些情况下会存在使用限制,需要进行 ToArray()
调用或类似的调用。
u8
字面量没有常量值。 这是因为 ReadOnlySpan<byte>
现在不可能是常量的类型。 如果将来将 const
的定义扩展到考虑 ReadOnlySpan<byte>
,那么这个值也应该被视为一个常量。 实际上,这意味着 u8
字面量不能用作可选参数的默认值。
// Error: The argument is not constant
void Write(ReadOnlySpan<byte> message = "missing"u8) { ... }
当输入的字面量文本为格式不正确的 UTF16 字符串时,语言将出现错误:
var bytes = "hello \uD8\uD8"u8; // Error: malformed UTF16 input string
var bytes2 = "hello \uD801\uD802"u8; // Allowed: invalid UTF16 values, but it's correctly formed.
§12.10.5 添加运算符中将添加一个新的项目符号,如下所示。
UTF8 字节表示形式串联:
C#ReadOnlySpan<byte> operator +(ReadOnlySpan<byte> x, ReadOnlySpan<byte> y);
此二进制
+
运算符执行字节序列串联,并且只有当两个操作数在语义上都是 UTF8 字节表示形式时才适用。 如果操作数是u8
字面量的值,或者是由 UTF8 字节表示形式串联运算符产生的值,那么该操作数在语义上就是 UTF8 字节表示形式。UTF8 字节表示形式串联的结果是一个
ReadOnlySpan<byte>
,由左操作数的字节和右操作数的字节组成。 null 终止符放置在内存中最后一个字节之后(ReadOnlySpan<byte>
的长度之外),以便处理调用需要 null 终止字符串的一些互操作方案。
该语言将完全按照开发人员在代码中键入 byte[]
字面量的方式来降低 UTF8 编码字符串。 例如:
ReadOnlySpan<byte> span = "hello"u8;
// Equivalent to
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00 }).
Slice(0,5); // The `Slice` call will be optimized away by the compiler.
这意味着应用于 new byte[] { ... }
窗体的所有优化也将适用于 utf8 字面量。 这意味着调用站点将无需分配内存,因为 C# 会对其进行优化,将其存储在 PE 文件的 .data
部分。
多个 UTF8 字节表示形式串联运算符的连续应用被合并为 ReadOnlySpan<byte>
的单个创建,其字节数组包含最终字节序列。
ReadOnlySpan<byte> span = "h"u8 + "el"u8 + "lo"u8;
// Equivalent to
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00 }).
Slice(0,5); // The `Slice` call will be optimized away by the compiler.
编译器实现将使用 UTF8Encoding
来检测无效字符串,以及转换为 byte[]
。 确切的 API 可能取决于编译器正在使用的目标框架。 但 UTF8Encoding
将是实现的主力部分。
从历史上看,编译器避免使用运行时 API 进行字面值处理。 这是因为它把常量的处理从语言转移到了运行时。 具体来说,这意味着像 bug 修复这样的项目可以更改常量编码,并意味着 C# 编译的结果取决于编译器在哪个运行时执行。
这不是一个假设的问题。 早期版本的 Roslyn 使用 double.Parse
来处理浮点常量分析。 这导致了一系列问题。 首先,这意味着某些浮点值在本机编译器和 Roslyn 之间具有不同的表示形式。 其次,随着 .NET core 在 double.Parse
代码中演变并修复了长期存在的 bug,这意味着这些常量在语言中的含义会根据编译器执行的运行时而更改。 因此,编译器最终编写了自己的浮点分析代码版本,并删除了对 double.Parse
的依赖项。
我们已与运行团队讨论过这一方案,我们认为它不存在以前遇到过的问题。 UTF8 解析在不同的运行时都很稳定,在这方面没有已知的问题,不会引起未来的兼容性问题。 如果真的出现了,我们可以重新评估策略。
这种设计可以只依赖目标键入,并移除 string
字面量的 u8
后缀。 在目前的大多数情况下,string
字面量会直接分配给 ReadOnlySpan<byte>
,因此没有必要。
ReadOnlySpan<byte> span = "Hello World;"
u8
后缀主要用于支持两种场景:var
和重载解析。 对于后者,请考虑以下用例:
void Write(ReadOnlySpan<byte> span) { ... }
void Write(string s) {
var bytes = Encoding.UTF8.GetBytes(s);
Write(bytes.AsSpan());
}
考虑到实现情况,最好调用 Write(ReadOnlySpan<byte>)
,而 u8
后缀则方便了调用:Write("hello"u8)
。 如果缺乏这一点,开发人员就需要使用麻烦的类型转换 Write((ReadOnlySpan<byte>)"hello")
。
尽管如此,这仍然是一个方便项目,没有它功能也可以存在,以后再添加也不会造成中断。
虽然目前 .NET 生态系统正在将 ReadOnlySpan<byte>
作为事实上的 Utf8 字符串类型进行标准化,但运行时有可能在未来引入实际的 Utf8String
类型。
面对这种可能的变化,我们应该在这里评估我们的设计,并反思我们是否后悔我们做出的决定。 不过,这应该与我们引入 Utf8String
的现实可能性相权衡,我们发现 ReadOnlySpan<byte>
作为可接受的替代方案的可能性似乎每天都在降低。
我们似乎不太可能对字符串字面量和 ReadOnlySpan<byte>
之间的目标类型转换感到遗憾。 现在,使用 ReadOnlySpan<byte>
以 utf8 的形式嵌入我们的 API,因此即使 Utf8String
成为“更好的”类型,转换也仍然具有价值。 语言可以简单地优先选择 Utf8String
而不是 ReadOnlySpan<byte>
。
我们似乎更有可能对 u8
后缀指向 ReadOnlySpan<byte>
而不是 Utf8String
感到遗憾。 这类似于我们对 stackalloc int[]
的自然类型是 int*
而不是 Span<int>
感到遗憾。 然而,这并非什么大问题,只是带来了不便。
本部分中的转换尚未实现。 这些转换仍然是积极的提案。
该语言允许在 string
常量和 byte
序列之间进行转换,其中文本转换为等效的 UTF8 字节表示形式。 具体而言,编译器将允许 string_constant_to_UTF8_byte_representation_conversion - 从 string
常量到 byte[]
、Span<byte>
和 ReadOnlySpan<byte>
的隐式转换。
隐式转换 §10.2 部分将添加一个新的项目符号。 此转换不是标准转换 §10.4。
byte[] array = "hello"; // new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f }
Span<byte> span = "dog"; // new byte[] { 0x64, 0x6f, 0x67 }
ReadOnlySpan<byte> span = "cat"; // new byte[] { 0x63, 0x61, 0x74 }
当转换的输入文本为格式不正确的 UTF16 字符串时,语言将出现错误:
const string text = "hello \uD801\uD802";
byte[] bytes = text; // Error: the input string is not valid UTF16
该功能主要用于字面量,但也可用于任何 string
常量值。
还将支持从具有 string
值的 null
常量进行转换。 转换结果将是目标类型的 default
值。
const string data = "dog"
ReadOnlySpan<byte> span = data; // new byte[] { 0x64, 0x6f, 0x67 }
在对字符串(如 +
)进行常量操作时,编码为 UTF8 将在最后的 string
中进行,而不是先对各个部分进行编码,然后再将结果连接起来。 考虑此排序很重要,因为它会影响转换是否成功。
const string first = "\uD83D"; // high surrogate
const string second = "\uDE00"; // low surrogate
ReadOnlySpan<byte> span = first + second;
这里的两个部分本身是无效的,因为它们是代理对的不完整部分。 单个字符无法正确转换为 UTF8,但组合在一起,它们形成了一个完整的代理对,可以成功转换为 UTF8。
Linq 表达式树中不允许使用 string_constant_to_UTF8_byte_representation_conversion。
虽然这些转换的输入是常量,而且数据在编译时已完全编码,但语言却不认为转换是常量。 这是因为数组今天不是常量。 如果将来扩展 const
的定义以考虑数组,则还应考虑这些转换。 实际上,在实际操作中,这意味着这些转换的结果不能用作可选参数的默认值。
// Error: The argument is not constant
void Write(ReadOnlySpan<byte> message = "missing") { ... }
一旦实现了字符串字面量,就会遇到与语言中其他字面量相同的问题:它们代表的类型取决于使用方式。 C# 提供了一个字面量后缀,用于消除其他字面量的含义。 例如,开发人员可以编写 3.14f
来强制值为 float
,或者编写 1l
来强制该值为 long
。
第一个三个设计问题涉及字符串到 Span<byte>
/ ReadOnlySpan<byte>
的转换。它们尚未被实现。
(已解决)具有 string
值的 null
常量与 byte
序列之间的转换
未指定是否支持此转换;如果支持,则未指定如何执行。
提案:
允许从具有 string
值的 null
常量隐式转换为 byte[]
、Span<byte>
和 ReadOnlySpan<byte>
。 转换的结果是目标类型的 default
值。
解决方法:
(已解决)string_constant_to_UTF8_byte_representation_conversion 属于哪里?
string_constant_to_UTF8_byte_representation_conversion 是隐式转换 §10.2 部分中的一个独立项、§10.2.11 的一部分,还是属于其他现有的隐式转换组?
提案:
它是隐式转换中的一个新要点,类似于“隐式内插字符串转换”或“方法组转换”,§10.2。 感觉它不属于“隐式常量表达式转换”,因为即使源是常量表达式,结果也永远不是常量表达式。 此外,“隐式常量表达式转换”被视为“标准隐式转换”§10.4.2,这可能会导致涉及用户定义的转换的非简单行为更改。
解决方法:
我们将为字符串常量引入一种新的转换类型,将其转换为 UTF-8 字节 - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#conversion-kinds
(已解决)string_constant_to_UTF8_byte_representation_conversion 是标准转换吗?
除了“纯粹的”标准转换(标准转换是那些可以作为用户定义转换的一部分出现的预定义转换),编译器还将一些预定义转换视为“某种程度上的”标准转换。 例如,如果在代码中显式地将字符串转换为目标类型,那么隐式内插字符串转换就会作为用户定义转换的一部分出现。 就好像它是一个标准显式转换,尽管它实际上是一个隐式转换,并未明确包含在标准隐式或显式转换的集合中。 例如:
class C
{
static void Main()
{
C1 x = $"hello"; // error CS0266: Cannot implicitly convert type 'string' to 'C1'. An explicit conversion exists (are you missing a cast?)
var y = (C1)$"dog"; // works
}
}
class C1
{
public static implicit operator C1(System.FormattableString x) => new C1();
}
提案:
新转换不是标准转换。 这将避免涉及用户定义转换的非关键行为变化。 例如,我们无需担心隐式元组字面量转换下的用户定义 cinversions 等问题。
解决方法:
暂时不是标准转换 - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#implicit-standard-conversion。
是否应允许在 Linq 表达式树转换中进行 string_constant_to_UTF8_byte_representation_conversion? 我们可以暂时禁止它,或者直接将“降低”形式纳入树中。 例如:
Expression<Func<byte[]>> x = () => "hello"; // () => new [] {104, 101, 108, 108, 111}
Expression<FuncSpanOfByte> y = () => "dog"; // () => new Span`1(new [] {100, 111, 103})
Expression<FuncReadOnlySpanOfByte> z = () => "cat"; // () => new ReadOnlySpan`1(new [] {99, 97, 116})
带有 u8
后缀的字符串字面量如何处理? 我们可以将这些数据作为字节数组进行创建:
Expression<Func<byte[]>> x = () => "hello"u8; // () => new [] {104, 101, 108, 108, 111}
解决方法:
在 Linq 表达式树中禁止 - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#expression-tree-representation。
“详细设计”部分中提到:“自然类型将是 ReadOnlySpan<byte>
。”与此同时:“使用 u8
后缀时,文本仍可转换为任何允许的类型:byte[]
、Span<byte>
或 ReadOnlySpan<byte>
。”
此方法有几个缺点:
-
ReadOnlySpan<byte>
不适用于桌面框架; - 不存在从
ReadOnlySpan<byte>
到byte[]
或Span<byte>
的现有转换。 为了支持它们,我们可能需要将字面量视为目标类型。 语言规则和实现将变得更加复杂。
提案:
自然类型将是 byte[]
。 它在所有框架上都随时可用。 顺便说一句,在运行时,我们将始终从创建字节数组开始,即使原始建议也是如此。 我们不需要任何特殊的转换规则来支持到 Span<byte>
和 ReadOnlySpan<byte>
的转换。 已经存在从 byte[]
到 Span<byte>
和 ReadOnlySpan<byte>
的隐式用户定义转换。 甚至还有用户自定义的 ReadOnlyMemory<byte>
的隐式转换(请参阅下面的“转换深度”问题)。 存在一个缺点,编程语言不允许串联用户定义的转换。 因此,以下代码无法编译:
using System;
class C
{
static void Main()
{
var y = (C2)"dog"u8; // error CS0030: Cannot convert type 'byte[]' to 'C2'
var z = (C3)"cat"u8; // error CS0030: Cannot convert type 'byte[]' to 'C3'
}
}
class C2
{
public static implicit operator C2(Span<byte> x) => new C2();
}
class C3
{
public static explicit operator C3(ReadOnlySpan<byte> x) => new C3();
}
但是,与任何用户定义的转换一样,显式转换也可用于使一个用户定义的转换成为另一个用户定义的转换的一部分。
感觉所有激励场景都将以 byte[]
作为自然类型来处理,而语言规则和实现将大大简化。
解决方法:
提案获得批准 - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#natural-type-of-u8-literals。
我们可能希望对 u8
字符串字面量是否应该具有可变数组的类型进行更深入的辩论,但我们认为现在没有必要进行辩论。
仅实现了显式转换运算符。
它还能在字节[]可以使用的任何地方使用吗? 如下所示:
static readonly ReadOnlyMemory<byte> s_data1 = "Data"u8;
static readonly ReadOnlyMemory<byte> s_data2 = "Data";
由于 u8
的自然类型,第一个示例应该可行。
第二个示例很难实现,因为它需要双向转换。 除非我们将 ReadOnlyMemory<byte>
添加为允许的转换类型之一。
提案:
不要做任何特别的事。
解决方法:
暂不添加新的转换目标 https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#conversion-depth。 两种转换都无法编译。
以下 API 将变得模棱两可:
M("");
static void M1(ReadOnlySpan<char> charArray) => ...;
static void M1(byte[] byteArray) => ...;
我们应该做些什么来解决这一问题?
提案:
与 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/lambda-improvements.md#overload-resolution 类似,更新了更好的函数成员 (§11.6.4.3),以便优先选择不需要将 string
常量转换为 UTF8 byte
序列的成员。
更好的函数成员
... 给定一个参数列表
A
,其中包含一组参数表达式{E1, E2, ..., En}
,以及两个适用的函数成员Mp
和Mq
(参数类型分别为{P1, P2, ..., Pn}
和{Q1, Q2, ..., Qn}
),如果符合以下条件,则Mp
会被定义为比Mq
- 对于每个参数,从
Ex
到Px
的隐式转换不是 string_constant_to_UTF8_byte_representation_conversion,并且对于至少一个参数,从Ex
到Qx
的隐式转换是 string_constant_to_UTF8_byte_representation_conversion 或 。- 对于每个参数,从
Ex
到Px
的隐式转换不是 function_type_conversion,并且
Mp
是一个非泛型方法,或者Mp
是一个带有类型参数{X1, X2, ..., Xp}
的泛型方法,并且对于每种类型参数Xi
而言,类型参数是从表达式或 function_type 以外的类型中推断出来的,并且- 对于至少一个参数,从
Ex
到Qx
的隐式转换是 类别转换函数,或者Mq
是具有类型参数{Y1, Y2, ..., Yq}
的泛型方法,且对于至少一个类型参数Yi
,类型参数从 函数类型被推断出,或者- 对于每个参数,从
Ex
到Qx
的隐式转换并不优于从Ex
到Px
的隐式转换,对于至少一个参数,从Ex
到Px
的转换优于从Ex
到Qx
的转换。
请注意,添加此规则并不包括实例方法变得适用和“覆盖”扩展方法的情况。 例如:
using System;
class Program
{
static void Main()
{
var p = new Program();
Console.WriteLine(p.M(""));
}
public string M(byte[] b) => "byte[]";
}
static class E
{
public static string M(this object o, string s) => "string";
}
此代码的行为将无提示地从打印"string"更改为打印"byte[]"。
我们是否同意此行为更改? 是否应将其记录为重大更改?
请注意,没有建议在针对 C#10 语言版本时使 string_constant_to_UTF8_byte_representation_conversion 不可用。 在这种情况下,上面的示例就会变成错误,而不是返回 C#10 的行为。 这遵循了目标语言版本不影响语言语义的一般原则。
我们是否同意此行为? 是否应将其记录为重大更改?
新规则也不会阻止涉及元组字面量转换的中断。 例如,
class C
{
static void Main()
{
System.Console.Write(Test(("s", 1)));
}
static string Test((object, int) a) => "object";
static string Test((byte[], int) a) => "array";
}
会静默打印“数组”而不是“对象”。
我们是否同意此行为? 是否应将其记录为重大更改? 也许我们可以将新规则复杂化,以便深入研究元组字面量转换。
解决方法:
原型不会在这里调整任何规则,因此我们希望能看到在实践中会出现什么问题 - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#breaking-changes。
提案:
同时支持 U8
后缀,以便与数字后缀保持一致。
解决方法:
当前运行时手动编码 UTF8 字节的示例
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/StatusCodes.cs#L13-L78
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Memory/src/System/Buffers/Text/Base64Encoder.cs#L581-L591
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Net.HttpListener/src/System/Net/Windows/HttpResponseStream.Windows.cs#L284
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs#L30
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs#L852
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs#L35-L42
在表上留下 perf 的示例
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Net.Security/src/System/Net/Security/Pal.Managed/SafeChannelBindingHandle.cs#L16-L17
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs#L37-L43
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs#L78
- https://github.com/dotnet/runtime/blob/e095fde94baa480a6d65dfdee43d9cc0ad0d0b38/src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpCommands.cs#L669-L687
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-04-18.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-06-06.md