Utf8 字串常值
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異是在 語言設計會議(LDM)的相關記錄中擷取的。
您可以在 規格一文中深入瞭解將特徵規範納入 C# 語言標準的過程。
總結
此提案新增了以 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
後綴
語言會在字串常值上提供 u8
後綴,以強制類型為UTF8。
後綴不區分大小寫,U8
後綴將受到支援,且具有與 u8
後綴相同的意義。
使用 u8
後綴時,字面常值的值是 ReadOnlySpan<byte>
,其中包含字串的 UTF-8 位元組表示。
空終止符會置於記憶體中最後一個位元組之外(且超出 ReadOnlySpan<byte>
長度),以處理呼叫需要空終止字元串的一些互操作案例。
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 字節表示串聯:
ReadOnlySpan<byte> operator +(ReadOnlySpan<byte> x, ReadOnlySpan<byte> y);
這個二進位
+
運算符用於串接位元組序列,且僅當兩個操作數都是語意上的 UTF8 位元組表示時才適用。 當操作數是u8
字面常值的值,或是由 UTF8 位元組表示法的串接運算子所產生的值時,從語意上講它是一個 UTF8 位元組表示法。UTF8 位元組表示串連的結果是
ReadOnlySpan<byte>
,其中包含左操作數的位元組,後面接著右操作數的位元組。 空終止符會置於記憶體中最後一個位元組之外(且超出ReadOnlySpan<byte>
長度),以處理呼叫需要空終止字元串的一些互操作案例。
降低
語言會降低UTF8編碼字串,就像開發人員在程式代碼中輸入產生的 byte[]
常值一樣。 例如:
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[] { ... }
格式的所有優化也會同樣適用於 UTF-8 字面值。 這表示呼叫點將無需配置,因為 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.
缺點
依賴核心 API
編譯程式實作將會同時使用 UTF8Encoding
進行無效字串偵測以及翻譯成 byte[]
。 確切的 API 可能取決於編譯程式所使用的目標架構。 但 UTF8Encoding
將是實作中的主力。
在過去,編譯程式已避免使用執行時期 API 進行常值處理。 這是因為它將控制常數如何處理從語言轉移到執行時。 具體來說,這表示 Bug 修正之類的專案可能會變更常數編碼,並表示 C# 編譯的結果取決於編譯程式執行所在的運行時間。
這不是假設的問題。 舊版 Roslyn 使用 double.Parse
來處理浮點常數剖析。 這造成了許多問題。 首先,它表示某些浮點值在原生編譯程式與 Roslyn 之間有不同的表示法。 其次,隨著 .NET Core 在 double.Parse
程式碼中演進並修正長期存在的 Bug,這表示這些常數的意義會隨著編譯器所執行的運行時環境而變更。 因此,編譯程式最終會撰寫自己的浮點剖析程序代碼版本,並移除對 double.Parse
的相依性。
此情境已與運行小組討論,我們並不覺得這個情境有我們之前遇到的相同問題。 UTF8 解析在所有運行時間中是穩定的,在這方面沒有已知問題,也沒有未來相容性方面的擔憂。 如果有的話,我們可以重新評估策略。
替代方案
僅限目標類型
設計只能依賴目標輸入,並移除 u8
常值上的 string
後綴。 在現今大部分情況下,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")
。
不過,這是一項便利專案,此功能即使沒有它也可以運作,並且稍後新增它不會造成任何影響。
等候Utf8String類型
雖然 .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
序列之間的轉換
本節中的轉換尚未實作。 這些轉換仍然是現行的提案。
語言允許 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,類似於「隱含插補字串轉換」或「方法群組轉換」。 它不像它屬於「隱含常數表達式轉換」,因為即使來源是常數表示式,結果絕不是常數表達式。 此外,「隱含常數表達式轉換」會被視為「標準隱含轉換」,這可能會導致涉及使用者定義轉換的非簡單行為變更。
解決方式:
我們將為字串常數引入一種新的轉換類型至 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();
}
提案:
新的轉換不是標準轉換。 這可避免涉及使用者定義轉換的非簡單行為變更。 例如,我們不需要擔心隱含元組字面量轉換或使用者定義的轉換等等。
解決方式:
(已解決)Linq Expression Tree 轉換
是否應在 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。
(已解決)具有 u8
後綴的字串字面值的自然類型
《詳細設計》一節提到:「自然類型將會是 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類似,Better 功能成員(§11.6.4.3)會更新,使得偏好那些不需要將 string
常數轉換為 UTF8 byte
序列的成員。
更好的功能成員
...假設自變數清單
具有一組自變數表達式 和兩個適用的函式成員 ,並使用參數類型 和 , 定義為比 更好的函式成員
- 每個自變數的 ,從
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
是 function_type_conversion,或Mq
是具類型參數的泛型方法{Y1, Y2, ..., Yq}
,且至少一個類型參數Yi
的類型參數是從 function_type推斷的,或- 對於每個自變數,從
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";
}
會靜默地列印「array」而不是「object」。
我們對這種行為感到滿意嗎? 是否應該將它記錄為重大變更? 或許我們可以使新規則更加複雜,以深入探討元組常值的轉換。
解決方式:
原型不會在這裡調整任何規則,希望我們可以觀察到實際操作中的故障 - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#breaking-changes。
(已解決)u8
後綴是否不區分大小寫?
提案:
支援 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
我們在數據表上留下效能的範例
- 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