Как я могу создать более удобный синтаксис string.format? - PullRequest
45 голосов
/ 24 августа 2009

Мне нужно создать очень длинную строку в программе, и я использую String.Format. Проблема, с которой я сталкиваюсь, заключается в отслеживании всех чисел, когда у вас есть более 8-10 параметров.

Можно ли создать какую-то форму перегрузки, которая будет принимать синтаксис, подобный этому?

String.Format("You are {age} years old and your last name is {name} ",
{age = "18", name = "Foo"});

Ответы [ 6 ]

71 голосов
/ 24 августа 2009

Как насчет следующего, который работает как для анонимных типов (пример ниже), так и для обычных типов (сущности домена и т. Д.):

static void Main()
{
    string s = Format("You are {age} years old and your last name is {name} ",
        new {age = 18, name = "Foo"});
}

с помощью:

static readonly Regex rePattern = new Regex(
    @"(\{+)([^\}]+)(\}+)", RegexOptions.Compiled);
static string Format(string pattern, object template)
{
    if (template == null) throw new ArgumentNullException();
    Type type = template.GetType();
    var cache = new Dictionary<string, string>();
    return rePattern.Replace(pattern, match =>
    {
        int lCount = match.Groups[1].Value.Length,
            rCount = match.Groups[3].Value.Length;
        if ((lCount % 2) != (rCount % 2)) throw new InvalidOperationException("Unbalanced braces");
        string lBrace = lCount == 1 ? "" : new string('{', lCount / 2),
            rBrace = rCount == 1 ? "" : new string('}', rCount / 2);

        string key = match.Groups[2].Value, value;
        if(lCount % 2 == 0) {
            value = key;
        } else {
            if (!cache.TryGetValue(key, out value))
            {
                var prop = type.GetProperty(key);
                if (prop == null)
                {
                    throw new ArgumentException("Not found: " + key, "pattern");
                }
                value = Convert.ToString(prop.GetValue(template, null));
                cache.Add(key, value);
            }
        }
        return lBrace + value + rBrace;
    });
}
2 голосов
/ 07 октября 2015

Начиная с C # 6, этот тип интерполяции строк теперь возможен с использованием нового синтаксиса строк синтаксис:

var formatted = $"You are {age} years old and your last name is {name}";
2 голосов
/ 24 августа 2009

не совсем то же самое, но как бы подделка ... используйте метод расширения, словарь и небольшой код:

как то так ...

  public static class Extensions {

        public static string FormatX(this string format, params KeyValuePair<string, object> []  values) {
            string res = format;
            foreach (KeyValuePair<string, object> kvp in values) {
                res = res.Replace(string.Format("{0}", kvp.Key), kvp.Value.ToString());
            }
            return res;
        }

    }
1 голос
/ 24 августа 2009

Что если возраст / имя является переменной в вашем приложении. Таким образом, вам потребуется синтаксис сортировки, чтобы сделать его почти уникальным, как {age_1}?

Если у вас проблемы с 8-10 параметрами: почему бы не использовать

"You are " + age + " years old and your last name is " + name + "
1 голос
/ 24 августа 2009

Примитивная реализация:

public static class StringUtility
{
  public static string Format(string pattern, IDictionary<string, object> args)
  {
    StringBuilder builder = new StringBuilder(pattern);
    foreach (var arg in args)
    {
      builder.Replace("{" + arg.Key + "}", arg.Value.ToString());
    }
    return builder.ToString();
  }
}

Использование:

StringUtility.Format("You are {age} years old and your last name is {name} ",
  new Dictionary<string, object>() {{"age" = 18, "name" = "Foo"}});

Вы также можете использовать анонимный класс, но это намного медленнее из-за необходимости отражения.

Для реальной реализации вы должны использовать регулярное выражение для

  • разрешить экранирование {}
  • проверить, есть ли заполнители, которые не были заменены, что, скорее всего, является ошибкой программирования.
0 голосов
/ 23 февраля 2016

Хотя C # 6.0 теперь может делать это с помощью интерполяции строк, иногда это необходимо делать со строками динамического формата во время выполнения. Мне не удалось использовать другие методы, для которых требуется DataBinder.Eval, поскольку они недоступны в .NET Core и были недовольны производительностью решений Regex.

Имея это в виду, вот бесплатный Regex, анализатор на основе конечного автомата, который я написал. Он обрабатывает неограниченные уровни {{{escaping}}} и выбрасывает FormatException, когда ввод содержит несбалансированные фигурные скобки и / или другие ошибки. Хотя основной метод принимает Dictionary<string, object>, вспомогательный метод также может принимать object и использовать его параметры через отражение.

