Всем, наверное, известно, что строки в .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", то от его использования никакой пользы точно не будет.
Комментариев нет:
Отправить комментарий