С# строка заменить словарем

У меня есть строка, в которой мне нужно сделать некоторые замены. У меня есть Dictionary<string, string>, в котором определены пары поиска-замены. Я создал следующие методы расширения для выполнения этой операции:

public static string Replace(this string str, Dictionary<string, string> dict)
{
    StringBuilder sb = new StringBuilder(str);

    return sb.Replace(dict).ToString();
}

public static StringBuild Replace(this StringBuilder sb, 
    Dictionary<string, string> dict)
{
    foreach (KeyValuePair<string, string> replacement in dict)
    {
        sb.Replace(replacement.Key, replacement.Value);
    }

    return sb;
}

Есть ли лучший способ сделать это?


person RaYell    schedule 05.08.2009    source источник


Ответы (8)


Если данные токенизированы (т. е. «Уважаемый $name$, на $date$ ваш баланс равен $amount$»), то Regex может быть полезен:

static readonly Regex re = new Regex(@"\$(\w+)\$", RegexOptions.Compiled);
static void Main() {
    string input = @"Dear $name$, as of $date$ your balance is $amount$";

    var args = new Dictionary<string, string>(
        StringComparer.OrdinalIgnoreCase) {
            {"name", "Mr Smith"},
            {"date", "05 Aug 2009"},
            {"amount", "GBP200"}
        };
    string output = re.Replace(input, match => args[match.Groups[1].Value]);
}

Однако без чего-то подобного я ожидаю, что ваш цикл Replace, вероятно, будет настолько большим, насколько вы можете сделать, не заходя в крайности. Если он не токенизирован, возможно, профилируйте его; Replace действительно проблема?

person Marc Gravell    schedule 05.08.2009
comment
Отличный ответ. Я думаю, что ваше предложение на самом деле будет лучше, чем повторение всего словаря, поскольку регулярное выражение заменит только найденные токены. Он не будет проверять всех, кто может быть внутри. Поэтому, если у меня есть большой словарь и небольшое количество токенов во входной строке, это действительно может дать толчок моему приложению. - person RaYell; 05.08.2009
comment
Очень полезный. Я реорганизовал его как метод расширения для Regex, который я не могу показать в комментарии, поэтому добавлю его как несколько избыточный дополнительный ответ ниже. - person Francis Norton; 20.03.2013
comment
Это вызовет исключение, если ключ не найден. - person Axel; 15.01.2017
comment
Есть ли у нас какое-нибудь решение, если ключ не найден? - person Phoenix_uy; 27.05.2019
comment
@Phoenix_uy match => args.TryGetValue(match.Groups[1].Value, out var val) ? val : "*whatever*" - person Marc Gravell; 28.05.2019

Сделайте это с помощью Linq:

var newstr = dict.Aggregate(str, (current, value) => 
     current.Replace(value.Key, value.Value));

dict – это объект словаря, определяемый парами поиска и замены.

str — это ваша строка, которую вам нужно заменить.

person Allen Wang    schedule 31.10.2011
comment
+1 Я использовал регулярное выражение в прошлом, но это отлично сработало для меня. - person clairestreb; 18.10.2014

Мне кажется разумным, за исключением одного: он чувствителен к порядку. Например, возьмите входную строку "$x $y" и заменяющий словарь:

"$x" => "$y"
"$y" => "foo"

Результатом замены является либо "foo foo" или "$y foo" в зависимости от того, какая замена выполняется первой.

Вместо этого вы можете управлять порядком, используя List<KeyValuePair<string, string>>. Альтернативой является просмотр строки, чтобы убедиться, что вы не используете замены в дальнейших операциях замены. Хотя, наверное, это будет намного сложнее.

person Jon Skeet    schedule 05.08.2009

Вот слегка переработанная версия отличного ответа @Marc, чтобы сделать функциональность доступной в качестве метода расширения для Regex:

static void Main() 
{
    string input = @"Dear $name$, as of $date$ your balance is $amount$";
    var args = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    args.Add("name", "Mr Smith");
    args.Add("date", "05 Aug 2009");
    args.Add("amount", "GBP200");

    Regex re = new Regex(@"\$(\w+)\$", RegexOptions.Compiled);
    string output = re.replaceTokens(input, args);

    // spot the LinqPad user // output.Dump();
}

public static class ReplaceTokensUsingDictionary
{
    public static string replaceTokens(this Regex re, string input, IDictionary<string, string> args)
    {
        return re.Replace(input, match => args[match.Groups[1].Value]);
    }
}
person Francis Norton    schedule 20.03.2013

