Глава 13. Символни низове

В тази тема...

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

Съдържание

Видео

Презентация

Мисловни карти


Символни низове

В практиката често се налага обработката на текст: четене на текстови файлове, търсене на ключови думи и заместването им в даден параграф, валидиране на входни потребителски данни и др. В такива случаи можем да запишем текстовото съдържание, с което ще боравим, в символни низове, и да го обработим с помощта на езика C#.

Какво е символен низ (стринг)?

Символният низ е последователност от символи, записана на даден адрес в паметта. Помните ли типа char? В променливите от тип char можем да запишем само един символ. Когато е необходимо да обработваме повече от един символ на помощ идват низовете.

В .NET Framework всеки символ има пореден номер от Unicode таблицата. Стандартът Unicode е създаден в края на 80-те и началото на 90-те години с цел съхраняването на различни типове текстови данни. Предшестве­никът му ASCII позволява записването на едва 128 или 256 символа (съответно ASCII стан­дарт със 7-битова или 8-битова таблица). За съжаление, това често не удовлетворява нуждите на потребителя – тъй като в 128 символа могат да се поберат само цифри, малки и главни латински букви и някои специ­ални знаци. Когато се наложи работа с текст на кирилица или друг специфи­чен език (например азиатски или африкански), 128 или 256 символа са крайно недостатъчни. Ето защо .NET използва 16-битова кодова таблица за символите. С помощта на знанията ни за бройните системи и предста­вянето на информацията в компютрите, можем да сметнем, че кодовата таблица съхранява 2^16 = 65536 символа. Някои от символите се кодират по специфичен начин, така че е възможно използването на два символа от Unicode таблицата за създаване на нов символ – така получените знаци надхвърлят 100 000.

Класът System.String

Класът System.String позволява обработка на символни низове в C#. За декларация на низовете ще продължим да използваме служебната дума string, която е псевдоним (alias) в C# на класа System.String от .NET Framework. Работата със string ни улеснява при манипулацията на текстови данни: построяване на текстове, търсене в текст и много други операции. Пример за декларация на символен низ:

string greeting = "Hello, C#";

Декларирахме променливата greeting от тип string, която има съдър­жание "Hello, C#". Представянето на съдържанието в символния низ изглежда по подобен начин:

H

e

l

l

o

,

 

C

#

Вътрешното представяне на класа е съвсем просто – масив от символи. Можем да избегнем използването на класа, като декларираме променлива от тип char[] и запълним елементите на масива символ по символ. Недостатъците на това обаче са няколко:

1.  Запълването на масива става символ по символ, а не наведнъж.

2.  Трябва да знаем колко дълъг ще е текстът, за да сме наясно дали ще се побере в заделеното място за масива.

3.  Обработката на текстовото съдържание става ръчно.

Класът String – универсално решение?

Използването на System.String не е идеално и универсално решение – понякога е уместно използването на други символни структури.

В C# съществуват и други класове за обработка на текст – с някои от тях ще се запознаем по-нататък в настоящата тема.

Типът string е по-особен от останалите типове данни. Той е клас и като такъв той спазва принципите на обектно-ориентираното програмиране. Стойностите му се записват в динамичната памет, а променливите от тип string пазят препратка към паметта (референция към обект в динамич­ната памет).

Класът string има важна особеност – последователностите от символи, записани в променлива от класа, са неизменими (immutable). След като е веднъж зададено, съдържанието на променливата не се променя директно – ако опитаме да променим стойността, тя ще бъде записана на ново място в динамичната памет, а променливата ще започне да сочи към него. Тъй като това е важна особеност, тя ще бъде онагледена малко по-късно.

Стринговете и масиви от символи

Стринговете много приличат на масиви от символи (char[]), но за разлика от тях не могат да се променят. Подобно на масивите те имат свойство Length, което връща дължината на низа, и позволяват достъп по индекс. Индексирането, както и при масивите, става по индекси от 0 до Length-1. Достъпът до символа на дадена позиция в даден стринг става с оператора [] (индексатор), но е позволен само за четене:

string str = "abcde";

char ch = str[1]; // ch == 'b'

str[1] = 'a'; // Compilation error

ch = str[50]; // IndexOutOfRangeException

 

Символни низове – прост пример

Да дадем един пример за използване на променливи от тип string:

string message = "Stand up, stand up, Balkan superman.";

 

Console.WriteLine("message = {0}", message);

Console.WriteLine("message.Length = {0}", message.Length);

 

for (int i = 0; i < message.Length; i++)

{

      Console.WriteLine("message[{0}] = {1}", i, message[i]);

}

// Console output:

// message = "Stand up, stand up, Balkan superman."

// message.Length = 36

// message[0] = S

// message[1] = t

// message[2] = a

// message[3] = n

// message[4] = d

// ...

Обърнете внимание на стойността на стринга – кавичките не са част от текста, а ограждат стойността му. В примера е демонстрирано как може да се отпечатва символен низ, как може да се извлича дължината му и как може да се извличат символите, от които е съставен.

Escaping при символните низове

Както вече знаем, ако искаме да използваме кавички в съдържанието на символен низ, трябва да поставим наклонена черта преди тях за да укажем, че имаме предвид самия символ кавички, а не използване кавичките за начало / край на низ:

string quote = "Book’s title is \"Intro to C#\"";

// Book's title is "Intro to C#"

Кавичките в примера са част от текста. В променливата те са добавени чрез поставянето им след екраниращия знак обратна наклонена черта (\). По този начин компилаторът разбира, че кавичките не служат за начало или край на символен низ, а са част от данните. Избягването на специал­ните символи в сорс кода се нарича екраниране (escaping).

Деклариране на символен низ

Можем да декларираме променливи от тип символен низ по следния начин:

string str;

Декларацията на символен низ представлява декларация на променлива от тип string. Това не е еквивалентно на създаването на променлива и заделянето на памет за нея! С декларацията уведомяваме компилатора, че ще използваме променлива str и очакваният тип за нея е string. Ние не създаваме променливата в паметта и тя все още не е достъпна за обра­ботки (има стойност null, което означава липса на стойност).

Създаване и инициализиране на символен низ

За да може да обработваме декларираната стрингова променлива, трябва да я създадем и инициализираме. Създаването на променлива на клас (познато още като инстанциране) е процес, свързан със заделянето на област от динамичната памет. Преди да зададем конкретна стойност на символния низ, стойността му е null. Това може да бъде объркващо за начинаещия програмист: неинициализираните променливи от типа string не съдържат празни стойности, а специалната стойност null – и опитът за манипу­лация на такъв стринг ще генерира грешка (изключение за достъп до липсваща стойност NullReferenceException)!

Можем да инициализираме променливи по 3 начина:

1.  Чрез задаване на низов литерал.

2.  Чрез присвояване стойността от друг символен низ.

3.  Чрез предаване стойността на операция, връщаща символен низ.

Задаване на литерал за символен низ

Задаването на литерал за символен низ означава предаване на преде­финирано текстово съдържание на променлива от тип string. Използваме такъв тип инициализация, когато знаем стойността, която трябва да се съхрани в променливата. Пример за задаване на литерал за символен низ:

string website = "https://introprogramming.info/";

В този пример създаваме променливата website и й задаваме като стой­ност символен литерал.

Присвояване стойността на друг символен низ

Присвояването на стойността е еквивалентно на насочване на string стойност или променлива към дадена променлива от тип string. Пример за това е следният фрагмент:

string source = "Some source";

string assigned = source;

Първо, декларираме и инициализираме променливата source. След това променливата assigned приема стойността на source. Тъй като класът string е референтен тип, текстът "Some source" е записан в динамичната памет (heap, хийп) на място, сочено от първата променлива.

clip_image002

На втория ред пренасочваме променливата assigned към същото място, към което сочи другата променлива. Така двата обекта получават един и същ адрес в динамичната памет и следователно една и съща стойност.

Промяната на коя да е от променливите обаче ще се отрази само и единствено на нея, поради неизменимостта на типа string, тъй като при промяната ще се създаде копие на променяния стринг. Това не се отнася за останалите референтни типове (нормалните изменими типове), защото при тях промените се нанасят директно на адреса в паметта и всички референции сочат към променения обект.

Предаване стойността на операция, връщаща символен низ

Третият вариант за инициализиране на символен низ е предаването на стойността на израз или операция, която връща стрингов резултат. Това може да бъде резултат от метод, който валидира данни; събиране на стойностите на няколко константи и променливи, преобразуване на съществуваща променлива и др. Пример за израз, връщащ символен низ:

string email = "[email protected]";

string info = "My mail is: " + email;

// My mail is: [email protected]

Променливата info е създадена от съединяването (concatenation) на литерали и променлива.

Четене и печатане на конзолата

Нека сега разгледаме как можем да четем символни низове, въведени от потребителя, и как можем да печатаме символни низове на стандартния изход (на конзолата).

Четене на символни низове

Четенето на символни низове може да бъде осъществено чрез методите на познатия ни клас System.Console:

string name = Console.ReadLine();

В примера прочитаме от конзолата входните данни чрез метода ReadLine(). Той предизвиква потребителя да въведе стойност и да натисне [Enter]. След натискане на клавиша [Enter] променливата name ще съдържа въведеното име от клавиатурата.

Какво можем да правим, след като променливата е създадена и в нея има стойност? Можем например да я използваме в изрази с други символни низове, да я подаваме като параметър на методи, да я записваме в текстови документи и др. На първо време, можем да я изведем на конзолата, за да се уверим, че данните са прочетени коректно.

Отпечатване на символни низове