public static class StringExtension {
    /// <summary>
    /// Extension method that replaces keys in a string with the values of matching object properties.
    /// </summary>
    /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
    /// <param name="injectionObject">The object whose properties should be injected in the string</param>
    /// <returns>A version of the formatString string with keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, object injectionObject) {
        return formatString.FormatWith(GetPropertiesDictionary(injectionObject));
    }

    /// <summary>
    /// Extension method that replaces keys in a string with the values of matching dictionary entries.
    /// </summary>
    /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
    /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
    /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, IDictionary<string, object> dictionary) {
        char openBraceChar = '{';
        char closeBraceChar = '}';

        return FormatWith(formatString, dictionary, openBraceChar, closeBraceChar);
    }
        /// <summary>
        /// Extension method that replaces keys in a string with the values of matching dictionary entries.
        /// </summary>
        /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
        /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
        /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, IDictionary<string, object> dictionary, char openBraceChar, char closeBraceChar) {
        string result = formatString;
        if (dictionary == null || formatString == null)
            return result;

        // start the state machine!

        // ballpark output string as two times the length of the input string for performance (avoids reallocating the buffer as often).
        StringBuilder outputString = new StringBuilder(formatString.Length * 2);
        StringBuilder currentKey = new StringBuilder();

        bool insideBraces = false;

        int index = 0;
        while (index < formatString.Length) {
            if (!insideBraces) {
                // currently not inside a pair of braces in the format string
                if (formatString[index] == openBraceChar) {
                    // check if the brace is escaped
                    if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
                        // add a brace to the output string
                        outputString.Append(openBraceChar);
                        // skip over braces
                        index += 2;
                        continue;
                    }
                    else {
                        // not an escaped brace, set state to inside brace
                        insideBraces = true;
                        index++;
                        continue;
                    }
                }
                else if (formatString[index] == closeBraceChar) {
                    // handle case where closing brace is encountered outside braces
                    if (index < formatString.Length - 1 && formatString[index + 1] == closeBraceChar) {
                        // this is an escaped closing brace, this is okay
                        // add a closing brace to the output string
                        outputString.Append(closeBraceChar);
                        // skip over braces
                        index += 2;
                        continue;
                    }
                    else {
                        // this is an unescaped closing brace outside of braces.
                        // throw a format exception
                        throw new FormatException($"Unmatched closing brace at position {index}");
                    }
                }
                else {
                    // the character has no special meaning, add it to the output string
                    outputString.Append(formatString[index]);
                    // move onto next character
                    index++;
                    continue;
                }
            }
            else {
                // currently inside a pair of braces in the format string
                // found an opening brace
                if (formatString[index] == openBraceChar) {
                    // check if the brace is escaped
                    if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
                        // there are escaped braces within the key
                        // this is illegal, throw a format exception
                        throw new FormatException($"Illegal escaped opening braces within a parameter - index: {index}");
                    }
                    else {
                        // not an escaped brace, we have an unexpected opening brace within a pair of braces
                        throw new FormatException($"Unexpected opening brace inside a parameter - index: {index}");
                    }
                }
                else if (formatString[index] == closeBraceChar) {
                    // handle case where closing brace is encountered inside braces
                    // don't attempt to check for escaped braces here - always assume the first brace closes the braces
                    // since we cannot have escaped braces within parameters.

                    // set the state to be outside of any braces
                    insideBraces = false;

                    // jump over brace
                    index++;

                    // at this stage, a key is stored in current key that represents the text between the two braces
                    // do a lookup on this key
                    string key = currentKey.ToString();
                    // clear the stringbuilder for the key
                    currentKey.Clear();

                    object outObject;

                    if (!dictionary.TryGetValue(key, out outObject)) {
                        // the key was not found as a possible replacement, throw exception
                        throw new FormatException($"The parameter \"{key}\" was not present in the lookup dictionary");
                    }

                    // we now have the replacement value, add the value to the output string
                    outputString.Append(outObject);

                    // jump to next state
                    continue;
                } // if }
                else {
                    // character has no special meaning, add it to the current key
                    currentKey.Append(formatString[index]);
                    // move onto next character
                    index++;
                    continue;
                } // else
            } // if inside brace
        } // while

        // after the loop, if all braces were balanced, we should be outside all braces
        // if we're not, the input string was misformatted.
        if (insideBraces) {
            throw new FormatException("The format string ended before the parameter was closed.");
        }

        return outputString.ToString();
    }

    /// <summary>
    /// Creates a Dictionary from an objects properties, with the Key being the property's
    /// name and the Value being the properties value (of type object)
    /// </summary>
    /// <param name="properties">An object who's properties will be used</param>
    /// <returns>A <see cref="Dictionary"/> of property values </returns>
    private static Dictionary<string, object> GetPropertiesDictionary(object properties) {
        Dictionary<string, object> values = null;
        if (properties != null) {
            values = new Dictionary<string, object>();
            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(properties);
            foreach (PropertyDescriptor prop in props) {
                values.Add(prop.Name, prop.GetValue(properties));
            }
        }
        return values;
    }
}

В конечном счете, вся логика сводится к 10 основным состояниям - поскольку, когда конечный автомат находится вне скобки и аналогично внутри скобки, следующим символом является либо открытая скобка, экранированная открытая скобка, закрытая скобка, экранированная закрытая скобка, или обычный символ. Каждое из этих условий обрабатывается индивидуально по мере прохождения цикла, добавляя символы либо к выводу StringBuffer, либо к клавише StringBuffer. Когда параметр закрыт, значение ключа StringBuffer используется для поиска значения параметра в словаре, который затем помещается в вывод StringBuffer.

EDIT:

Я превратил это в полноценный проект на https://github.com/crozone/FormatWith

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...