при использовании решения RegEx Марка Гравелла сначала проверьте, доступен ли токен, используя, например, ContainsKey, чтобы предотвратить ошибки KeyNotFoundException:

string output = re.Replace(zpl, match => { return args.ContainsKey(match.Groups[1].Value) ? arg[match.Groups[1].Value] : match.Value; });

при использовании следующего слегка измененного примера кода (1-й параметр имеет другое имя):

    var args = new Dictionary<string, string>(
        StringComparer.OrdinalIgnoreCase) 
        {
            {"nameWRONG", "Mr Smith"},
            {"date", "05 Aug 2009"},
            {"AMOUNT", "GBP200"}
        };

это производит следующее:

"Уважаемый $name$, по состоянию на 05 августа 2009 г. ваш баланс составляет 200 фунтов стерлингов"

person DannyB    schedule 17.03.2015

Вот ты где:

public static class StringExm
{
    public static String ReplaceAll(this String str, KeyValuePair<String, String>[] map)
    {
        if (String.IsNullOrEmpty(str))
            return str;

        StringBuilder result = new StringBuilder(str.Length);
        StringBuilder word = new StringBuilder(str.Length);
        Int32[] indices = new Int32[map.Length];

        for (Int32 characterIndex = 0; characterIndex < str.Length; characterIndex++)
        {
            Char c = str[characterIndex];
            word.Append(c);

            for (var i = 0; i < map.Length; i++)
            {
                String old = map[i].Key;
                if (word.Length - 1 != indices[i])
                    continue;

                if (old.Length == word.Length && old[word.Length - 1] == c)
                {
                    indices[i] = -old.Length;
                    continue;
                }

                if (old.Length > word.Length && old[word.Length - 1] == c)
                {
                    indices[i]++;
                    continue;
                }

                indices[i] = 0;
            }

            Int32 length = 0, index = -1;
            Boolean exists = false;
            for (int i = 0; i < indices.Length; i++)
            {
                if (indices[i] > 0)
                {
                    exists = true;
                    break;
                }

                if (-indices[i] > length)
                {
                    length = -indices[i];
                    index = i;
                }
            }

            if (exists)
                continue;

            if (index >= 0)
            {
                String value = map[index].Value;
                word.Remove(0, length);
                result.Append(value);

                if (word.Length > 0)
                {
                    characterIndex -= word.Length;
                    word.Length = 0;
                }
            }

            result.Append(word);
            word.Length = 0;
            for (int i = 0; i < indices.Length; i++)
                indices[i] = 0;
        }

        if (word.Length > 0)
            result.Append(word);

        return result.ToString();
    }
}
person Albeoris    schedule 21.04.2016
comment
Вы можете добавить некоторые пояснения или комментарии к своему ответу, чтобы читателям не приходилось внимательно изучать код, чтобы понять, что вы предлагаете. Тем более, что это довольно длинный фрагмент, намного длиннее, чем другие предлагаемые решения. - person Fabio says Reinstate Monica; 22.04.2016
comment
Он делает то, что хотел автор. В отличие от других, он не использует регулярные выражения и проходит через строку только один раз, что важно, когда нужно произвести несколько десятков замен. - person Albeoris; 23.04.2016
comment
Похоже, что он игнорирует границы слов и кажется очень подверженным ошибкам. С другой стороны, никаких объяснений нет, так что я могу ошибаться. - person Uwe Keim; 02.05.2016
comment
Да, нет проверки границ слов. - person Albeoris; 08.05.2016

Правильным инструментом для этой конкретной задачи является Mustache, простой стандартный язык шаблонов без логики.

Есть много реализаций. В этом примере я буду использовать щетину:

var stubble = new StubbleBuilder().Build();
var dataHash = new Dictionary<string, Object>()
{
    {"Foo","My Foo Example"},
    {"Bar",5}
};

var output = stubble.Render(
   "Hey, watch me replace this: {{Foo}} ... with example text.  Also {{bar}} is 5"
   , dataHash
);
person Pxtl    schedule 31.07.2020

Почему бы просто не проверить, существует ли такой ключ?

  • если существует, то удалите пару, иначе пропустите этот шаг;

  • добавьте тот же ключ, но теперь с новым желаемым значением.

      // say, you have the following collection
      var fields = new Dictionary<string, string>();
      fields.Add("key1", "value1");
      fields.Add("key2", "value2");
      fields.Add("key3", "value3");
    
      // now, you want to add a pair "key2"/"value4"
      // or replace current value of "key2" with "value4"
      if (fields.ContainsKey("key2"))
      {
          fields.Remove("key2");
      }
      fields.Add("key2", "value4");
    
person Mike Osipov    schedule 18.11.2020