Извеждането на данни на стандартния изход се извършва също чрез познатия ни клас System.Console:

Console.WriteLine("Your name is: " + name);

Използвайки метода WriteLine(…) извеждаме съобщението: "Your name is:", следвано от стойността на променливата name. След края на съобще­нието се добавя символ за нов ред. Ако искаме да избегнем символа за нов ред и съобщенията да се извеждат на един и същ, тогава прибягваме към метода Write(…).

Можем да си припомним класа System.Console от темата "Вход и изход от конзолата".

Операции върху символни низове

След като се запознахме със семантиката на символните низове, как можем да ги създаваме и извеждаме, следва да се научим как да боравим с тях и да ги обработваме. Езикът C# ни дава набор от готови функции, които ще използваме за манипулация на стринговете.

Сравняване на низове по азбучен ред

Има множество начини за сравнение на символни низове и в зависимост от това какво точно ни е необходимо в конкретния случай, може да се възползваме от различните възможности на класа string.

Сравнение за еднаквост

Ако условието изисква да сравним два символни низа и да установим дали стойностите им са еднакви или не, удобен метод е методът Equals(…), който работи еквивалентно на оператора ==.  Той връща булев резултат със стойност true, ако низовете имат еднакви стойности, и false, ако те са различни. Методът Equals(…) проверява за побуквено равенство на стойностите на низовете, като прави разлика между малки и главни букви. Т.е. сравняването на "c#" и "C#" с метода Equals(…) ще върне стойност false. Нека разгледаме един пример:

string word1 = "C#";

string word2 = "c#";

Console.WriteLine(word1.Equals("C#"));

Console.WriteLine(word1.Equals(word2));

Console.WriteLine(word1 == "C#");

Console.WriteLine(word1 == word2);

 

// Console output:

// True

// False

// True

// False

В практиката често ще ни интересува самото текстово съдържание при сравнение на два низа, без значение от регистъра (casing) на буквите. За да игнорираме разликата между малки и главни букви при сравнението на низове можем да използваме Equals(…) с пара­метър StringComparison. CurrentCultureIgnoreCase. Така в долния пример при сравнение на "C#" със "c#" методът ще върне стойност true:

Console.WriteLine(word1.Equals(word2,

      StringComparison.CurrentCultureIgnoreCase));

// True

StringComparison.CurrentCultureIgnoreCase е константа от изброения тип StringComparison. Какво е изброен тип и как можем да го използва­ме, ще научим в темата "Дефиниране на класове".

Сравнение на низове по азбучен ред

Вече стана ясно как сравняваме низове за еднаквост, но как ще устано­вим лексикографската подредба на няколко низа? Ако пробваме да използваме операторите < и >, които работят отлично за сравнение на числа, ще установим, че те не могат да се използват за стрингове.

Ако искаме да сравним две думи и да получим информация коя от тях е преди другата, според азбучния ред на буквите в нея, на помощ идва методът CompareTo(…). Той ни дава възможност да сравняваме стой­ностите на два символни низа и да установяваме лексикографската им наредба. За да бъдат два низа с еднакви стойности, те трябва да имат една и съща дължина (брой символи) и изграждащите ги символи трябва съответно да си съвпадат. Например низовете "give" и "given" са различни, защото имат различна дължина, а "near" и "fear" се различават по първия си символ.

Методът CompareTo(…) от класа String връща отрицателна стойност, 0 или положителна стойност в зависимост от лексико­графската подредба на двата низа, които се сравняват. Отрицателна стойност означава, че първият низ е лексикографски преди втория, нула означава, че двата низа са еднакви, а положителна стойност означава, че вторият низ е лексикографски преди първия. За да си изясним по-добре как се сравняват лексикографски низове, нека разгле­даме няколко примера:

string score = "sCore";

string scary = "scary";

Console.WriteLine(score.CompareTo(scary));

Console.WriteLine(scary.CompareTo(score));

Console.WriteLine(scary.CompareTo(scary));

 

// Console output:

// 1

// -1

// 0

Първият експеримент е извикването на метода CompareTo(…) на низа score, като подаден параметър е променливата scary. Първият символ връща знак за равенство. Тъй като методът не игнорира регистъра за малки и главни букви, още във втория символ открива несъот­ветствие (в първия низ е "C", а във втория "c"). Това е достатъчно за определяне на подредбата на низовете и CompareTo(…) връща +1. Извикването на същия метод с разменени места на низовете връща -1, защото тогава отправната точка е низът scary. Последното му извикване логично връща 0, защото сравняваме scary със себе си.

Ако трябва да сравняваме низове лексикографски, игнорирайки регистъра на буквите, можем да използваме string.Compare(string strA, string strB, bool ignoreCase). Това е стати­чен метод, който действа по същия начин както CompareTo(…), но има опция ignoreCase за игнориране на регистъра за главни и малки букви. Нека разгледаме метода в действие:

string alpha = "alpha";

string score1 = "sCorE";

string score2 = "score";

 

Console.WriteLine(string.Compare(alpha, score1, false));

Console.WriteLine(string.Compare(score1, score2, false));

Console.WriteLine(string.Compare(score1, score2, true));

Console.WriteLine(string.Compare(score1, score2,

      StringComparison.CurrentCultureIgnoreCase));

// Console output:

// -1

// 1

// 0

// 0

В последния пример методът Compare(…) приема като трети параметър StringComparison.CurrentCultureIgnoreCase – вече познатата от метода Equals(…) константа, чрез която също можем да сравняваме низове, без да отчитаме разликата между главни и малки букви.

Забележете, че според методите Compare(…) и CompareTo(…) малките латински букви са лексикографски преди главните. Коректността на това правило е доста спорна, тъй като в Unicode таблицата главните букви са преди малките. Например според стандарта Unicode буквата "A" има код 65, който е по-малък от кода на буквата "a" (97).

Следователно, трябва да имате предвид, че лексикографското сравнение не следва подредбата на буквите в Unicode таблицата и при него могат да се наблюдават и други аномалии породени от особености на текущата култура.

clip_image003

Когато искате просто да установите дали стойностите на два символни низа са еднакви или не, използвайте метода Equals(…) или оператора ==. Методите CompareTo(…) и string.Compare(…) са проектирани за употреба при лексикографска подредба на низове и не трябва да се използват за проверка за еднаквост.

Операторите == и !=

В езика C# операторите == и != за символни низове работят чрез вът­решно извикване на Equals(…). Ще прегледаме примери за използването на тези два оператора с променливи от тип символни низове:

string str1 = "Hello";

string str2 = str1;

Console.WriteLine(str1 == str2);

// Console output:

// True

Сравнението на съвпадащите низове str1 и str2 връща стойност true. Това е напълно очакван резултат, тъй като насочваме променливата str2 към мястото в динамичната памет, което е запазено за променливата str1. Така двете променливи имат един и същ адрес и проверката за равенство връща истина. Ето как изглежда паметта с двете променливи:

clip_image005

Да разгледаме още един пример:

string hel = "Hel";

string hello = "Hello";

string copy = hel + "lo";

Console.WriteLine(copy == hello);

// True

Обърнете внимание, че сравнението е между низовете hello и copy. Първата променлива директно приема стойността "Hello". Втората полу­чава стойността си като резултат от съединяването на променлива и литерал, като крайният резултат е еквивалентен на стойността на първата променлива. В този момент двете променливи сочат към различни области от паметта, но съдържанието на съответните блокове памет е еднакво. Сравнението, направено с оператора == връща резултат true, въпреки че двете променливи сочат към различни области от паметта. Ето как изглежда паметта в този момент:

clip_image007

Оптимизация на паметта при символни низове

Нека видим следния пример:

string hello = "Hello";

string same = "Hello";

Създаваме първата променлива със стойност "Hello". Създаваме и втората променлива със стойност същия литерал. Логично е при създаването на променливата hello да се задели място в динамичната памет, да се запише стойността и променливата да сочи към въпросното място. При създаването на same също би трябвало да се създаде нова област, да се запише стойността и да се насочи препратката.

Истината обаче е, че съществува оптимизация в C# компилатора и в CLR, която спестява създаването на дублирани символни низове в паметта. Тази оптимизация се нарича интерниране на низовете (strings interning) и благодарение на нея двете променливи в паметта ще сочат към един и същ общ блок от паметта. Това намалява разхода на памет и оптимизира някои операции, например сравнението на такива напълно съвпадащи низове. Те се запис­ват в паметта по следния начин:

clip_image009

Когато инициализираме променлива от тип string с низов литерал, скрито от нас дина­мичната памет се обхожда и се прави проверка дали такава стойност вече съществува. Ако съществува, новата променлива просто се пренасочва към нея. Ако не, заделя се нов блок памет, стойността се записва в него и референцията се препраща да сочи към новия блок. Интернирането на низове в .NET е възможно, защото низовете по концепция са неизменими и няма опасност някой да промени областта, сочена от няколко стрингови променливи едновременно.

Когато не инициализираме низовете с литерали, не се ползва интерни­ране. Все пак, ако искаме да използваме интерниране изрично, можем да го направим чрез метода Intern(…):

string declared = "Intern pool";

string built = new StringBuilder("Intern pool").ToString();

string interned = string.Intern(built);

Console.WriteLine(object.ReferenceEquals(declared, built));

Console.WriteLine(object.ReferenceEquals(declared, interned)); // Output:

// False

// True

Ето и състоянието на паметта в този момент:

clip_image011

В примера ползвахме статичния метод Object.ReferenceEquals(…), който сравнява два обекта в паметта и връща дали сочат към един и същи блок памет. Ползвахме и класа StringBuilder, който служи за ефективно построяване на низове. Кога и как се ползва StringBuilder ще обясним в детайли след малко, а сега нека се запознаем с основните операции върху низове.

