Как создать более удобный синтаксис string.format?
Мне нужно создать очень длинную строку в программе и использовать String.Format. Проблемой, с которой я столкнулся, является отслеживание всех чисел, когда у вас более 8-10 параметров.
Возможно ли создать некоторую форму перегрузки, которая примет синтаксис, подобный этому?
String.Format("You are {age} years old and your last name is {name} ",
{age = "18", name = "Foo"});
Ответы
Ответ 1
Как насчет следующего, которое работает как для анонимных типов (пример ниже), так и для обычных типов (сущности домена и т.д.):
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
не совсем то же самое, но вроде spoofing его... используйте метод расширения, словарь и небольшой код:
что-то вроде этого...
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;
}
}
Ответ 3
примитивная реализация:
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"}});
Вы также можете использовать анонимный класс, но это намного медленнее из-за отражения, которое вам нужно.
Для реальной реализации вы должны использовать регулярное выражение для
- позволяют избежать {}
- проверьте, есть ли заполнители, которые не заменяются, что, скорее всего, является ошибкой программирования.
Ответ 4
Как насчет того, будет ли age/name переменной в вашем приложении. Таким образом, вам нужен синтаксис вида, чтобы сделать его почти уникальным, например {age_1}?
Если у вас есть проблемы с 8-10 параметрами: почему бы не использовать
"You are " + age + " years old and your last name is " + name + "
Ответ 5
Начиная с С# 6, этот тип интерполяции строк теперь можно использовать с помощью нового синтаксиса string >
var formatted = $"You are {age} years old and your last name is {name}";
Ответ 6
Хотя С# 6.0 теперь может делать это со строчной интерполяцией, иногда это необходимо делать с динамическими строками формата во время выполнения. Я не смог использовать другие методы, которые требуют DataBinder.Eval из-за того, что они не доступны в .NET Core, и были недовольны работой решений 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 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