Элегантные строки

Представим, что нам нужно что-нибудь сделать со строками в .net. Что-то не очень сложное, но и не совсем простое. Например, для правильного форматирования, расставить пробелы после запятых в тексте. Что же предлагает .net из коробки?
Что-то такое:

string str = "...";
str.Replace(",", ", ");


Постойте, но мы же хотели расставлять пробелы, а не заменять запятые!..

Хорошо, пойдем дальше.
Давайте, введем цензуру. Не будем разрешать в наших текстах, скажем, слово «медведь». Вот так вот запросто. Будем подменять каждого «медведя» многоточием.
Ага, подменять. Значит логично использовать все тот же метод Replace. Сказано — сделано:

string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал.";
var result = str.Replace("медведь", "...");


Хм, многоточие вместо первого медведя появилось, а вот второй был слишком горд и начинался с большой буквы. И наш метод перед ним спасовал. Придется пробежаться второй раз и поменять еще и гордых «Медведей».

string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал.";
var result = str.Replace("медведь", "...").Replace("Медведь", "...");

Фух, получилось. Не очень красиво, но работает. Но работает ли? Вдруг придет такой «меДВедь»?
Мы подумали, напряглись и отсекли таких наглецов тоже. Но какой ценой!

string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал.";
int index = str.IndexOf("медведь", StringComparison.CurrentCultureIgnoreCase);
while (index >= 0)
{
    str = str.Remove(index, "медведь".Length);
    str = str.Insert(index, "...");
    index = str.IndexOf("медведь", StringComparison.CurrentCultureIgnoreCase);
}

Что-то в этом коде не так. И проблемы две:
  1. Медленное выполнение из-за пересоздания строк на каждом шаге благодаря иммутабельности
  2. Низкоуровневый кусок утилитарного кода, который обычно ссылают в класс с названием Util и забывают, посреди прелестного семантично-выверенного проекта (ну, хотя бы фантазиях же можно?)

При этом, решение для улучшения быстродействия есть — переписать, используя StringBuilder.
Но что делать с тихо ворчащим эстетическим чувством?

Согласитесь, существующий интерфейс работы со строками в .net морально устарел. Он архаичен, недостаточно гибок и заставляет писать много странного кода для, казалось бы, обычных и простых операций раз за разом.
Еще не забудьте проверки на null. Проверки граничных значений индекса. И извольте правильно обойтись с длинами строк.

Так родилась идея Fluent интерфейса библиотеки для работы со строками.
Современного, читабельного, и так же хорошо протестированного.

Посмотрим, что же из этого получилось.

Пример операции вставки:
string t = "Строка будет вставлена после второго слова маркер. Я тот самый маркер! А этот маркер будет проигнорирован"
           .Insert(", а тут был Вася").After(2, "Маркер").IgnoringCase().From(The.Beginning);
t.Should().Be("Строка будет вставлена после второго слова маркер. Я тот самый маркер, а тут был Вася! А этот маркер будет проигнорирован");

Читается как Insert " а тут был Вася" after second "маркер" ignoring case from the beginning. Хотя, что это я? И так же все понятно.

Что-нибудь удалим:
string t = "Эта строчка будет удалена ->ТЕСТ и эта тоже ->ТЕСТ, а эта останется ->ТЕСТ"
           .Remove(2, "ТЕСТ");
transformed.Should().Be("Эта строчка будет удалена -> и эта тоже ->, а эта останется ->ТЕСТ");


А теперь удалим все, учитывая регистр:
string t = "Строка ТЕСТ будет удалена с обоих концов ТЕСТ".RemoveAll("тЕСт").IgnoringCase();
t.Should().Be("Строка  будет удалена с обоих концов ");


Или даже так:
string t = "Some very long string".RemoveChars('e', 'L', 'G').IgnoringCase();
t.Should().Be("Som vry on strin");


И так:
string t = "Очень длинная строка с русскими буквами, ё".RemoveVowels().For("ru");
t.Should().Be("чнь длнн стрк с рсскм бквм, ");


Нашлось место и для расширения стандартной логики:
bool isEmptyOrWhiteSpace = "  ".IsEmpty().OrWhiteSpace();
isEmptyOrWhiteSpace.Should().Be(true);


Проход туда:
var indexes = "Текст в котором есть маркер, потом еще один маркер и напоследок МАРКЕР большими буквами"
              .IndexesOf("МаРкЕр").IgnoringCase();
indexes.Should().ContainInOrder(21, 44, 64);


И обратно:
var indexes = "Текст в котором есть маркер, потом еще один маркер и напоследок МАРКЕР большими буквами"
              .IndexesOf("маркер").From(The.End);
indexes.Should().ContainInOrder(44, 21);


А пример с медведями получается компактным и легко читаемым:
string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал.";
var result = str.ReplaceAll("медведь").With("...").IgnoringCase();


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