Операции за манипулация на символни низове

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

clip_image003[1]

Символните низове са неизменими! Всяка промяна на променлива от тип string създава нов низ, в който се записва резултатът. По тази причина операциите, които прилагате върху символните низове, връщат като резултат референция към получения резултат.

Възможна е и обработката на символни низове без създаването на нови обекти в паметта при всяка модификация, но за целта трябва да се използва класът StringBuilder, с който ще се запознаем след малко.

Долепване на низове (конкатенация)

Долепването на символни низове и получаването на нов низ като резултат, се нарича конкатенация. То може да бъде извършено по няколко начина: чрез метода Concat(…) или чрез операторите + и +=.

Пример за използване на функцията Concat(…):

string greet = "Hello, ";

string name = "reader!";

string result = string.Concat(greet, name);

Извиквайки метода, ще долепим променливата name, която е подадена като аргумент, към променливата greet. Резултатният низ ще има стой­ност "Hello, reader!".

Вторият вариант за конкатенация е чрез операторите + и +=. Горният пример може да реализираме още по следния начин:

string greet = "Hello, ";

string name = "reader!";

string result = greet + name;

И в двата случая в паметта тези променливи ще се представят по следния начин:

clip_image013

Обърнете внимание, че долепванията на низове не променят стойностите на съществува­щите променливи, а връщат нова променлива като резул­тат. Ако опитаме да долепим два стринга, без да ги запазим в променлива, промените нямат да бъдат съхранени. Ето една типична грешка:

string greet = "Hello, ";

string name = "reader!";

string.Concat(greet, name);

В горния пример двете променливи се слепват, но резултатът от слеп­ването не се съхранява никъде и се губи.

Ако искаме да добавим някаква стойност към съществуваща променлива, например променливата result, с познатите ни оператори можем да ползваме следния код:

result = result + " How are you?";

За да си спестим повторното писане на декларираната по-горе промен­лива, можем да използваме оператора +=:

result += " How are you?";

И в двата случая резултатът ще бъде един и същ: "Hello, reader! How are you?".

Към символните низове можем да долепим и други данни, които могат да бъдат представени в текстов вид. Възможна е конкатенацията с числа, символи, дати и др. Ето един пример:

string message = "The number of the beast is: ";

int beastNum = 666;

string result = message + beastNum;

// The number of the beast is: 666

Както виждаме от горния пример, няма проблем да съединяваме символни низове с други данни, които не са от тип string. Нека прегледаме още един, пълен пример за слепването на символни низове:

public class DisplayUserInfo

{

      static void Main()

      {

            string firstName = "Svetlin";

            string lastName = "Nakov";

            string fullName = firstName + " " + lastName;

 

            int age = 28;

            string nameAndAge = "Name: " + fullName + "\nAge: " + age;

            Console.WriteLine(nameAndAge);

      }

}

// Console output:

// Name: Svetlin Nakov

// Age: 28

Преминаване към главни и малки букви

Понякога имаме нужда да променим съдържанието на символен низ, така че всички символи в него да бъдат само с главни или малки букви. Двата метода, които биха ни свършили работа в случая, са ToLower(…) и ToUpper(…). Първият от тях конвертира всички главни букви към малки:

string text = "All Kind OF LeTTeRs";

Console.WriteLine(text.ToLower());

// all kind of letters

От примера се вижда, че всички главни букви от текста сменят регистъра си и целият текст преминава изцяло в малки букви. Такова преминаване към малки букви е удобно например при съхраняване на потребителските имена (username) в различни онлайн системи. При регистрация потреби­телите могат да ползват смесица от главни и малки букви, но системата след това може да ги направи всичките малки за да ги унифицира и да избегне съвпадения по букви с разлики в регистъра.

Ето още един пример. Искаме да сравним въведен от потребителя вход и не сме сигурни по какъв точно начин е написан той – с малки или главни букви или смесено. Един възможен подход е да уеднаквим регистъра на буквите и да го сравним с дефинираната от нас константа. По този начин не правим разлика за малки и главни букви. Например ако имаме входен панел на потребителя, в който въвеждаме име и парола, и няма значение дали паролата е написана с малки или главни букви, може да направим подобна проверка на паролата:

string pass1 = "Parola";

string pass2 = "PaRoLa";

string pass3 = "parola";

Console.WriteLine(pass1.ToUpper() == "PAROLA");

Console.WriteLine(pass2.ToUpper() == "PAROLA");

Console.WriteLine(pass3.ToUpper() == "PAROLA");

 

// Console output:

// True

// True

// True

В примера сравняваме три пароли с еднакво съдържание, но с различен регистър. При проверката съдържанието им се проверява дали съвпада побуквено със символния низ "PAROLA". Разбира се, горната проверка бихме могли да направим и чрез метода Equals(…)във варианта с игнори­ране на регистъра на символите, който вече разгледахме.

Търсене на низ в друг низ

Когато имаме символен низ със зададено съдържание, често се налага да обработим само част от стойността му. .NET платформата ни предоставя два метода за търсене на низ в друг низ: IndexOf(…) и LastIndexOf(…). Те претърсват даден символен низ и прове­ряват дали подаденият като параметър подниз се среща в съдържанието му. Връщаният резултат от методите е цяло число. Ако резултатът е неот­рицателна стойност, тогава това е позицията, на която е открит първият символ от подниза. Ако методът върне стойност -1, това означава, че поднизът не е открит. Напомняме, че в C# индексите на символите в низовете започват от 0.

Методите IndexOf(…) и LastIndexOf(…) претърсват съдържанието на текстова последователност, но в различна посока. Търсенето при първия метод започва от началото на низа в посока към неговия край, а при втория метод – търсенето се извършва отзад напред. Когато се интересу­ваме от първото срещнато съвпадение, използваме IndexOf(…). Ако искаме да претърсваме низа от неговия край (например за откриване на последната точка в името на даден файл или последната наклонена черта в URL адрес), ползваме LastIndexOf(…).

При извикването на IndexOf(…) и LastIndexOf(…) може да се подаде и втори параметър, който указва от коя позиция да започне търсенето. Това е полезно, ако искаме да претърсваме част от даден низ, а не целия низ.

Търсене в символен низ – пример

Да разгледаме един пример за използване на метода IndexOf(…):

string book = "Introduction to C# book";

int index = book.IndexOf("C#");

Console.WriteLine(index);

// index = 16

В примера променливата book има стойност "Introduction to C# book". Търсенето на подниза "C#" в тази променлива ще върне стойност 16, защото поднизът ще бъде открит в стойността на отправната променлива и първият символ "C" от търсената дума се намира на 16-та позиция.

Търсене с IndexOf(…) – пример

Нека прегледаме още един, по-подробен пример за търсенето на отделни символи и символни низове в текст:

string str = "C# Programming Course";

 

int index = str.IndexOf("C#"); // index = 0

index = str.IndexOf("Course"); // index = 15

index = str.IndexOf("COURSE"); // index = -1

index = str.IndexOf("ram");    // index = 7

index = str.IndexOf("r");      // index = 4

index = str.IndexOf("r", 5);   // index = 7

index = str.IndexOf("r", 10);  // index = 18

Ето как изглежда в паметта символният низ, в който търсим:

clip_image015

Ако обърнем внимание на резултата от третото търсене, ще забележим, че търсенето на думата "COURSE" в текста връща резултат -1, т.е. няма наме­рено съответствие. Въпреки че думата се намира в текста, тя е написана с различен регистър на буквите. Методите IndexOf(…) и LastIndexOf(…) правят разлика между малки и главни букви. Ако искаме да игнорираме тази разлика, можем да запишем текста в нова променлива и да го превърнем към текст с изцяло малки или изцяло главни букви, след което да извършим търсене в него, независещо от регистъра на буквите.

Всички срещания на дадена дума – пример

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

Ето един пример за използването на IndexOf(…) по дадена дума и нача­лен индекс: откриване на всички срещания на думата "C#" в даден текст:

string quote = "The main intent of the \"Intro C#\"" +

      " book is to introduce the C# programming to newbies.";

string keyword = "C#";

int index = quote.IndexOf(keyword);

 

while (index != -1)

{

      Console.WriteLine("{0} found at index: {1}", keyword, index);

      index = quote.IndexOf(keyword, index + 1);

}

Първата стъпка е да направим търсене за ключовата дума "C#". Ако думата е открита в текста (т.е. връщаната стойност е различна от -1), извеждаме я на конзолата и продължаваме търсенето надясно, започ­вайки от пози­цията, на която сме открили думата, увеличена с единица. Повтаряме действи­ето, докато IndexOf(…) върне стойност -1.

Забележка: ако на последния ред пропуснем задаването на начален индекс, то търсенето винаги ще започва отначало и ще връща една и съща стойност. Това ще доведе до зацикляне на програмата ни. Ако пък търсим директно от индекса, без да увеличаваме с единица, ще попадаме всеки път отново и отново на последния резултат, чийто индекс сме вече намерили. Ето защо правилното търсене на следващ резултат трябва да започва от начална позиция index + 1.

Извличане на част от низ

За момента знаем как да проверим дали даден подниз се среща в даден текст и на кои позиции се среща. Как обаче да извлечем част от низа в отделна променлива?

Решението на проблема ни е методът Substring(…). Използвайки го, можем да извлечем дадена част от низ (подниз) по зададени начална позиция в текста и дължина. Ако дължината бъде пропусната, ще бъде направена извадка от текста, започваща от началната позиция до неговия край.

