この記事で使用されている仕様:
- Unicode 7.0.0標準-特に第3章
- C#5 (Word文書)
- ECMA-335 (CLI仕様)
文字列とは何ですか?
string
(または
System.String
)の型をどのように宣言し
System.String
か? 漠然としたものからかなり具体的なものまで、この質問に対するいくつかの答えを提案できます。
- 「引用符で囲まれたテキスト」
- 文字シーケンス
- Unicode文字シーケンス
- 16ビットの文字シーケンス
- UTF-16ワードシーケンス
最後のステートメントのみが完全に真実です。 C#5仕様(セクション1.3)の状態:
C#での文字列と文字の処理はUTF-16を使用します。 タイプchar
は単語UTF-16を表し、タイプ文字列は単語UTF-16のシーケンスを表します。
これまでのところ良い。 しかし、これはC#です。 ILはどうですか? そこで何が使用され、それは重要ですか? ストリングはILで定数として宣言する必要があり、この表現方法の性質は重要です。エンコードだけでなく、このエンコードされたデータの解釈も重要です。 特に、単語のシーケンスUTF-16は、単語のシーケンスUTF-8として常に表現できるとは限りません。
すべてが非常に悪い(形成された)
たとえば、文字列リテラル
“X\uD800Y”
ます。 これは、次のUTF-16単語の文字列表現です。
-
0x0058
「X」 -
0xD800
サロゲートペアの最初の部分 -
0x0059
「Y」
これは完全に通常の文字列です。仕様(セクションD80)によると、Unicode文字列です。 しかし、その形成は不十分です(セクションD84)。 これは、単語UTF-16
0xD800
がUnicodeスカラー値に対応していないためです(セクションD76)。サロゲートペアはスカラー値のリストから明示的に除外されます。
サロゲートペアについて最初に耳にする人向け:UTF-16は16ビットワードのみを使用するため、有効なUnicode値をすべて網羅することはできません。その範囲は
U+10FFFF
です。
U+FFFF
よりも大きいコードの文字をUTF-16で表す必要がある場合、2つの単語が使用されます:サロゲートペアの最初の部分(
0xD800
から
0xDBFF
範囲)と2番目の
0xDC00 … 0xDFFF
(
0xDC00 … 0xDFFF
)。 したがって、サロゲートペアの最初の部分だけは意味をなしません。正しい単語UTF-16ですが、2番目の部分が続く場合にのみ意味を持ちます。
コードを見せてください!
そして、これはどのようにC#に関係していますか? まあ、定数は何らかの形でILレベルで表現する必要があります。 それを表すには2つの方法があることがわかります-ほとんどの場合、UTF-16が使用されますが、属性コンストラクタの引数にはUTF-8が使用されます。
以下に例を示します。
using System; using System.ComponentModel; using System.Text; using System.Linq; [Description(Value)] class Test { const string Value = "X\ud800Y"; static void Main() { var description = (DescriptionAttribute) typeof(Test).GetCustomAttributes(typeof(DescriptionAttribute), true)[0]; DumpString("", description.Description); DumpString("", Value); } static void DumpString(string name, string text) { var utf16 = text.Select(c => ((uint) c).ToString("x4")); Console.WriteLine("{0}: {1}", name, string.Join(" ", utf16)); } }
.NETでは、このプログラムの出力は次のようになります。
: 0058 fffd fffd 0059 : 0058 d800 0059
ご覧のとおり、「定数」は変更されていませんが、属性プロパティの値には文字
U+FFFD
(バイナリ値をテキストにデコードするときに破損データをマークするために使用される特別なコード )が表示されます。 さらに詳しく見て、属性と定数を説明するILコードを見てみましょう。
.custom instance void [System]System.ComponentModel.DescriptionAttribute::.ctor(string) = ( 01 00 05 58 ED A0 80 59 00 00 ) .field private static literal string Value = bytearray (58 00 00 D8 59 00 )
定数(
Value
)の形式は非常に単純です-低から高( リトルエンディアン )のバイト順のUTF-16です。 属性の形式は、セクションII.23.3のECMA-335仕様で説明されています。 詳細に分析します。
- プロローグ(01 00)
- 固定引数(選択したコンストラクター用)
- 05 58 ED A0 80 59(1ライン)
- 05(5に等しい長さ-PackedLen)
- 58 ED A0 80 59(UTF-8でエンコードされた文字列値)
- 名前付き引数の数(00 00)
- 名前付き引数自体(なし)
ここで最も興味深い部分は、「UTF-8でエンコードされた文字列値」です。 値の形式が不適切であるため、値は有効なUTF-8ストリングではありません。 コンパイラーは、サロゲートペアの最初の単語を取得し、2番目の単語が後に続かないと判断し、
U+0800
から
U+FFFF
までの範囲の他の文字を処理するのと同じ方法で単純に処理しました。
サロゲートペア全体がある場合、UTF-8は4バイトを使用して1つのUnicodeスカラー値としてエンコードすることに注意してください。 たとえば、
Value
宣言を次のように変更します。
const string Value = "X\ud800\udc00Y";
この場合、ILレベルで、次のバイトセットを取得します
58 F0 90 80 80 59
-
F0 90 80 80
は、数値
U+10000
下の単語UTF8の表現です。 この行は正しく形成されており、属性と定数の値は同じです。
ただし、最初の例では、定数の値は正しく形成されているかどうかをチェックせずにデコードされますが、追加のチェックは属性値に使用され、誤ったコードを検出して置換します。
エンコード動作
それで、どのアプローチが正しいですか? Unicode仕様(セクションC10)によると、両方とも真です:
プロセスがUnicodeエンコード文字である可能性のある一連のコードを解釈する場合、不適切に形成されたシーケンスは、文字として処理されるのではなく、エラー状態を引き起こすはずです。
同時に:
この仕様に準拠するプロセスは、不適切に形成されたシーケンスを解釈しないでください。 ただし、この仕様では、Unicodeエンコード文字ではないコードの処理を禁止していません。 たとえば、パフォーマンスを向上させるために、低レベルの文字列操作はコードを文字として解釈せずに処理できます。
定数と引数引数の値が「エンコードされたUnicode文字でなければならない」かどうかは完全にはわかりません。 私の経験では、仕様は実際には正しく形成された文字列が必要かどうかをどこにも示していません。
さらに、
System.Text.Encoding
実装は、不適切な形式のデータをエンコードまたはデコードしようとした場合の動作を指定することによりカスタマイズできます。 例:
Encoding.UTF8.GetBytes(Value)
バイト
58 EF BF BD 59
シーケンスを返します。つまり、不正なデータを検出して
U+FFFD
、デコードは問題なく機能します。 ただし:
new UTF8Encoding(true, true).GetBytes(Value)
例外をスローします。 コンストラクターの最初の引数は、 BOMを生成する必要があることを示し、2番目は、誤ったデータを処理する方法を示します(
EncoderFallback
および
DecoderFallback
プロパティも使用されます)。
言語行動
それでは、このコードをコンパイルする必要がありますか? 現時点では、言語仕様はこれを禁止していませんが、仕様は修正できます:)
一般的に言えば、
csc
とRoslynの両方は、一部の属性、たとえば
DllImportAttribute
文字列の使用を依然として禁止しています。
[DllImport(Value)] static extern void Foo();
Value
形式が不適切な場合、このコードはコンパイラエラーをスローします。
エラーCS0591: 'DllImport'属性の引数の値が無効です
おそらく、同じ動作をする他の属性があります-確かではありません。
属性インスタンスを作成するときに属性引数の値が元の形式にデコードされないと仮定する場合、これはコンパイル段階でエラーと見なされます。 (もちろん、不適切に形成された文字列の値を正確に保持するようにランタイムを変更しない限り)
しかし、定数をどうするか? これは有効ですか? これは理にかなっていますか? この例で使用されている行がありそうにない形式では、サロゲートペアの最初の部分で終わる必要がある場合があります。その後、2番目の部分で始まる別の行に追加して正しい行を取得します。 もちろん、ここでは細心の注意を払う必要があります。Unicodeテクニカルレポート#36(セキュリティに関する考慮事項)には、エラーについて非常に警戒すべき可能性がいくつかあります。
前述の結果
このすべての興味深い側面の1つは、「文字列エンコーディング算術」があなたが思うように動作しないかもしれないことです:
// ! string SplitEncodeDecodeAndRecombine(string input, int splitPoint, Encoding encoding) { byte[] firstPart = encoding.GetBytes(input.Substring(0, splitPoint)); byte[] secondPart = encoding.GetBytes(input.Substring(splitPoint)); return encoding.GetString(firstPart) + encoding.GetString(secondPart); }
どこにも
null
がなく、
splitPoint
値が範囲内にある場合、エラーはないと考えるかもしれません。 しかし、サロゲートカップルの真ん中にいることに気付いた場合、すべてが非常に悲しくなります。 正規化の形式などの理由で追加の問題が発生する可能性もあります。もちろん、おそらくそうではありませんが、現時点では100%確信が持てません。
この例が現実と離婚しているように思える場合、いくつかのネットワークパケットまたはファイルに分割された大きなテキストを想像してください-それは重要ではありません。 あなたは十分に賢明であり、バイナリデータがUTF-16コードペアの途中で共有されないように注意しているように見えるかもしれません-しかし、これでもあなたを救うことはありません。 ああ、ああ。
テキストの処理を拒否するのは本当にのどが渇いています。 浮動小数点数は本当の悪夢であり、日付と時刻です...まあ、あなたはそれらについて私が思うことを知っています。 決してオーバーフローしないことが保証されている整数のみを使用するプロジェクトがあるのだろうか? そのようなプロジェクトがある場合-お知らせください!
おわりに
テキストは難しい!
翻訳者注:
この記事のオリジナルへのリンクは、 「モノとMS.NETの違いについて話しましょう」という投稿で見つけました。 DreamWalkerに感謝します ! ちなみに、彼はまた、同じ例がMonoでどのように動作するかについてのブログに小さなブログ投稿を持っています。