Быстро попробовать можно при помощи NuGet.
А помочь проекту на GitHub или CodePlex.
Share post

Comments 41

    0
    В C# разве нет аналогов библиотек apache commons?
      0
      Я о таком не слышал. Может что есть?
      Почитал про Apache Commons — понравилось.
    • UFO just landed and posted this here
        –1
        Лично я буду благодарен за такую обёртку на регэксами. Удобно же.
          0
          А как с читабельностью?
          Вопрос риторический.
          • UFO just landed and posted this here
              0
              Для того, чтобы совсем наверняка «порвал», можно RegexOptions.Compiled добавить.
              А в остальном, да, конкретный, утрированный пример из статьи, данное регулярное выражение решает.

              Видимо, я не совсем ясно выразил цели проекта.
              Цель проекта не заменить собой регулярные выражения, существующую логику работы со строками или устроить революцию.
              Цель — создать Fluent интерфейс для работы со строками и реализовать его на практике.

              Если кому-то это тоже будет интересно — добро пожаловать. Остальные могут продолжать писать на асме использовать регулярные выражения.

              А про быстродействие библиотеки, кстати, я даже нигде в статье не упоминал.
          +7
          Забавно, но не думаю, что применимо в реальных проектах. Столько дополнительных классов ради того, что можно было бы сделать регулярками?
          С точки зрения эстетики кода, весьма элегантно, но слишком нетипично для C#. Если метод Should() я еще могу понять, то перечислимый тип The
            0
            С каких пор текучие вызовы нетипичны для C#?) Очень даже в духе тенденций, так скажем.
              +1
              Вызовы по цепочке как таковые — вполне типичны, а вот то, как они читаются — нет. В традициях C# было бы как-то типа такого:
              var str = "some string";
              var res = str.ToFluent()
                           .Find("some", StringSearchDirection.FromEnd)
                           .ReplaceWith("any", StringComparison.IgnoreCase);
              

              А попытки сделать программный код правильным предложением на английском свойственны скорее DSL на Ruby.
                0
                А, в этом смысле да. Хотя, если посмотреть на мок- и модульно-тестовые библиотеки, там это вполне основной подход. Да и внутренние DSL на C# тоже вполне неплохо пишутся, например, когда предметная область из каких-нибудь тарифов по-разному собираемых — очень читаемо и красиво получается :)
            +1
            Сам люблю велосипедостроение, но здесь-то чем регекспы не угодили?
              +11
              Стоп. А в каком месте эти строки элегантные?
                –8
                Простите, что влезаю немного не в тему, но вот меня всегда поражало, как создатели .NET могли додуматься до названий вроде StringComparison.CurrentCultureIgnoreCase. Ведь это совершенно нечитабельно.
                  +2
                  А как было бы читабельно?
                    +6
                    Хотите strcmpi?

                      0
                      Ну вот зачем сразу крайности (хотя в strcmpi не так всё и плохо). Посмотреть хотя бы на библиотеку Delphi, который был по сути прародителем .NET (и то, и другое ваял в том числе Андерс Хейлсберг). Там сделано из без extreme verbosity, и читаемо.
                        0
                        Так название конкретное предложите этому компаратору, как бы вы его назвали?
                          –3
                          Дело не конкретно в этом компараторе, а в общем стиле именования, который предполагает имена в духе «НазваниеНеймспейсаИзДвадцатиБуквВCamelCase.ЧтоТоСлишкомДлинное». Как по мне, в отсутствие в языке (по понятным причинам) возможности передавать произвольные «незакавыченные» строки куда-либо неплохо было бы сделать своеобразный локальный неймспейсинг, чтобы default namespace для параметров функций мог меняться на тот, где они были объявлены, например. Ну или иной механизм похожего назначения. Потому как выглядит это совершенно уродливо и нечитаемо, что, собственно, и натолкнуло автора к написанию его библиотечки, кстати.
                            +1
                            В значении StringComparison.CurrentCultureIgnoreCase нет неймспейса. Это enum и его значение через точку.

                            Вы предлагаете разрешить при вызове меотда принимающего enum (например StringComparison) не указывать тип enum'а?
                            А-ля «hello world».IndexOf(«w», CurrentCultureIgnoreCase)?
                              0
                              Ну хотя бы. Непонятно, почему все так радикально против, что даже в карму минусуют. Такое ощущение, что у присутствующих начисто отсутствуют какие-либо эстетические чувства.
                                0
                                Ну и помимо этого, как мне кажется, CurrentCulture само по себе излишне. Не уверен насчёт того, какие есть другие значения, но вроде как логичнее было бы указывать отличия от некоего «Current», чем каждый раз писать о его наличии.
                                  +2
                                  Дело в том что StringComparison опционален. Есть перегрузки методов без него. Кроме CurrentCulture есть ещё InvariantCulture и Ordinal (это режим где сравнение идёт по charcode, не задумываясь о схожих символах в UTF).

                                  Я с вами соглашусь про излишнюю многословность .NET. Но это его суть. Он создавался чтобы быть однозначным и простым в изучении (главным образом для индусов :)).

                                +1
                                Т.е. конкретных предложений по конкретному примеру у вас нет?
                        +4
                        string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал."; var result = str.ReplaceAll("медведь").With("...").IgnoringCase();

                        Желание сделать что-то новое это похвально. Реализация — нет.

                        Я не готов посмотреть код на github (боюсь за психическое здоровье), но если бы меня заставили процитированный код саппортить, сделал бы такие выводы:
                        1. Extension метод Replace() создает какой-то промежуточный класс и туда пишет изначальную строку и слово «медведь»
                        2. метод With() либо создает еще один промежуточный класс, либо добавляет флаг в предыдущий
                        3. метод IgnoringCase(), не смотря на функциональную незначительность названия, является «спусковым ключком», который выполняет операцию замены и возвращает строку
                        4. Если в пунктах 1-3 нет промежуточного класса и данные хранятся в статических переменных, то автору надо отрубить руки
                        5. Если промежуточный класс есть, но пропустив IgnoringCase (или даже с ним) мы получаем в var result какой-то внутренний класс, а не строку, то автору надо отрубить руки
                        6. Если утверждения 4 и 5 не верны, а реализовано оно еще мудренее, то автору надо обязательно отрубить руки
                          +4
                          Там везде возвращается внутренний класс с оператором неявной конвертации в строку. Не продолжайте, мы поняли, что нужно сделать с автором. =)
                            +5
                            Куда высылать руки?
                            +3
                            Fluent-интерфейсы возвращают ссылку на промежуточный объект (скорее всего всегда новый), постепенно добавляя туда разнообразные флажки. И данный объект имеет метод, разворачивающий флажки в то, что нам нужно. В данном случае, что очевидно, это будет ToString() и\или оператор перевода в строку.

                            Исходя из ваших утверждений, с подобным подходом вы не знакомы.
                            Почему же тогда вы позволили себе быть столь категоричным, даже не заглянув за ответами в код?
                              –3
                              Еще в Java и AlertDialog.Builder я понял, что изобретателю этой «красивой» конструкции надо… ну вы поняли :)
                              Теперь буду знать, как это называется, спасибо!
                              Благо, в Java нет Extension methods и никто не наращивает так функционал базовых классов.
                              Да и в случае AlertDialog.Builder есть явный метод-триггер — там не додумались важный функционал на implicit type conversion привязывать :)

                              Почему же тогда вы позволили себе быть столь категоричным, даже не заглянув за ответами в код?

                              Потому как имею консервативное и устойчивое собственное мнение на счет подобных конструкций, вне зависимости от их dirty internals.
                                +2
                                Да что там конструкция. Вы крайне негативно отозвались о реализации, сделав большое кол-во неверных утверждений, в то время как лежащий на поверхности правильный ответ вы явно проигнорировали. Ну банально нельзя так, невежливо.
                                  0
                                  ок, пристыдил. извиняюсь =)
                                0
                                Спасибо deilux за популярное описание внутреннего устройства Fluent интерфейсов. Я думал такое на Хабре объяснять не нужно.
                              +8
                              My.Eyes.Are("Bleeding");
                              
                                +6
                                В русле статьи будет

                                   My("Eyes").Are("Bleeding").Badly();
                                
                                +1
                                А теперь удалим все, учитывая регистр:

                                string t = "Строка ТЕСТ будет удалена с обоих концов ТЕСТ".RemoveAll("тЕСт").IgnoringCase();
                                t.Should().Be("Строка  будет удалена с обоих концов ");
                                



                                Не пойму, ведь IgnoringCase означет «игнорить регистр», где ж тут учет того самого регистра?
                                  0
                                  Да, вы правы. Здесь должно быть "… без учета регистра".
                                  +6
                                  Каждый программист должен написать библиотеку/обёртку для работы со строками)
                                  +2
                                  Кстати говоря, с версии C# 4.0 он позволяет именовать аргументы при вызове. Можно сделать, например, такой метод, и будет ничуть не менее читаемо:\

                                  var str = "hello world";
                                  var res = str.Replace(
                                      from: "hello",
                                      to: "goodbye",
                                      ignoreCase: true
                                  );
                                  
                                    0
                                    Омайгадбл!
                                    на каждый возможный вариант действия Insert свой отдельный класс (метод Remove — еще больше)?!!!
                                    github.com/MSayfullin/FluentStrings/tree/master/FluentStrings/Actions/Insert/Auxiliary

                                  Only users with full accounts can post comments. Log in, please.