Следва пример за извличане на подниз от даден низ:

string path = "C:\\Pics\\Rila2010.jpg";

string fileName = path.Substring(8, 8);

// fileName = "Rila2010"

Променливата, която манипулираме, е path. Тя съдържа пътя до файл от файловата ни система. За да присвоим името на файла на нова промен­лива, използваме Substring(8, 8) и взимаме последователност от 8 символи, стартираща от позиция 8, т.е. символите на позиции от 8 до 15.

clip_image003[2]

Извикването на метода Substring(startIndex, length) из­влича подниз от даден стринг, който се намира между startIndex и (startIndex + length – 1) включи­телно. Символът на позицията startIndex + length не се взима предвид! Например ако посочим Substring(8, 3), ще бъдат извлечени символите между индекс 8 и 10 включи­телно.

Ето как изглеждат символите, които съставят текста, от който извличаме подниз:

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

C

:

\\

P

i

c

s

\\

R

i

l

a

2

0

1

0

.

j

p

g

Придържайки се към схемата, извикваният метод трябва да запише символите от позиции от 8 до 15 включително (тъй като последният индекс не се включва), а именно "Rila2010".

Да разгледаме една по-интересна задача. Как бихме могли да изведем името на файла и неговото разширение? Тъй като знаем как се записва път във файловата система, можем да процедираме по следния план:

-     Търсим последната обратна наклонена черта в текста;

-     Записваме позицията на последната наклонена черта;

-     Извличаме подниза, започващ от получената позиция + 1.

Да вземем отново за пример познатия ни path. Ако нямаме информация за съдържанието на променливата, но знаем, че тя съдържа път до файл, може да се придържаме към горната схема:

string path = "C:\\Pics\\Rila2010.jpg";

int index = path.LastIndexOf("\\");

// index = 7

string fullName = path.Substring(index + 1);

// fullName = "Rila2010.jpg"

Разцепване на низ по разделител

Един от най-гъвкавите методи за работа със символни низове е Split(…). Той ни дава възможност да разцепваме един низ по разделител или масив от възможни разделители. Например можем да обработваме променлива, която има следното съдържание:

string listOfBeers = "Amstel, Zagorka, Tuborg, Becks";

Как можем да отделим всяка една бира в отделна променлива или да запишем всички бири в масив? На пръв поглед може да изглежда трудно – трябва да търсим с IndexOf(…) за специален символ, след това да отделяме подниз със Substring(…), да итерираме всичко това в цикъл и да записваме резултата в дадена променлива. Тъй като разделянето на низ по разделител е основна задача от текстообработката, в .NET Framework има готови методи за това.

Разделяне на низ по множество от разделители – пример

По-лесния и гъвкав начин да разрешим проблема е следният:

char[] separators = new char[] {' ', ',', '.'};

string[] beersArr = listOfBeers.Split(separators);

Използвайки вградената функционалност на метода Split(…) от класа String, ще разделим съдържанието на даден низ по масив от символи-разделители, които са подадени като аргумент на метода. Всички подни­зове, между които присъстват интервал, запетая или точка, ще бъдат отделени и записани в масива beersArr.

Ако обходим масива и изведем елементите му един по един, резултатите ще бъдат: "Amstel", "", "Zagorka", "", "Tuborg", "" и "Becks". Получаваме 7 резултата, вместо очакваните 4. Причината е, че при разде­лянето на текста се откриват 3 подниза, които съдържат два разделителни символа един до друг (например запетая, последвана от интервал). В този случай празният низ между двата разделителя също е част от връщания резултат.

Как да премахнем празните елементи?

Ако искаме да игнорираме празните низове, едно възможно разрешение е да правим проверка при извеждането им:

foreach (string beer in beersArr)

{

      if (beer != "")

      {

            Console.WriteLine(beer);

      }

}

С този подход обаче не премахваме празните низове от масива, а просто не ги отпечатваме. Затова можем да променим аргументите, които пода­ваме на метода Split(…), като подадем една специална опция:

string[] beersArr = listOfBeers.Split(

      separators, StringSplitOptions.RemoveEmptyEntries);

След тази промяна масивът beersArr ще съдържа 4 елемента – четирите думи от променливата listOfBeers.

clip_image003[3]

При разделяне на низове добавяйки като втори параметър константата StringSplitOptions.RemoveEmptyEntries ние ин­структираме метода Split(…) да работи по следния начин: "Върни всички поднизове от променливата, които са разде­лени от интервал, запетая или точка. Ако срещнеш два или повече съседни разделителя, считай ги за един".

Замяна на подниз с друг

Текстообработката в .NET Framework предлага готови методи за замяна на един подниз с друг. Например ако сме допуснали една и съща техническа грешка при въвеждане на email адреса на даден потребител в официален документ, можем да го заменим с помощта на метода Replace(…):

string doc = "Hello, [email protected], " +

      "you have been using [email protected] in your registration.";

string fixedDoc =

      doc.Replace("[email protected]", "[email protected]");

Console.WriteLine(fixedDoc);

 

// Console output:

// Hello, [email protected], you have been using

// [email protected] in your registration.

Както се вижда от примера, методът Replace(…) замества всички среща­ния на даден подниз с даден друг подниз, а не само първото.

Регулярни изрази

Регулярните изрази (regular expressions) са мощен инструмент за обработка на текст и позволяват търсене на съвпадения по шаблон (pattern). Пример за шаблон е [A-Z0-9]+, който означава непразна поредица от главни латински букви и цифри. Регулярните изрази позво­ляват по-лесна и по-прецизна обработка на текстови данни: извличане на определени ресурси от текстове, търсене на телефонни номера, откри­ване на електронна поща в текст, разделяне на всички думи в едно изречение, валидация на данни и т.н.

Регулярни изрази – пример

Ако имаме служебен документ, който се използва само в офиса, и в него има лични данни, трябва да ги цензурираме, преди да ги пратим на клиента. Например можем да цензурираме всички номера на мобилни телефони и да ги заместим със звездички. Използвайки регулярните изрази, това би могло да стане по следния начин:

string doc = "Smith's number: 0898880022\nFranky can be " +

      " found at 0888445566.\nSteven’ mobile number: 0887654321";

string replacedDoc = Regex.Replace(

      doc, "(08)[0-9]{8}", "$1********");

Console.WriteLine(replacedDoc);

 

// Console output:

// Smith's number: 08********

// Franky can be  found at 08********.

// Steven' mobile number: 08********

Обяснение на аргументите на Regex.Replace()

В горния фрагмент от код използваме регулярен израз, с който откриваме всички телефонни номера в зададения ни текст и ги заменяме по шаблон. Използваме класа System.Text.RegularExpressions.Regex, който е пред­назначен за работа с регулярни изрази в .NET Framework. Променливата, която имитира документа с текстовите данни, е doc. В нея са записани няколко имена на клиенти заедно с тех­ните телефонни номера. Ако искаме да предпазим контактите от неправо­мерно използване и желаем да цензурираме телефонните номера, то може да заменим всички мобилни телефони със звездички. Приемайки, че телефоните са записани във формат: "08 + 8 цифри", методът Regex.Replace(…) открива всички съвпадения по дадения формат и ги замества с: "08********".

Регулярният израз, отговорен за откриването на номерата, е следният: "(08)[0-9]{8}". Той намира всички поднизове в текста, изградени от константата "08" и следвани от точно 8 символа в диапазона от 0 до 9. Примерът може да бъде допълнително подобрен за подбиране на номерата само от дадени мобилни оператори, за работа с телефони на чуждестранни мрежи и др., но в случая  използван опростен вариант.

Литералът "08" е заграден от кръгли скоби. Те служат за обособяване на отделна група в регулярния израз. Групите могат да бъдат използвани за обработка само на определена част от израза, вместо целия израз. В нашия пример, групата е използвана в замест­ването. Чрез нея откритите съвпадения се заместват по шаблон "$1********", т.е. текстът намерен от първата група на регулярния израз ($1) + последователни 8 звездички за цензурата. Тъй като дефинираната от нас група винаги е константа (08), то заместеният текст ще бъде винаги: 08********.

Настоящата тема няма за цел да обясни как се работи с регулярни изрази в .NET Framework, тъй като това е голяма и сложна материя, а само да обърне внимание на читателя, че регулярните изрази съществуват и са много мощно средство за текстообработка. Който се интересува повече, може да потърси статии, книги и самоучители, от които да разучи как се конструират регулярните изрази, как се търсят съвпадения, как се прави валидация, как се правят замествания по шаблон и т.н. По-конкретно препоръчваме да посетите сайтовете http://www.regular-expressions.info/ и http://regexlib.com/. Повече инфор­ма­ция за класовете, които .NET Framework предлага за работа с регулярни изрази и как точно се използват, може да бъде открита на адрес: http://msdn.microsoft.com/en-us/library/system.text.regularexpressions.regex%28VS.100%29.aspx.

Премахване на ненужни символи в началото и в края на низ

Въвеждайки текст във файл или през конзолата, понякога се появяват "паразитни" празни места (white-space) в началото или в края на текста – някой друг интервал или табулация, които да може да не се доловят на пръв поглед. Това може да не е съществено, но ако не валиди­раме потребителски данни, би било проблем от гледна точка на проверка съдържанието на входната информация. За решаване на проблема на помощ идва методът Trim(). Той се грижи именно за премахването на паразитните празни места в началото или края на даден символен низ. Празните места могат да бъдат интервали, табулация, нови редове и др.

