Использование символов Unicode больше 2 байтов с .Net
Я использую этот код для генерации U+10FFFC
var s = Encoding.UTF8.GetString(new byte[] {0xF4,0x8F,0xBF,0xBC});
Я знаю это для частного использования и т.д., но он отображает один символ, как я ожидал бы при его отображении. Проблемы возникают при манипулировании этим символом юникода.
Если я потом сделаю это:
foreach(var ch in s)
{
Console.WriteLine(ch);
}
Вместо того, чтобы печатать только один символ, он печатает два символа (т.е. строка, по-видимому, состоит из двух символов). Если я изменю свой цикл, чтобы добавить эти символы в пустую строку, например:
string tmp="";
foreach(var ch in s)
{
Console.WriteLine(ch);
tmp += ch;
}
В конце этого, tmp
будет печатать только один символ.
Что именно здесь происходит? Я думал, что char
содержит один символ Юникода, и мне никогда не приходилось беспокоиться о том, сколько байтов имеет символ, если я не делаю преобразование в байты. Мой реальный прецедент - я должен уметь обнаруживать, когда в строке используются очень большие символы Юникода. В настоящее время у меня есть что-то вроде этого:
foreach(var ch in s)
{
if(ch>=0x100000 && ch<=0x10FFFF)
{
Console.WriteLine("special character!");
}
}
Однако из-за этого разделения очень больших символов это не работает. Как я могу изменить это, чтобы заставить его работать?
Ответы
Ответ 1
U + 10FFFC - это одна кодовая точка Юникода, но интерфейс string
не предоставляет прямой код кодов Unicode. Его интерфейс предоставляет последовательность кодовых блоков UTF-16. Это очень низкоуровневое представление текста. Очень печально, что такое низкоуровневое представление текста было перенесено на наиболее очевидный и интуитивно понятный интерфейс... Я постараюсь не рассказать о том, как мне не нравится этот дизайн, и просто скажу, что не важно как печально, это просто (печальный) факт, с которым вам нужно жить.
Во-первых, я предлагаю использовать char.ConvertFromUtf32
, чтобы получить исходную строку. Гораздо проще и понятнее:
var s = char.ConvertFromUtf32(0x10FFFC);
Итак, эта строка Length
не 1, потому что, как я сказал, интерфейс имеет дело с кодовыми единицами UTF-16, а не с кодами Unicode. U + 10FFFC использует два кодовых блока UTF-16, поэтому s.Length
равно 2. Все кодовые точки выше U + FFFF требуют для их представления двух кодовых блоков UTF-16.
Следует отметить, что ConvertFromUtf32
не возвращает char
: char
- это код кода UTF-16, а не кодовая точка Юникода. Чтобы иметь возможность возвращать все коды кода Unicode, этот метод не может вернуть один char
. Иногда ему нужно вернуть два, и поэтому он делает его строкой. Иногда вы можете найти некоторые API, работающие в int
вместо char
, потому что int
может использоваться для обработки всех кодовых точек (что то, что ConvertFromUtf32
принимает как аргумент, и что ConvertToUtf32
производит как результат).
string
реализует IEnumerable<char>
, что означает, что при повторении итерации по string
вы получаете один кодовый блок UTF-16 за итерацию. Вот почему повторение строки и ее распечатка приводит к некоторому сломанному результату с двумя "вещами" в ней. Это два блока кода UTF-16, которые составляют представление U + 10FFFC. Их называют "суррогатами". Первый - суррогат высокого/свинцового, а второй - суррогат с низким/низким уровнем. Когда вы печатаете их отдельно, они не создают значимого вывода, потому что одиночные суррогаты даже не действуют в UTF-16, и они также не считаются символами Юникода.
Когда вы добавляете эти два суррогата к строке в цикле, вы фактически восстанавливаете суррогатную пару и печатаете эту пару позже, когда вы получаете правильный результат.
И в разглагольствовающем фронте обратите внимание, как ничто не жалуется на то, что вы использовали неправильную последовательность UTF-16 в этом цикле. Он создает строку с одиноким суррогатом, и все же все происходит так, как будто ничего не произошло: тип string
не является даже типом хорошо сформированных UTF-16 последовательностей кода, но тип любой последовательности кода UTF-16.
Структура char
предоставляет статические методы для обработки суррогатов: IsHighSurrogate
, IsLowSurrogate
, IsSurrogatePair
, ConvertToUtf32
и ConvertFromUtf32
, Если вы хотите, вы можете написать итератор, который выполняет итерацию по символам Unicode вместо кодовых блоков UTF-16:
static IEnumerable<int> AsCodePoints(this string s)
{
for(int i = 0; i < s.Length; ++i)
{
yield return char.ConvertToUtf32(s, i);
if(char.IsHighSurrogate(s, i))
i++;
}
}
Затем вы можете повторить так:
foreach(int codePoint in s.AsCodePoints())
{
// do stuff. codePoint will be an int will value 0x10FFFC in your example
}
Если вы хотите получить каждую кодовую точку в виде строки, вместо этого измените тип возврата на IEnumerable<string>
и строку доходности:
yield return char.ConvertFromUtf32(char.ConvertToUtf32(s, i));
С этой версией работает следующее:
foreach(string codePoint in s.AsCodePoints())
{
Console.WriteLine(codePoint);
}
Ответ 2
Как уже было опубликовано Martinho, гораздо проще создать строку с этим приватным кодеком таким образом:
var s = char.ConvertFromUtf32(0x10FFFC);
Но для прокрутки двух char элементов этой строки бессмысленно:
foreach(var ch in s)
{
Console.WriteLine(ch);
}
Зачем? Вы просто получите высокий и низкий суррогат, который кодирует код. Помните, что char - это 16-разрядный тип, поэтому он может содержать только максимальное значение 0xFFFF. Ваш код не подходит для 16-разрядного типа, и для самого высокого кода вам понадобится 21 бит (0x10FFFF), поэтому следующий более широкий тип будет просто 32-битным. Два элемента char не являются символами, а суррогатной. Значение 0x10FFFC кодируется в два суррогата.
Ответ 3
Пока @R. Ответ Martinho Fernandes правильный, его метод расширения AsCodePoints
имеет две проблемы:
- Он набрасывает
ArgumentException
на неверные кодовые точки (высокий суррогат без низкого суррогата или наоборот).
- Вы не можете использовать статические методы
char
, которые принимают (char)
или (string, int)
(например, char.IsNumber()
), если у вас есть только внутренние коды.
Я разделил код на два метода: один похожий на оригинал, но возвращает символ замены Unicode на недопустимые кодовые точки. Второй метод возвращает struct IEnumerable с более полезными полями:
StringCodePointExtensions.cs
public static class StringCodePointExtensions {
const char ReplacementCharacter = '\ufffd';
public static IEnumerable<CodePointIndex> CodePointIndexes(this string s) {
for (int i = 0; i < s.Length; i++) {
if (char.IsHighSurrogate(s, i)) {
if (i + 1 < s.Length && char.IsLowSurrogate(s, i + 1)) {
yield return CodePointIndex.Create(i, true, true);
i++;
continue;
} else {
// High surrogate without low surrogate
yield return CodePointIndex.Create(i, false, false);
continue;
}
} else if (char.IsLowSurrogate(s, i)) {
// Low surrogate without high surrogate
yield return CodePointIndex.Create(i, false, false);
continue;
}
yield return CodePointIndex.Create(i, true, false);
}
}
public static IEnumerable<int> CodePointInts(this string s) {
return s
.CodePointIndexes()
.Select(
cpi => {
if (cpi.Valid) {
return char.ConvertToUtf32(s, cpi.Index);
} else {
return (int)ReplacementCharacter;
}
});
}
}
CodePointIndex.cs
:
public struct CodePointIndex {
public int Index;
public bool Valid;
public bool IsSurrogatePair;
public static CodePointIndex Create(int index, bool valid, bool isSurrogatePair) {
return new CodePointIndex {
Index = index,
Valid = valid,
IsSurrogatePair = isSurrogatePair,
};
}
}
![CC0]()
Насколько это возможно по закону, лицо, связавшее CC0 с этой работой, отказалось от всех авторских и смежных или смежных прав на эту работу.Суб >
Ответ 4
Еще одна альтернатива перечислять символы UTF32 в строке С# заключается в использовании метода System.Globalization.StringInfo.GetTextElementEnumerator
, как в приведенном ниже коде.
public static class StringExtensions
{
public static System.Collections.Generic.IEnumerable<UTF32Char> GetUTF32Chars(this string s)
{
var tee = System.Globalization.StringInfo.GetTextElementEnumerator(s);
while (tee.MoveNext())
{
yield return new UTF32Char(s, tee.ElementIndex);
}
}
}
public struct UTF32Char
{
private string s;
private int index;
public UTF32Char(string s, int index)
{
this.s = s;
this.index = index;
}
public override string ToString()
{
return char.ConvertFromUtf32(this.UTF32Code);
}
public int UTF32Code { get { return char.ConvertToUtf32(s, index); } }
public double NumericValue { get { return char.GetNumericValue(s, index); } }
public UnicodeCategory UnicodeCategory { get { return char.GetUnicodeCategory(s, index); } }
public bool IsControl { get { return char.IsControl(s, index); } }
public bool IsDigit { get { return char.IsDigit(s, index); } }
public bool IsLetter { get { return char.IsLetter(s, index); } }
public bool IsLetterOrDigit { get { return char.IsLetterOrDigit(s, index); } }
public bool IsLower { get { return char.IsLower(s, index); } }
public bool IsNumber { get { return char.IsNumber(s, index); } }
public bool IsPunctuation { get { return char.IsPunctuation(s, index); } }
public bool IsSeparator { get { return char.IsSeparator(s, index); } }
public bool IsSurrogatePair { get { return char.IsSurrogatePair(s, index); } }
public bool IsSymbol { get { return char.IsSymbol(s, index); } }
public bool IsUpper { get { return char.IsUpper(s, index); } }
public bool IsWhiteSpace { get { return char.IsWhiteSpace(s, index); } }
}