Всем, наверное, известно, что строки в .Net хоть и являются ссылочным типом, неизменяемы, что делает их поведение отчасти похожим на поведение значимых типов. То есть, вот в этом коде мы создали не две строки, а три:
string s1 = "te"; string s2 = "st"; s1 = s1 + s2;
В первой строке создается первая строка, во второй вторая, а в третьей - третья, адрес которой потом присваевается первой. В этом примере создание еще одной строковой переменной "за кулисами" кроме лишней траты памяти ничем не грозит, но вот вам еще один пример. Поинтереснее.
string s1 = "hello"; string s2 = s1; Console.WriteLine(s1); Console.WriteLine(s2); Console.WriteLine(); s1 = "world"; Console.WriteLine(s1); Console.WriteLine(s2);
В этом примере неизменяемость строк приводит к не очень очевидным результатам. Как думаете, что будет выведено на экран? Правильный ответ:
hello hello world hello
И еще, чтобы окончательно запутать бедных программистов, тип string переопределяет оператор ==, чтобы он сравнивал не ссылки, а содержимое объектов. Что, впрочем, для строк очень часто одно и то же, что и сравнение по ссылке.
Так почему строки сделали неизменяемыми, спросите вы. Причин несколько.
Во-первых, это сделано для того, чтобы строки были потокобезопасными. А так как строки в CLR хранятся как обычные BSTR-строки (четыре байта длинны, затем сама строка по два байта на символ и два нуля в конце) то их легко можно передавать в неуправляемый код как WCHAR*.
[Хотя, на самом деле, строки в .Net занимают 18+кол-во символов*2 (.Net версии до 4) или 14+кол-во символов*2 (.Net 4+) с округлением до 4 байт на x86 и 30+кол-во символов*2 (.Net версии до 4) или 26+кол-во символов*2 (.Net 4+) с округлением до 8 байт на x64 ]
Во-вторых строки часто используются в качестве ключей в хешах, а если строки неизменяемы, то вы не можете изменить уже созданный ключ, даже случайно. В-третьих, это скорость и эффективность операций над строками. Например, при конкатенации строк просто выделяется участок памяти равный размеру первой и второй строк, в который строки копируются.
И еще одна важная особенность строк в .Net ради которой строки сделаны неизменяемыми - интернирование. Что это такое хорошо проиллюстрирует следующий пример:
string s1 = "world"; string s3 = "wo" + "rld"; Console.WriteLine(object.ReferenceEquals(s1, s3)); // True, s1 и s3 указывают на один и тот объект в памяти.
Правда, интернирование работает только во время компиляции. Во время выполнения строки автоматически не интернируются. Что, впрочем, не мешает вам сделать это самостоятельно.
string s4 = "wo"; string s5 = "rld"; string s6 = s4 + s5; Console.WriteLine(object.ReferenceEquals(s1, s6)); //False s1 и s6 указывают на разные объекты в памяти. s6 = string.Intern(s4 + s5); Console.WriteLine(object.ReferenceEquals(s1, s6)); // True, s1 и s3 указывают на один и тот объект в памяти.
Смысл интернирования в оптимизации во-первых, памяти, а во-вторых, неэффективного кода как в первой части примера выше.
Ну и последнее, по поводу StringBuilder, который частенько рекомендуют использовать в коде чуть ли не везде вместо string. Давайте проверим. Возьмем вот такой код:
Stopwatch timer = new Stopwatch(); long mem = GC.GetTotalMemory(true); timer.Start(); string testString = ""; for (int i = 0; i < 10000; i++) { testString += i.ToString(); } timer.Stop(); mem = GC.GetTotalMemory(true) - mem; Console.WriteLine(mem); Console.WriteLine(timer.Elapsed); timer.Reset(); mem = GC.GetTotalMemory(true); timer.Start(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.Append(i.ToString()); } string result = sb.ToString(); sb.Clear(); timer.Stop(); mem = GC.GetTotalMemory(true) - mem; Console.WriteLine(mem); Console.WriteLine(timer.Elapsed);
У меня получились следующие результаты:
77808 00:00:00.1026275 80456 00:00:00.0015509
Как видим, память сборщиком мусора почистилась довольно эффективно, правда за счет его работы время выполнения увеличилось почти в 7 раз, что, конечно, не хорошо. А если попробовать то же самое, но не с 10000 строк, а с 10?
48 00:00:00.0000115 104 00:00:00.0000149
Картина, не сказать, что противоположная, но теперь у варианта со StringBuilder и памяти занято в два раза больше и время выполнения на 40% больше.
Так что, StringBuilder имеет смысл использовать только при работе с действительно большим количеством строк. Если же вам нужно сделать "Hello" + "World", то от его использования никакой пользы точно не будет.
Комментариев нет:
Отправить комментарий