Нека в променливата fileData сме прочели съдържанието на файл, в който е записано име на студент. Пишейки текста или преобръщайки го от един формат в друг може да са се появили паразитни празни места и тогава променливата ще изглежда по подобен начин:

string fileData = "   \n\n     Ivan Ivanov      ";

Ако изведем съдържанието на конзолата, ще получим 2 празни реда, последвани от няколко интервала, търсеното от нас име и още няколко допълнителни интервала в края. Можем да редуцираме информацията от променливата само до нужното ни име по следния начин:

string reduced = fileData.Trim();

Когато изведем повторно информацията на конзолата, съдържанието ще бъде "Ivan Ivanov", без нежеланите празни места.

Премахване на ненужни символи по зададен списък

Методът Trim(…) може да приема масив от символи, които искаме да премахнем от низа. Това може да направим по следния начин:

string fileData = "   111 $  %    Ivan Ivanov  ### s   ";

char[] trimChars = new char[] {' ', '1', '$', '%', '#', 's'};

string reduced = fileData.Trim(trimChars);

// reduced = "Ivan Ivanov"

Отново получаваме желания резултат "Ivan Ivanov".

clip_image003[4]

Обърнете внимание, че трябва да изброим всички символи, които искаме да премахнем, включително праз­ните интервали (интервал, табулация, нов ред и др.). Без наличието на ' ' в масива trimChars, нямаше да получим желания резултат!

Ако искаме да премахнем паразитните празни места само в началото или в края на низа, можем да използваме методите TrimStart(…) и TrimEnd(…):

string reduced = fileData.TrimEnd(trimChars);

// reduced = "   111 $  %    Ivan Ivanov"

Построяване на символни низове. StringBuilder

Както обяснихме по-горе, символните низове в C# са неизменими. Това означава, че всички корекции, приложени върху съществуващ низ, не го променят, а връщат като резултат нов символен низ. Например използва­нето на методите Replace(…), ToUpper(…), Trim(…) не променят низа, за който са извикани, а заделят нова област от паметта, в която се записва новото съдържание. Това има много предимства, но в някои случаи може да ни създаде проблеми с производителността.

Долепяне на низове в цикъл: никога не го правете!

Сериозен проблем с производителността може да срещнем, когато се опитаме да конкатенираме символни низове в цикъл. Проблемът е пряко свързан с обработката на низовете и динамичната памет, в която се съхраняват те. За да разберем как се получава недостатъчното бързодей­ствие при съединяване на низове в цикъл, трябва първо да разгледаме какво се случва при използване на оператора "+" за низове.

Как работи съединяването на низове?

Вече се запознахме с начините за съединяване на низове в C#. Нека сега разгледаме какво се случва в паметта, когато се съединяват низове. Да вземем за пример две променливи str1 и str2 от тип string, които имат стойности съответно "Super" и "Star". В хийпа (динамичната памет) има заделени две области, в които се съхраняват стойностите. Задачата на str1 и str2 е да пазят препратка към адресите в паметта, на които се намират записаните от нас данни. Нека създадем променлива result и й придадем стойността на другите два низа чрез долепяне. Фрагментът от код за създаването и дефинирането на трите променливи би изглеждал по следния начин:

string str1 = "Super";

string str2 = "Star";

string result = str1 + str2;

Какво ще се случи с паметта? Създаването на променливата result ще задели нова област от динамичната памет, в която ще запише резултата от str1 + str2, който е "SuperStar". След това самата променлива ще пази адреса на заделената област. Като резултат ще имаме три области в паметта, както и три референ­ции към тях. Това е удобно, но създаването на нова област, записването на стойност, създаването на нова променлива и реферира­нето й към паметта е времеотнемащ процес, който би бил проблем при многократното му повтаряне в цикъл.

За разлика от други езици за програмиране, в C# не е необходимо ръчното освобождаване на обектите, записани в паметта. Съществува специален механизъм, наречен garbage collector (система за почист­ване на паметта), който се грижи за изчистването на неизползваната памет и ресурси. Системата за почистване на паметта е отговорна за освобождаването на обектите в динамичната памет, когато вече не се използват. Създаването на много обекти, придру­жени с множество рефе­ренции в паметта, е вредно, защото така се запълва паметта и тогава автома­тично се налага изпълнение на garbage collector. Това отнема немалко време и забавя цялостното изпълнение на процеса. Освен това преместването на символи от едно място на паметта в друго, което се изпълнява при съединяване на низове, е бавно, особено ако низовете са дълги.

Защо долепянето на низове в цикъл е лоша практика?

Да приемем, че имаме за задача да запишем числата от 1 до 20 000 последователно едно до друго в променлива от тип string. Как можем да решим задачата с досегашните си знания? Един от най-лесните начини за имплементация е създаването на променливата, която съхранява числата, и завъртането на цикъл от 1 до 20 000, в който всяко число се долепва към въпросната променлива. Реализирано на C#, решението би изглеж­дало примерно така:

string collector = "Numbers: ";

for (int index = 1; index <= 20000; index++)

{

      collector += index;

}

Изпълнението на горния код ще завърти цикъла 20 000 пъти, като след всяко завъртане ще добавя текущия индекс към променливата collector. Стойността на променливата collector след края на изпълнението ще бъде: "Numbers: 12345678910111213141516..." (останалите числа от 17 до 20 000 са заместени с многоточие, с цел относителна представа за резултата).

Вероятно не ви е направило впечатление забавянето при изпълнение на фрагмента. Всъщност използването на конкатенацията в цикъл е забавила значително нормалния изчислителен процес и на средностатистически компютър (от януари 2010 г.) итерацията на цикъла отнема 1-3 секунди. Потребителят на програмата ни би бил доста скептично настроен, ако се налага да чака няколко секунди за нещо елементарно, като слепване на числата от 1 до 20 000. Освен това в случая 20 000 е само примерна крайна точка. Какво ли ще бъде забавянето, ако вместо 20 000, потреби­телят има нужда да долепи числата до 200 000? Пробвайте!

Конкатениране в цикъл с 200000 итерации - пример

Нека развием горния пример. Първо, ще променим крайната точка на цикъла от 20 000 на 200 000. Второ, за да отчетем правилно времето за изпълнение, ще извеждаме на конзолата текущата дата и час преди и след изпълнението на цикъла. Трето, за да видим, че променливата съдържа желаната от нас стойност, ще изведем част от нея на конзолата. Ако искате да се уверите, че цялата стойност е запаметена, може да премахнете прилагането на метода Substring(…), но самото отпечатване в този случай също ще отнеме доста време.

Крайният вариант на примера би изглеждал така:

class NumbersConcatenator

{

      static void Main()

      {

            Console.WriteLine(DateTime.Now);

 

            string collector = "Numbers: ";

            for (int index = 1; index <= 200000; index++)

            {

                  collector += index;

            }

 

            Console.WriteLine(collector.Substring(0, 1024));

            Console.WriteLine(DateTime.Now);

      }

}

При изпълнението на примера на конзолата се извеждат дата и час на стартиране на програмата, отрязък от първите 1024 символа от промен­ливата, както и дата и час на завършване на програмата. Причината да покажем само първите 1024 символа е, че искаме да измерим само времето за изчисленията без времето за отпечатване на резултата. Нека видим примерния изход от изпълнението:

clip_image017

Със зелена линия е подчертан часът в началото на изпълнението на прог­рамата, а с червена – нейният край. Обърнете внимание на времето за изпълнение – почти 6 минути! Подобно изчакване е недопустимо за подобна задача и не само ще изнерви потребителя, а ще го накара да спре програмата без да я изчака до край.

Обработка на символни низове в паметта

Проблемът с времеотнемащата обработка на цикъла е свързан именно с работата на низовете в паметта. Всяка една итерация създава нов обект в динамичната памет и насочва референцията към него. Процесът изисква определено физическо време.

На всяка стъпка се случват няколко неща:

1.  Заделя се област от паметта за записване на резултата от долепва­нето на поредното число. Тази памет се използва само временно, докато се изпълнява долепването, и се нарича буфер.

2.  Премества се старият низ в новозаделения буфер. Ако низът е дълъг (примерно 1 MB или 10 MB), това може да е доста бавно!

3.  Долепя се поредното число към буфера.

4.  Буферът се преобразува в символен низ.

5.  Старият низ, както и временният буфер, остават неизползвани и по някое време биват унищожени от системата за почистване на паметта (garbage collector). Това също може да е бавна операция.

Много по-елегантен и удачен начин за конкатениране на низове в цикъл е използването на класа StringBuilder. Нека видим как става това.

Построяване и промяна на низове със StringBuilder

StringBuilder е клас, който служи за построяване и промяна на символни низове. Той преодолява проблемите с бързодействието, които възникват при конкатениране на низове от тип string. Класът е изграден под формата на масив от символи и това, което трябва да знаем за него е, че информацията в него може свободно да се променя. Промените, които се налагат в променливите от тип StringBuilder, се извършват в една и съща област от паметта (буфер), което спестява време и ресурси. За промяната на съдържанието не се създава нов обект, а просто се променя текущият.

Нека пренапишем горния код, в който слепвахме низове в цикъл. Ако си спомняте, операцията отне 6 минути. Нека измерим колко време ще отнеме същата операция, ако използваме StringBuilder:

class ElegantNumbersConcatenator

{

      static void Main()

      {

            Console.WriteLine(DateTime.Now);

 

            StringBuilder sb = new StringBuilder();

            sb.Append("Numbers: ");

 

            for (int index = 1; index <= 200000; index++)

            {

                  sb.Append(index);

            }

 

            Console.WriteLine(sb.ToString().Substring(0, 1024));

            Console.WriteLine(DateTime.Now);

      }

}

