Низове и неизменност

В света на програмирането низ е масив от символи System.Char, които, когато са събрани заедно, представляват текст. В езика за програмиране C# можете да декларирате низ и да отпечатате стойността му, както следва:

string str = "Hello World";
Console.WriteLine(str);
//This will print out "Hello World" to the console.

Когато създадете низ, той е неизменен. Това означава, че е само за четене. Когато нещо е неизменно или само за четене, това означава, че не може да бъде променено по-късно.

Защо низовете са неизменни

В C# CLR (Common Language Runtime) отговаря за определянето къде да се съхраняват низове. В последния раздел отбелязах, че низът е масив от знаци. CLR имплементира масив за съхраняване на низове. Масивите са структура от данни с фиксиран размер, което означава, че не могат да бъдат динамично увеличавани или намалявани по размер. След като на масив бъде зададен размер, размерът не може да бъде променен. За да направите масива по-голям, данните трябва да бъдат копирани и клонирани в нов масив, който се поставя в нов блок памет от CLR. Ако редактирате низ, вие наистина не модифицирате този низ; по-скоро CLR създава нова препратка към паметта за модифицирания низ и оригиналният низ ще бъде премахнат от паметта чрез събиране на отпадъци.

Да погледнем под капака

Въпреки че не е наложително, е абсолютно важно да знаете какво се случва в паметта, докато пишете кода си, независимо дали е с низове или структури от данни или нещо друго. Помага по различни начини, независимо дали става въпрос за коригиране на софтуерни грешки или диагностициране на изтичане на памет или проблеми с производителността. Смятам, че най-доброто сравнение е да знаете какво се случва под капака на вашия автомобил. Въпреки че не е критично да знаете, е полезно да знаете частите под капака на превозното средство и каква цел служат в случай на проблеми с превозното средство.

Нека разгледаме по-подробно примера от предишния раздел. Ще създадем нов низ, след това ще го модифицираме и ще обясним какво се случва в паметта по време на всичко това.

string str = "Hello World";
Console.WriteLine(str);
//This will print out "Hello World" to the console.

В програмата създаваме низов обект с име str и му присвояваме стойността „Hello World“. В паметта обаче CLR създава блокове пространство за съхраняване на тази променлива. За простота, нека кажем, че CLR използва място в паметта 1000, за да съхранява str. Тъй като това е обект, CLR ще съхранява това в купчината, а не в стека. Сега нека модифицираме този низ.

str += " edited";
Console.WriteLine(str);
//This will print out "Hello World edited" to the console.

Когато стартирате този код, ще видите „Hello World edited“. Тъй като този низ е неизменен, CLR отново създава нови блокове пространство в паметта, за да съхранява тази променлива. CLR ще присвои ново място в паметта, да кажем местоположение 1500 за тази нова променлива. В крайна сметка събирачът на отпадъци ще изхвърли оригиналния низ, съхранен в местоположение 1000, и ще го изчисти от паметта.

Предимства и недостатъци

Както почти всичко останало, има причини да се използват неизменни низове и има причини да не се използват неизменни низове. Защо трябва да използвате неизменни низове? Едно предимство е, че те са безопасни за нишки. Ако работите с многонишкова система, няма да има риск от блокиране или проблеми с паралелността, тъй като когато модифицирате низ, вие всъщност просто създавате нов обект в паметта. Друго предимство е, че няма да се притеснявате от случайна смяна. Не е необходимо да предприемате допълнителни мерки за безопасност (т.е. копие на защитен обект), които може да се наложи да вземете с променлив обект.

Защо неизменните низове са лоша идея? Основният проблем е, че постоянно променящите се низове могат да доведат до проблеми с производителността. Ще обясним това в кодов блок. Ако се върнете към кодовите фрагменти от предишния раздел, ще видите, че променихме низа само веднъж. Да предположим, че имаме сценарий като този:

string str = "Hello World";
Console.WriteLine(str);
//This will print out "Hello World" to the console.

for (int i = 0; i < 10; i++)
{
	str += " again";
	Console.WriteLine(str);
}

Този код отпечатва „отново“ за всяка итерация (десет пъти) след оригиналното „Hello World“. Въпреки това, за всяка итерация, тъй като низът е неизменен, това, което се случва в паметта е, че десет пъти CLR разпределя ново място в паметта и съхранява нова str променлива и всеки път създава по-големи блокове, за да спести повече данни.