はじめに
文字列データ型は、数値(int、long、double)および論理(bool)とともに、基本型の1つです。 少なくとも、このタイプを使用しない有用なプログラムを想像するのは困難です。
.NETプラットフォームでは、文字列型は不変のStringクラスとして表されます。 さらに、CLRの共通言語環境に高度に統合されており、C#コンパイラからのサポートもあります。
この記事では、数値の加算操作と同じくらい頻繁に文字列に対して実行される操作である連結について説明します。 ここでは、文字列演算子+についてすべて知っているため、ここで説明できるように思えますが、結局のところ、彼には独自の微妙さがあります。
文字列演算子の言語仕様+
C#言語仕様は、文字列に3つの演算子+オーバーロードを提供します。
string operator + (string x, string y) string operator + (string x, object y) string operator + (object x, string y)
文字列連結のオペランドの1つがnullの場合、空の文字列が置換されます。 それ以外の場合、文字列ではない引数は、仮想ToStringメソッドを呼び出して文字列にキャストされます。 ToStringメソッドがnullを返す場合、空の文字列が置換されます。 仕様によれば、この操作は決して nullを返すべきではないということを言っておくべきです。
演算子の説明は十分に明確に見えますが、Stringクラスの実装を見ると、2つの演算子==および!=のみの明示的な定義が見つかります。 合理的な疑問が生じます。ストリング連結の背後で何が起こるのでしょうか? コンパイラは文字列演算子+をどのように処理しますか?
この質問に対する答えはそれほど複雑ではありませんでした。 静的なString.Concatメソッドをよく見る必要があります。 String.Concatメソッド-Stringクラスの1つ以上のインスタンス、またはObjectの1つ以上のインスタンスの値のString表現を組み合わせます。 このメソッドの次のオーバーロードが利用可能です。
public static String Concat(String str0, String str1) public static String Concat(String str0, String str1, String str2) public static String Concat(String str0, String str1, String str2, String str3) public static String Concat(params String[] values) public static String Concat(IEnumerable<String> values) public static String Concat(Object arg0) public static String Concat(Object arg0, Object arg1) public static String Concat(Object arg0, Object arg1, Object arg2) public static String Concat(Object arg0, Object arg1, Object arg2, Object arg3, __arglist) public static String Concat<T>(IEnumerable<T> values)
詳細
次の式s = a + bがあるとします。ここで、aとbは文字列です。 コンパイラは、それを静的なConcatメソッドの呼び出しに変換します。
s = string.Concat(a, b)
文字列連結操作は、C#の他の追加操作と同様に、左結合です。
2行ですべてが明確になりましたが、さらに行がある場合はどうでしょうか。 操作の左結合性が与えられると、式s = a + b + cは
s = string.Concat(string.Concat(a, b), c)
ただし、3つの引数を取るオーバーロードが存在する場合、次のように変換されます。
s = string.Concat(a, b, c)
状況は、4行の連結と似ています。 5行以上を連結するには、string.Concat(params string [])のオーバーロードがあるため、配列にメモリを割り当てることに伴うオーバーヘッドを考慮する必要があります。
また、 文字列連結操作は完全に連想的であると言う必要があります。 文字列を連結する順序は関係ないため、連結の優先順位の明示的な指示にもかかわらず式s = a +(b + c)は次のように処理されます。
s = (a + b) + c = string.Concat(a, b, c)
期待の代わりに
s = string.Concat(a, string.Concat(b, c))
したがって、上記を要約すると、文字列連結操作は常に左から右に提示され、静的String.Concatメソッドの呼び出しを使用します。
リテラル文字列のコンパイラ最適化
C#言語コンパイラには、リテラル文字列に関連する最適化があります。 したがって、たとえば、式s = "a" + "b" + cは、演算子+の左結合性が与えられると、s =( "a" + "b")+ cと同等に変換されます。
s = string.Concat("ab", c)
連結演算の左結合性(s =(c + "a")+ "b")にもかかわらず、式s = c + "a" + "b"は次のように変換されます。
s = string.Concat(c, "ab")
一般に、リテラルの場所に関係なく、コンパイラーは可能な限りすべてを連結してから、適切なConcatメソッドのオーバーロードを選択しようとします。 式s = a + "b" + "c" + dは次のように変換されます
s = string.Concat(a, "bc", d)
また、空のヌル文字列に関連付けられた最適化についても説明する必要があります。 コンパイラは、空の文字列を追加しても連結の結果に影響しないことを知っているため、式s = a + "" + bは次のように変換されます。
s = string.Concat(a, b),
期待の代わりに
s = string.Concat (a, "", b)
同様に、値がnullのconst文字列の場合、次のようになります。
const string nullStr = null; s = a + nullStr + b;
に変換
s = string.Concat(a, b)
式s = a + nullStrはs = a ??に変換されます aが文字列の場合は ""、aが文字列でない場合(たとえば、s = 17 + nullStr)のstring.Concat(a)メソッドの呼び出しは、s = string.Concat(17)に変換されます。
処理リテラルの最適化と文字列演算子+の左結合性に関連する興味深い機能。
次の式を検討してください。
var s1 = 17 + 17 + "abc";
左結合性が与えられた場合、それは同等です
var s1 = (17 + 17) + "abc"; // string.Concat(34, "abc")
その結果、コンパイル段階で数値の加算が発生し、結果は34abcになります。
一方、表現
var s2 = "abc" + 17 + 17;
同等に
var s2 = ("abc" + 17) + 17; // string.Concat("abc", 17, 17)
abc1717になります。
そのため、同じ連結操作は異なる結果につながるように思われます。
String.Concat VS StringBuilder.Append
この比較についていくつかの言葉を言う必要があります。 次のコードを検討してください。
string name = "Timur"; string surname = "Guev"; string patronymic = "Ahsarbecovich"; string fio = surname + name + patronymic;
StringBuilderを使用してコードに置き換えることができます。
var sb = new StringBuilder(); sb.Append(surname); sb.Append(name); sb.Append(patronymic); string fio = sb.ToString();
しかし、この状況では、StringBuilderを使用するメリットはほとんど得られません。 コードを読みにくくすることに加えて、Concatメソッドの実装は結果の文字列の長さを計算し、結果の文字列の長さについて何も知らないStringBuilderとは異なり、メモリを1回だけ割り当てるため、効率も低下しました。
3行の連結メソッドの実装:
public static string Concat(string str0, string str1, string str2) { if (str0 == null && str1 == null && str2 == null) return string.Empty; if (str0 == null) str0 = string.Empty; if (str1 == null) str1 = string.Empty; if (str2 == null) str2 = string.Empty; string dest = string.FastAllocateString(str0.Length + str1.Length + str2.Length); // string.FillStringChecked(dest, 0, str0); / string.FillStringChecked(dest, str0.Length, str1); string.FillStringChecked(dest, str0.Length + str1.Length, str2); return dest; }
Javaの+演算子
Javaの文字列演算子+に関するいくつかの単語。 私はJavaでプログラミングしていませんが、物事がどのように存在するかを知ることは興味深いです。 Javaコンパイラは+演算子を最適化して、StringBuilderクラスとappendメソッド呼び出しを使用します。
前のコードはに変換されます
String fio = new StringBuilder(String.valueOf(surname)).append(name).append(patronymic).ToString()
また、C#がそのような最適化を意図的に拒否したことにも言及する価値があります。EricLippertがこの主題に関する投稿をしています。 実際のところ、このような最適化は最適化そのものではなく、コードの書き直しです。 さらに、C#言語の作成者は、開発者がStringクラスを操作する機能を知っておく必要があると考えており、必要に応じてStringBuilderの使用に切り替えます。
ちなみに、文字列の連結に関連するC#コンパイラの最適化に関与したのはEric Lippertでした。
おわりに
おそらく一見すると、より大きなコードの可視性に関連するコンパイラ最適化の可能性について考えるまで、Stringクラスが+演算子を定義しないのは奇妙に思えるかもしれません。 たとえば、+演算子がStringクラスで定義されている場合、式s = a + b + c + dにより、2つの中間行が作成され、stringへの唯一の呼び出しが行われます。Concat(a、b、c、d)メソッドは、効果的に。