Примерът е базиран на предходния, със съвсем леки корекции. Връща­ният резултат е същият, а какво ще кажете за времето за изпълнение?

clip_image019

Необходимото време за слепване на 200 000 символа със StringBuilder е вече по-малко от секунда!

Обръщане на низ на обратно – пример

Да разгледаме друг пример, в който искаме да обърнем съществуващ символен низ на обратно (отзад напред). Например ако имаме низа "abcd", върнатият резултат трябва да бъде "dcba". Взимаме първоначал­ния низ, обхождаме го отзад-напред символ по символ и добавяме всеки символ към променлива от тип StringBuilder:

public class WordReverser

{

      public static void Main()

      {

            string text = "EM edit";

            string reversed = ReverseText(text);

            Console.WriteLine(reversed);

 

            // Console output:

            // tide ME

      }

 

      public static string ReverseText(string text)

      {

            StringBuilder sb = new StringBuilder();

            for (int i = text.Length - 1; i >= 0; i--)

                  sb.Append(text[i]);

            return sb.ToString();

      }

}

В демонстрацията имаме променливата text, която съдържа стойността "EM edit". Подаваме променливата на метода ReverseText(…) и приемаме новата стойност в променлива с име reversed. Методът, от своя страна, обхожда символите от променливата в обратен ред и ги записва в нова променлива от тип StringBuilder, но вече наредени обратно. В крайна сметка резултатът е "tide ME".

Как работи класът StringBuilder?

Класът StringBuilder представлява реализация на символен низ в C#, но различна от тази на класа string. За разлика от познатите ни вече сим­волни низове, обектите на класа StringBuilder не са неизменими, т.е. редак­циите не налагат създаването на нов обект в паметта. Това нама­лява излишното прехвърляне на данни в паметта при извършване на основни операции, като например долепяне на низ в края.

StringBuilder поддържа буфер с определен капацитет (по подразбиране 16 символа). Буферът е реализиран под формата на масив от символи, който е предоставен на програмиста с удобен интерфейс – методи за лесно и бързо добавяне и редактиране на елементите на низа. Във всеки един момент част от символите в буфера се използват, а останалите стоят в резерв. Това дава възможност добавянето да работи изклю­чително бързо. Останалите операции също работят по-бързо, отколкото при класа string, защото промените не създават нов обект.

Нека създадем обект от класа StringBuilder с буфер от 15 символа. Към него ще добавим символния низ: "Hello,C#!". Получаваме следния код:

StringBuilder sb = new StringBuilder(15);

sb.Append("Hello,C#!");

След създаването на обекта и записването на стойността в него, той ще изглежда по следния начин:

clip_image021

Оцветените елементи са запълнената част от буфера с въведеното от нас съдържание. Обикновено при добавяне на нов символ към променливата не се създава нов обект в паметта, а се използва заделеното вече, но неизползвано прост­ранство. Ако целият капа­цитет на буфера е запълнен, тогава вече се заделя нова област в динамичната памет с удвоен размер (текущия капацитет, умножен по 2) и данните се прехвърлят в нея. След това може отново да се добавят спокойно символи и символни низове, без нужда от непрекъснатото заделяне на памет.

StringBuilder – по-важни методи

Класът StringBuilder ни предоставя набор от методи, които ни помагат за лесно и ефективно редактиране на текстови данни и построяване на текст. Вече срещнахме някои от тях в примерите. По-важните са:

-     StringBuilder(int capacity) – конструктор с параметър начален капацитет. Чрез него може предварително да зададем размера на буфера, ако имаме приблизителна информация за броя итерации и слепвания, които ще се извършат. Така спестяваме излишни заделя­ния на динамична памет.

-     Capacity – връща размера на целия буфер (общият брой заети и свободни позиции в буфера).

-     Length – връща дължината на записания низ в променливата (броя заети позиции в буфера).

-     Индексатор [int index] – връща символа на указаната позиция.

-     Append(…) – слепва низ, число или друга стойност след последния записан символ в буфера.

-     Clear(…) – премахва всички символи от буфера (изтрива го).

-     Remove(int startIndex, int length) – премахва (изтрива) низ от буфера по дадена начална позиция и дължина.

-     Insert(int offset, string str) – вмъква низ на дадена позиция.

-     Replace(string oldValue, string newValue) – замества всички срещания на даден подниз с друг подниз.

-     ТoString() – връща съдържанието на StringBuilder обекта във вид на string.

Извличане на главните букви от текст – пример

Следващата задача е да извлечем всички главни букви от даден текст. Можем да я реализираме по различни начини – използвайки масив и брояч и пълнейки масива с всички открити главни букви; създавайки обект от тип string и долепвайки главните букви една по една към него; използвайки класа StringBuilder.

Спирайки се на варианта за използване на масив, имаме проблем: не знаем какъв да бъде размерът на масива, тъй като предварително нямаме идея колко са главните букви в текста. Може да създадем масива толкова голям, колкото е текста, но по този начин хабим излишно място в паметта и освен това трябва да поддържаме брояч, който пази до къде е пълен масива.

Друг вариант е използването на променлива от тип string. Тъй като ще обходим целия текст и ще долепваме всички букви към променливата, вероятно е отново да загубим производителност заради конка­тенирането на символни низове.

StringBuilder – правилното решение

Най-уместното решение на поставената задача отново е използването на StringBuilder. Можем да започнем с празен StringBuilder, да итери­раме по буквите от зададения текст символ по символ, да проверяваме дали текущият символ от итерацията е главна буква и при положителен резул­тат да долепваме символа в края на нашия StringBuilder. Накрая можем да върнем натрупания резултат, който взимаме с извикването на метода ToString(). Следва примерна реализация:

public static string ExtractCapitals(string str)

{

      StringBuilder result = new StringBuilder();

      for (int i = 0; i < str.Length; i++)

      {

            char ch = str[i];

            if (char.IsUpper(ch))

            {

                  result.Append(ch);

            }

      }

      return result.ToString();

}

Извиквайки метода ExtractCapitals(…) и подавайки му зададен текст като параметър, връщаната стойност е низ от всички главни букви в текста, т.е. началният низ с изтрити от него всички символи, които не са главни букви. За проверка дали даден символ е главна буква използваме char.IsUpper(…) – метод от стандартните класове в .NET. Можете да разгледате документацията за класа char, защото той предлага и други полезни методи за обработка на символи.

Форматиране на низове

.NET Framework предлага на програмиста механизми за форматиране на символни низове, числа и дати. Вече се запознахме с някои от тях в темата "Вход и изход от конзолата". Сега ще допълним знанията си с методите за форматиране и преобразуване на низове на класа string.

Служебният метод ToString(…)

Една от интересните концепции в .NET, е че практически всеки обект на клас, както и примитивните промен­ливи, могат да бъдат предста­вяни като текстово съдържание. Това се извършва чрез метода ToString(…), който присъства във всички .NET обекти. Той е заложен в дефиницията на класа object – базовият клас, който наследяват пряко или непряко всички .NET типове данни. По този начин дефиницията на метода се появява във всеки един клас и можем да го ползваме, за да изведем във вид на някакъв текст съдържанието на всеки един обект.

Методът ToString(…) се извиква автоматично, когато извеждаме на конзо­лата обекти от различни класове. Например когато печатаме дати, скрито от нас подадената дата се преобразува до текст чрез извикване на ToString(…):

DateTime currentDate = DateTime.Now;

Console.WriteLine(currentDate);

// 10.1.2010 г. 13:34:27 ч.

Когато подаваме currentDate като параметър на метода WriteLine(…), нямаме точна декларация, която обработва дати. Методът има конкретна реализация за всички примитивни типове и символни низове. За всички останали обекти WriteLine(…) извиква метода им ToString(…), който първо ги преобразува до текст, и след това извежда полученото текстово съдържание. Реално примерният код по-горе е еквивалентен на следния:

DateTime currentDate = DateTime.Now;

Console.WriteLine(currentDate.ToString());

Имплементацията по подразбиране на метода ToString(…) в класа object връща пълното име на съответния клас. Всички класове, които не преде­финират изрично поведението на ToString(…), използват именно тази имплемен­тация. Повечето класове в C# имат собствена имплементация на метода, представяща четимо и раз­бираемо съдържанието на съответния обект във вид на текст. Например при преобразуване на число към текст се ползва стандартния за текущата култура формат на числата. При преобразуване на дата към текст също се ползва стандартния за текущата култура формат на датите.

Използване на String.Format(…)

String.Format(…) е статичен метод, чрез който можем да форматираме текст и други данни по шаблон (форматиращ низ). Шаблоните съдържат текст и декларирани параметри (placeholders) и служат за получаване на форматиран текст след заместване на параметрите от шаблона с конкретни стойности. Може да се направи директна асоциация с метода Console.WriteLine(…), който също форма­тира низ по шаблон:

Console.WriteLine("This is a template from {0}", "Ivan");

Как да ползваме метода String.Format(…)? Нека разгледаме един пример, за да си изясним този въпрос:

DateTime date = DateTime.Now;

string name = "Svetlin Nakov";

string task = "Telerik Academy courses";

string location = "his office in Sofia";

 

string formattedText = String.Format(

      "Today is {0:dd.MM.yyyy} and {1} is working on {2} in {3}.",

      date, name, task, location);

Console.WriteLine(formattedText);

 

// Output: Today is 22.10.2010 and Svetlin Nakov is working on

// Telerik Academy courses in his office in Sofia.

