Поделиться через


Литералы строк UTF-8

Заметка

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих собраниях по проектированию языка (LDM).

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Сводка

Это предложение добавляет возможность писать строковые литералы UTF8 в C# и автоматически кодировать их в представление UTF-8 byte.

Мотивация

UTF8 — это язык веб-сайта, и его использование необходимо в значительной части стека .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 на языке и кодировать их в byte[] UTF8 во время компиляции.

Подробный дизайн

u8 суффикс в строковых литералах

Язык использует суффикс 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> не будет препятствовать его возврату или передаче куда-либо ещё. Однако некоторые контексты, в частности, в асинхронных функциях, не разрешают локальные переменные структурных ссылок, поэтому в этих ситуациях может понадобиться вызов 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. Операнд является семантическим представлением байтов UTF8, если это либо значение литерала u8, либо значение, созданное оператором объединения представлений байтов UTF8.

    Результатом объединения байтов UTF-8 является ReadOnlySpan<byte>, состоящий из байтов левого операнда, за которыми следуют байты правого операнда. Терминатор null помещается за пределы последнего байта в памяти (и за пределы длины ReadOnlySpan<byte>) для обработки некоторых сценариев взаимодействия, в которых вызов ожидает строки, завершающиеся null.

Снижение

Язык будет уменьшать строки в кодировке 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[] { ... }, также применяются к литералам utf8. Это означает, что место вызова не будет требовать выделения памяти, поскольку C# оптимизирует его для хранения в разделе .data файла PE.

Несколько последовательных применений операторов объединения байтов 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 среды выполнения для обработки литерала. Это связано с тем, что управление тем, как константы обрабатываются, переходит от языка к среде выполнения. Именно это означает, что такие элементы, как исправления ошибок, могут изменить кодировку констант и означать, что результат компиляции C# зависит от среды выполнения компилятора.

Это не гипотетическая проблема. Ранние версии Roslyn использовали double.Parse для обработки синтаксического анализа констант с плавающей запятой. Это вызвало ряд проблем. Сначала это означало, что некоторые значения с плавающей запятой имеют разные представления между собственным компилятором и Roslyn. Во-вторых, так как .NET core развивался и исправил длительные ошибки в коде double.Parse, это означало, что значение этих констант изменилось на языке в зависимости от среды выполнения компилятора, выполняемого в нем. В результате компилятор в конце концов написал собственную версию кода синтаксического анализа с плавающей запятой и удалив зависимость от 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.

Решение :

Предложение утверждено - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#conversions-from-null-literals.

(Решено) К какому объекту принадлежит 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();
}

предложение:

Новое преобразование не является стандартным преобразованием. Это позволит избежать нетривиальных изменений поведения, связанных с пользовательскими преобразованиями. Например, нам не нужно беспокоиться о преобразованиях, заданных пользователем, при неявных преобразованиях литералов кортежей и т. д.

Решение :

Не стандартное преобразование на текущий момент - 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 в контексте преобразования дерева выражений Linq? Мы могли бы запретить его пока, или просто включить "пониженную" форму в дерево. Например:

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[]. Он легко доступен на всех платформах. BTW, во время выполнения мы всегда будем начинать с создания массива байтов, даже с исходного предложения. Кроме того, для поддержки преобразований в 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.

Лучший член функции

... Учитывая список аргументов A с набором выражений аргументов {E1, E2, ..., En} и двумя применимыми элементами функции Mp и Mq с типами параметров {P1, P2, ..., Pn} и {Q1, Q2, ..., Qn}, Mp определяется как более подходящий элемент функции, чем Mq, если

  1. для каждого аргумента неявное преобразование из Ex в Px не является string_constant_to_UTF8_byte_representation_conversion, а для хотя бы одного аргумента неявное преобразование из Ex в Qx является string_constant_to_UTF8_byte_representation_conversionили
  2. для каждого аргумента неявное преобразование из Ex в Px не является преобразованием типа функции.
    • Mp является не универсальным методом или Mp является универсальным методом с параметрами типа {X1, X2, ..., Xp} и для каждого параметра типа Xi аргумент типа выводится из выражения или типа, отличного от function_type, и
    • для хотя бы одного аргумента неявное преобразование из Ex в Qx представляет собой function_type_conversion, или Mq является обобщённым методом с параметрами типа {Y1, Y2, ..., Yq}, и по крайней мере для одного параметра типа Yi тип аргумента определяется из function_type, или
  3. для каждого аргумента неявное преобразование из 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[]".

Мы в порядке с этим изменением поведения? Следует ли задокументировать его как критическое изменение?

Обратите внимание, что не предлагается сделать string_constant_to_UTF8_byte_representation_conversion недоступным при использовании версии языка C#10. В этом случае приведенный выше пример становится ошибкой, а не возвращает поведение 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 быть нечувствительным к регистру?

предложение:

Поддержка суффикса U8 также для обеспечения согласованности с числовыми суффиксами.

Решение :

Утверждено — https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-26.md#suffix-case-sensitivity.

Примеры сегодня

Примеры того, где среда выполнения вручную кодирует байты в формате UTF8 сегодня

Примеры, в которых мы оставим перф в таблице

Дизайн-встречи

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