Както се вижда от примера, форматирането чрез String.Format() из­ползва пара­метри от вида {0}, {1}, и т.н. и приема форматиращи низове (като например :dd.MM.yyyy). Методът приема като първи пара­метър форматиращ низ, съдържащ текст с параметри, следван от стойностите за всеки от параметрите, а като резултат връща форматирания текст. Повече информация за форматиращите низове можете да намерите в Интернет и в статията Composite Formatting в MSDN (http://msdn.microsoft.com/en-us/library/txafckwd.aspx).

Парсване на данни

Обратната операция на форматирането на данни е тяхното парсване. Парсване на данни (data parsing) означава от текстово представяне на стойностите на някакъв тип в определен формат да се получи стойност от съответния тип, например от текста "22.10.2010" да се получи инстанция на типа DateTime, съдържаща съответната дата.

Често работата с приложения с графичен потребителски интерфейс пред­полага потреби­телският вход да бъде предаван през променливи от тип string, защото практически така може да се работи както с числа и символи, така и с текст и дати, форматирани по предпочитан от потреби­теля начин. Въпрос на опит на програмиста е да представи входните данни, които очаква, по правил­ния за потребителя начин. След това данните се преобразуват към по-конкретен тип и се обработват. Например числата могат да се преобразуват към променливи от int или double, а след това да участват в математически изрази за изчисления.

clip_image003[5]

При преобразуването на типове не бива да се осланяме само на доверието към потребителя. Винаги проверявайте коректността на входните потребителски данни! В про­тивен случай ще настъпи изключение, което може да промени нормалната логика на програмата.

Преобразуване към числови типове

За преобразуване на символен низ към число можем да използваме метода Parse(…) на примитивните типове. Нека видим пример за преобра­зу­ване на стринг към целочислена стойност (парсване):

string text = "53";

int intValue = int.Parse(text);

// intValue = 53

Можем да преобразуваме и променливи от булев тип:

string text = "True";

bool boolValue = bool.Parse(text);

// boolValue = true

Връщаната стойност е true, когато подаваният параметър е инициа­ли­зиран (не е обект със стойност null) и съдържанието му е "true", без значение от регистъра на буквите в него, т.е. всякакви текстове като "true", "True" или "tRUe" ще зададат на променливата boolValue стойност true. Всички останали случаи връщат стойност false.

В случай, че подадената на Parse(…) метод стойност е невалидна за типа (например подаваме "Пешо" при преобразуване на число), се получава изключение.

Преобразуване към дата

Парсването към дата става по подобен начин, като парсването към числов тип, но е препоръчително да се зададе конкретен формат за датата. Ето един пример как може да стане това:

string text = "11.09.2001";

DateTime parsedDate = DateTime.Parse(text);

Console.WriteLine(parsedDate);

// 11-Sep-01 0:00:00 AM

Дали датата ще бъде успешно парсната и в какъв точно формат ще бъде отпечатана на конзолата зависи силно от текущата култура на Windows. В примера е използван модифициран вариант на американската култура (en-US). Ако искаме да зададем изрично формат, който не зависи от културата, можем да ползваме метода DateTime.ParseExact(…):

string text = "11.09.2001";

string format = "dd.MM.yyyy";

DateTime parsedDate = DateTime.ParseExact(

      text, format, CultureInfo.InvariantCulture);

Console.WriteLine("Day: {0}\nMonth: {1}\nYear: {2}",

      parsedDate.Day, parsedDate.Month, parsedDate.Year);

// Day: 11

// Month: 9

// Year: 2001

При парсването по изрично зададен формат се изисква да се подаде конкретна култура, от която да се вземе информация за формата на датите и разделителите между дни и години. Тъй като искаме парсването да не зависи от конкретна култура, използваме неутралната култура: CultureInfo.InvariantCulture. За да използваме класа CultureInfo, трябва първо да включим пространството от имена System.Globalization.

Упражнения     

1.      Разкажете за низовете в C#. Какво е типично за типа string? Обяснете кои са най-важните методи на класа string.

2.      Напишете програма, която прочита символен низ, обръща го отзад напред и го принтира на конзолата. Например: "introduction" à "noitcudortni".

3.      Напишете програма, която проверява дали в даден аритметичен израз скобите са поставени коректно. Пример за израз с коректно поставени скоби: ((a+b)/5-d). Пример за некоректен израз: )(a+b)).

4.      Колко обратни наклонени черти трябва да посочите като аргумент на метода Split(…), за да разделите текста по обратна наклонена черта?

Пример: one\two\three

Забележка: В C# обратната наклонена черта е екраниращ символ.

5.      Напишете програма, която открива колко пъти даден подниз се съдържа в текст. Например нека търсим подниза "in" в текста:

We are living in a yellow submarine. We don't have anything else. Inside the submarine is very tight. So we are drinking all the day. We will move out of it in 5 days.

           Резултатът е 9 срещания.

6.      Даден е текст. Напишете програма, която променя регистъра на буквите до главни на всички места в текста, заградени с таговете <upcase> и </upcase>. Таговете не могат да бъдат вложени.

Пример:

We are living in a <upcase>yellow submarine</upcase>. We don't have <upcase>anything</upcase> else.

Резултат:

We are living in a YELLOW SUBMARINE. We don't have ANYTHING else.

7.      Напишете програма, която чете от конзолата стринг от максимум 20 символа и ако е по-кратък го допълва отдясно със "*" до 20 символа.

8.      Напишете програма, която преобразува даден стринг във вид на поредица от Unicode екраниращи последователности. Примерен входен стринг: "Наков". Резултат: "\u041d\u0430\u043a\u043e\u0432".

9.      Напишете програма, която кодира текст по даден шифър като прилага шифъра побуквено с операция XOR (изключващо или) върху текста. Кодирането трябва да се извършва като се прилага XOR между пър­вата буква от текста и първата буква на шифъра, втората буква от текста и втората буква от шифъра и т.н. до последната буква от шифъра, след което се продължава отново с първата буква от шифъра и поредната буква от текста. Отпечатайте резултата като поредица от Unicode кодирани екраниращи символи.

Примерен текст: "Nakov". Примерен шифър: "ab". Примерен резултат: "\u002f\u0003\u000a\u000d\u0017".

10.   Напишете програма, която извлича от даден текст всички изречения, които съдържат определена дума. Считаме, че изреченията са разде­лени едно от друго със символа ".", а думите са разделени една от друга със символ, който не е буква. Примерен текст:

We are living in a yellow submarine. We don't have anything else. Inside the submarine is very tight. So we are drinking all the day. We will move out of it in 5 days.

Примерен резултат:

We are living in a yellow submarine.

We will move out of it in 5 days.

11.   Даден е символен низ, съставен от няколко "забранени" думи, разде­лени със запетая. Даден е и текст, съдържащ тези думи. Да се напише програма, която замества забранените думи в текста със звездички. Примерен текст:

Microsoft announced its next generation C# compiler today. It uses advanced parser and special optimizer for the Microsoft CLR.

Примерен низ от забранените думи: "C#,CLR,Microsoft".

Примерен съответен резултат:

********* announced its next generation ** compiler today. It uses advanced parser and special optimizer for the ********* ***.

12.   Напишете програма, която чете число от конзолата и го отпечатва в 15-символно поле, подравнено вдясно по няколко начина: като десе­тично число, като шестнайсетично число, като процент, като валутна сума и във вид на експоненциален запис (scientific notation).

13.   Напишете програма, която приема URL адрес във формат:

[protocol]://[server]/[resource]

и извлича от него протокол, сървър и ресурс. Например при подаден адрес: http://www.devbg.org/forum/index.php резултатът е:

[protocol]="http"

[server]="www.devbg.org"

[resource]="/forum/index.php"

14.   Напишете програма, която обръща думите в дадено изречение без да променя пунктуацията и интервалите. Например: "C# is not C++ and PHP is not Delphi" -> "Delphi not is PHP and C++ not is C#".

15.   Даден е тълковен речник, който се състои от няколко реда текст. На всеки ред има дума и нейното обяснение, разделени с тире:

.NET – platform for applications from Microsoft
CLR – managed execution environment for .NET
namespace – hierarchical organization of classes

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

16.   Напишете програма, която заменя в HTML документ всички препратки (hyperlinks) от вида <a href="…">…</a> с препратки стил "форум", които имат вида [URL=…]…/URL].

Примерен текст:

<p>Please visit <a href="https://softuni.bg">our site</a> to choose a training course. Also visit <a href="www.devbg.org">our forum</a> to discuss the courses.</p>

Примерен съответен резултат:

<p>Please visit [URL=https://softuni.bg]our site[/URL] to choose a training course. Also visit [URL=www.devbg.org]our forum[/URL] to discuss the courses.</p>

17.   Напишете програма, която чете две дати, въведени във формат "ден.месец.година" и изчислява броя дни между тях.

Enter the first date: 27.02.2006

Enter the second date: 3.03.2004

Distance: 4 days

18.   Напишете програма, която чете дата и час, въведени във формат "ден.месец.година час:минути:секунди" и отпечатва датата и часа след 6 часа и 30 минути, в същия формат.

19.   Напишете програма, която извлича от даден текст всички e-mail адреси. Това са всички поднизове, които са ограничени от двете страни с край на текст или разделител между думи и съответстват на формата <sender>@<host>…<domain>. Примерен текст:

Please contact us by phone (+359 222 222 222) or by email at [email protected] or at [email protected]. This is not email: test@test. This also: @softuni.bg.com. Neither this: [email protected].

Извлечени e-mail адреси от примерния текст:

example@abv.bg

baj.ivan@yahoo.co.uk

20.   Напишете програма, която извлича от даден текст всички дати, които се срещат изписани във формат DD.MM.YYYY и ги отпечатва на конзолата в стандартния формат за Канада. Примерен текст:

I was born at 14.06.1980. My sister was born at 3.7.1984. In 5/1999 I graduated my high school. The law says (see section 7.3.12) that we are allowed to do this (section 7.4.2.9).

Извлечени дати от примерния текст:

14.06.1980

3.7.1984

21.   Напишете програма, която извлича от даден текст всички думи, които са палиндроми, например "ABBA", "lamal", "exe".

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

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

24.   Напишете програма, която чете от конзолата символен низ и заменя в него всяка последователност от еднакви букви с единична съответна буква. Пример: "aaaaabbbbbcdddeeeedssaa" à "abcdedsa".

25.   Напишете програма, която чете от конзолата списък от думи, разде­лени със запетайки и ги отпечатва по азбучен ред.

26.   Напишете програма, която изважда от даден HTML документ всичкия текст без таговете и техните атрибути.

Примерен текст:

<html>

  <head><title>News</title></head>

  <body><p><a href="https://softuni.bg">Telerik

    Academy</a>aims to provide free real-world practical

    training for young people who want to turn into

    skillful .NET software engineers.</p></body>

</html>

Примерен съответен резултат:

Title: News

Body:

Telerik Academy aims to provide free real-world practical training for young people who want to turn into skillful .NET software engineers.

Решения и упътвания

1.      Прочетете в MSDN или вижте първия абзац в тази глава.

2.      Използвайте StringBuilder и for (или foreach) цикъл.

3.      Използвайте броене на скобите: при отваряща на скоба увеличавайте брояча с 1, а при затваряща го намалявайте с 1. Следете броячът да не става отрицателно число и да завършва винаги на 0.

4.      Ако не знаете колко наклонени черти трябва да използвате, изпроб­вайте Split(…) с нарастващ брой черти, докато достигнете до желания резултат.

5.      Обърнете регистъра на буквите в текста до малки и търсете в цикъл дадения подниз. Не забравяйте да използвате IndexOf(…) с начален индекс, за да избегнете безкраен цикъл.

6.      Използвайте регулярни изрази или IndexOf(…) за отварящ и затварящ таг. Пресметнете началния и крайния индекс на текста. Обърнете текста в главни букви и заменете целия подниз отварящ таг + текст + затварящ таг с текста в горен регистър.

7.      Използвайте метода PadRight(…) от класа String.

8.      Използвайте форматиращ низ форматиращ низ "\u{0:x4}" за Unicode кода на всеки символ от входния стринг (можете да го получите чрез преобразуване на char към ushort).

9.      Нека шифърът cipher се състои от cipher.Length букви. Завъртете цикъл по буквите от текста и буквата на позиция index в текста шифрирайте с cipher[cipher.Length % index]. Ако имаме буква от текстa и буква от шифъра, можем да извършим XOR операция между тях като предварително превърнем двете букви в числа от тип ushort. Можем да отпечатаме резултата с форматиращ низ "\u{0:x4}".

10.   Първо разделете изреченията едно от друго чрез метода Split(…). След това проверявайте дали всяко от изреченията съдържа търсената дума като я търсите като подниз с IndexOf(…) и ако я намерите прове­рявате дали отляво и отдясно на намерения подниз има разделител (символ, който не е буква или начало / край на низ).

11.   Първо разделете забранените думи с метода Split(…), за да ги получите като масив. За всяка забранена дума обхождайте текста и търсете срещане. При срещане на забранена дума, заменете с толкова звездички, колкото букви се съдържат в забранената дума.

Друг, по-лесен, подход е да използвате RegEx.Replace(…) с подходящ регулярен израз и подходящ MatchEvaluator метод.

12.   Използвайте подходящи форматиращи низове.

13.   Използвайте регулярен израз или търсете по съответните разделители – две наклонени черти за край на протокол и една наклонена черта за разделител между сървър и ресурс. Разгледайте специалните случаи, в които части от URL адреса могат да липсват.

14.   Можете да решите задачата на две стъпки: обръщане на входния низ на обратно; обръщане на всяка от думите от резултата на обратно.

Друг, интересен подход е да разделете входния текст по препина­телните знаци между думите, за да получите само думите от текста и след това да разделите по буквите, за да получите препинателните знаци от текста. Така, имайки списък от думи и списък от препина­телни знаци между тях, лесно можете да обърнете думите на обратно, запазвайки препинателните знаци.

15.   Можете да парснете текст като го разделите първо по символа на нов ред, а след това втори път по " - ". Речникът е най-удачно да запишете във хеш-таблица (Dictionary<string, string>), която ще осигури бързо търсене по зададена дума. Прочетете в Интернет за хеш-таблици и за класа Dictionary<K,T>.

16.   Най-лесно задачата може да решите с регулярен израз.

Ако все пак изберете да не ползвате регулярни изрази, може да намерите всички поднизове, които започват с "<a href=" и завършват с "</a>" и вътре в тях да замените "<a href="" с "[URL=", след това първото срещнато "">" с "]" и след това "</a>" с "[/URL]".

17.   Използвайте методите на структурата DateTime, а за парсване на датите може да ползвате разделяне по "." или парсване с метода DateTime.ParseExact(…).

18.   Използвайте методите DateTime.ToString() и DateTime.ParseExact() с подходящи форматиращи низове.

19.   Използвайте RegEx.Match(…) с подходящ регулярен израз.

Ако реша­вате задачата без регу­лярни изрази, ще трябва да обработ­вате текста побуквено от начало до край и да обработвате поредния символ в зависимост от текущия режим на работа, който може да е един OutsideOfEmail, ProcessingSender или ProcessingHostOrDomain. При срещане на разделител или край на текста, ако се обработва хост или домейн (режим ProcessingHostOrDomain), значи е намерен email, а иначе потенциално започва нов e-mail и трябва да се премине в състояние ProcessingSender. При срещане на @ в режим на работа ProcessingSender се преминава към режим ProcessingSender. При срещане на букви или точка в режими ProcessingSender или ProcessingHostOrDomain те се натрупват в буфер. По пътя на тези разсъждения можете да разглеждате всички възможни групи символи, срещнати съответно във всеки от трите режима и да ги обработите по подходящ начин. Реално се получава нещо като краен автомат (state machine), който разпознава e-mail адреси. Всички намерени e-mail адреси трябва да се проверят дали имат непразен получател, непразен хост, домейн с дължина между 2 и 4 букви, както и да не започват или завършват с точка.

Друг по-лесен подход за тази задача е да се раздели текста по всички символи, които не са букви и точки и да се проверят така извлечените "думи" дали са валидни e-mail адреси чрез опит да се раздробят на непразни части: <sender>, <host>, <domain>, отговарящи на изброе­ните вече условия.

20.   Използвайте RegEx.Match(…) с подходящ регулярен израз. Алтернатив­ният вариант е да си реализирате автомат, който има състояния OutOfDate, ProcessingDay, ProcessingMonth, ProcessingYear и обработ­вайки текста побуквено да преминавате между състоянията според поредната буква, която обработвате. Както и при предходната задача, можете предварително да извадите всички "думи" от текста и след това да проверите кои от тях съответстват на шаблона за дата.

21.   Раздробете текста на думи и проверете всяка от тях дали е палиндром.

22.   Използвайте масив от символи char[65536], в който ще отбелязвате колко пъти се среща всяка буква. Първоначално всички елементи на масива са нули. След побуквена обработка на входния низ можете да отбележите в масива коя буква колко пъти се среща. Примерно ако се срещне буквата 'A', ще се увеличи с единици броят срещания в масива на индекс 65 (Unicode кодът на 'A'). Накрая с едно сканиране на масива може да се отпечатат всички ненулеви елементи (като се преобразуват char, за да се получи съответната буква) и и приле­жащия им брой срещания.

23.   Използвайте хеш-таблица (Dictionary<string,int>), в която пазите за всяка дума от входния низ колко пъти се среща. Прочетете в Интернет за класа System.Collections.Generic.Dictionary<K,T>. С едно обхождане на думите можете да натрупате в хеш-таблицата информация за срещанията на всяка дума, а с обхождане на хеш-таблицата можете да отпечатате резултата.

24.   Можете да сканирате текста отляво надясно и когато текущата буква съвпада с предходната, да я пропускате, а в противен случай да я долепяте в StringBuilder.

25.   Използвайте статичния метод Array.Sort(…).

26.   Сканирайте текста побуквено и във всеки един момент пазете в една променлива дали към момента има отворен таг, който не е бил затворен или не. Ако срещнете "<", влизайте в режим "отворен таг". Ако срещнете ">", излизайте от режим "отворен таг". Ако срещнете буква, я добавяйте към резултата, само ако програмата не е в режим "отворен таг". След затваряне на таг може да добавяте по един интервал, за да не се слепва текст преди и след тага.

Демонстрации (сорс код)

Изтеглете демонстрационните примери към настоящата глава от книгата: Символни-низове-Демонстрации.zip.

Дискусионен форум

Коментирайте книгата и задачите в нея във: форума на софтуерната академия.


3 отговора до “Глава 13. Символни низове”

  1. Ludmil says:

    Решения и опътвания – Символни низове т.9 (стр 504 в книгата)
    написано: cipher[cipher.Length % index] (DividedByZeroException)
    да се чете:cipher[index % cipher.Length] (correct answers)

  2. Да, има такива недоразумения. В английския вариант на книгата всичко това е коригирано.

Отговори на vasko

Трябва да сте влезнали, за да коментирате.