Форматування іменних рядків у C #


156

Чи є якийсь спосіб відформатувати рядок по імені, а не позиції в C #?

У python я можу зробити щось на зразок цього прикладу (безсоромно вкраденого звідси ):

>>> print '%(language)s has %(#)03d quote types.' % \
      {'language': "Python", "#": 2}
Python has 002 quote types.

Чи можна це зробити в C #? Скажімо, наприклад:

String.Format("{some_variable}: {some_other_variable}", ...);

Бути в змозі зробити це за допомогою назви змінної було б добре, але і словник прийнятний.


Я також пропускаю цього з Рубі.
JesperE

Я вважаю, що ваш приклад є надто спрощеним і спонукає людей давати вам корисні відповіді. Можливо, використання змінної більше одного разу в рядку було б більш показовим.
Клин

Власне, СПЕЦИФІЧНА плутанина - це використання String.Format. Це піддається таким відповідям, як моя, які не корисні, оскільки вони не орієнтовані на змінну, але є точними, наскільки це стосується String.Format.
Джон Руді

1
Виклик до String.Format, очевидно, надуманий приклад. Якщо, звичайно, ви не знали, що викликати String.Format з еліпсами неможливо. Проблема полягала в тому, що я не вважав, що хотів, щоб форматування відбулося за названими параметрами, а не позицією, яка була встановлена.
Джейсон Бейкер

FYI: Надісланий користувачеві MS Connect голос із проханням зробити його стандартною функцією фреймворку. Для всіх, хто цікавиться, зверніться до них: visualstudio.uservoice.com/forums/121579-visual-studio/…
JohnLBevan

Відповіді:


130

Не існує вбудованого методу для вирішення цього питання.

Ось один метод

string myString = "{foo} is {bar} and {yadi} is {yada}".Inject(o);

Ось ще один

Status.Text = "{UserName} last logged in at {LastLoginDate}".FormatWith(user);

Третій вдосконалений метод, частково заснований на двох вище , від Філа Хакка


11
Я був дуже радий використовувати FormatWith (), але хотів вказати на проблему, на яку я нещодавно зіткнувся. Реалізація покладається на DataBinder від System.Web.UI, який не підтримується в SQL CLR. Inject (o) не покладається на зв'язувач даних, що зробило його корисним для заміни багатотокенів у моєму об'єкті CLR CLR.
EBarr

1
Можливо, ви можете оновити перше речення своєї відповіді. Строкова інтерполяція є у C # і VB протягом декількох місяців (нарешті ...). Ваша відповідь знаходиться вгорі, тому вона може бути корисною читачам, якщо ви зможете зв’язати їх з деякими оновленими ресурсами .NET.
miroxlav

1
@miroxlav це насправді не те саме. Ви не можете передавати інтерпольовані рядки навколо: stackoverflow.com/q/31987232/213725
DixonD

@DixonD - ти напевно правий, але це не було їх метою. У зв'язаному питанні та питаннях ОП намагається посилатися на ім’я змінної ще до її існування. Не дуже гарна ідея, але якщо хтось наполягає на цьому, він може побудувати спеціалізований аналізатор. Але я б не псував це з загальною концепцією інтерполяції рядків.
miroxlav

44

У мене є реалізація, яку я щойно опублікував у своєму блозі: http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx

У ньому розглядаються деякі проблеми, які виникають у цих інших реалізацій із усуненням дужок. У публікації є подробиці. Це робить і DataBinder.Eval, але все ще дуже швидко.


3
Код, доступний для завантаження у цій статті 404. Я теж хотів би це побачити.
квентин-зірин

2
@qes: оновлене посилання було розміщено в коментарях: code.haacked.com/util/NamedStringFormatSolution.zip
Der Hochstapler

3
@OliverSalzburg: Я вже деякий час використовую SmartFormat для всіх моїх потреб у форматуванні. github.com/scottrippey/SmartFormat
-starin

@qes: Чи не заперечували б ви написати та відповісти про це та показати, як це працює? Виглядає цікаво
Der Hochstapler

@qes: Ви обов'язково додасте SmartFormat як відповідь, оскільки це дуже приємно та активно підтримується (2015).
Разван Флавій Панда

42

Інтерпольовані рядки були додані до C # 6.0 та Visual Basic 14

Обидва були представлені за допомогою нового компілятора Roslyn у Visual Studio 2015 .

  • C # 6.0:

    return "\{someVariable} and also \{someOtherVariable}" АБО
    return $"{someVariable} and also {someOtherVariable}"

  • VB 14:

    return $"{someVariable} and also {someOtherVariable}"

Заслуговують на увагу функції (у ID Visual Studio 2015):

  • підтримується синтаксичне забарвлення - виділяються змінні, що містяться в рядках
  • Рефакторинг підтримується - під час перейменування також можуть бути перейменовані змінні, що містяться в рядках
  • насправді підтримуються не тільки імена змінних, але й вирази - наприклад, не тільки {index}працює, але й{(index + 1).ToString().Trim()}

Насолоджуйтесь! (і натисніть "Надіслати посмішку" у VS)


2
Питання позначено тегом .net 3.5, тому ваша інформація є дійсною, але це не альтернатива
Дуглас Гандіні

1
@miroxlav - Ви праві щодо рамкової версії. Інтерполяція рядків якраз залежить від нового компілятора Roslyn, який використовується у VS 2015.
Дуглас Гандіні

2
Це також не буде працювати, якщо ваш рядок формату не буде введено в сам код. тобто він не працюватиме, якщо ваш форматний рядок надходить із зовнішнього джерела, наприклад, конфігураційного файла чи бази даних.
Крейг Бретт

40

Ви також можете використовувати анонімні типи на зразок цього:

    public string Format(string input, object p)
    {
        foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(p))
            input = input.Replace("{" + prop.Name + "}", (prop.GetValue(p) ?? "(null)").ToString());

        return input;
    }

Звичайно, знадобиться більше коду, якщо ви також хочете проаналізувати форматування, але ви можете відформатувати рядок, використовуючи цю функцію, як:

Format("test {first} and {another}", new { first = "something", another = "something else" })

1
Ідеально підходить для тих, хто з нас ще працює на 2.0. Так, я знаю .... Це рішення просте і зрозуміле легко. І ЦЕ РОБОТАЄ !!!
Бред Брюс

14

Здається, не існує способу зробити це поза коробкою. Хоча здається можливим реалізувати власні, IFormatProviderщо посилаються на IDictionaryзначення для.

var Stuff = new Dictionary<string, object> {
   { "language", "Python" },
   { "#", 2 }
};
var Formatter = new DictionaryFormatProvider();

// Interpret {0:x} where {0}=IDictionary and "x" is hash key
Console.WriteLine string.Format(Formatter, "{0:language} has {0:#} quote types", Stuff);

Виходи:

Python має 2 типи цитат

Застереження полягає в тому, що ви не можете змішувати FormatProviders, тому не можна використовувати одночасно фантазійне форматування тексту.


1
+1 для викладення, ІМХО, найкращий концептуальний метод, який добре реалізується на сайті mo.notono.us/2008/07/c-stringinject-format-strings-by-key.html - інші посади включають це, але вони також запропонувати методи, що базуються на рефлексії, які, ІМХО, є досить злими
Адам Ральф

9

Сам фреймворк не забезпечує способу цього зробити, але ви можете поглянути на цю посаду Скотта Хензельмана. Приклад використання:

Person p = new Person();  
string foo = p.ToString("{Money:C} {LastName}, {ScottName} {BirthDate}");  
Assert.AreEqual("$3.43 Hanselman, {ScottName} 1/22/1974 12:00:00 AM", foo); 

Цей код Джеймса Ньютона-Кінга схожий і працює з підвластивостями та індексами,

string foo = "Top result for {Name} was {Results[0].Name}".FormatWith(student));

Код Джеймса покладається на System.Web.UI.DataBinder для розбору рядка і вимагає посилання System.Web, що деякі люди не люблять робити в не веб-додатках.

EDIT: О, і вони прекрасно працюють з анонімними типами, якщо у вас немає об’єкта з властивостями, готовими до цього:

string name = ...;
DateTime date = ...;
string foo = "{Name} - {Birthday}".FormatWith(new { Name = name, Birthday = date });


4

Я думаю, що найближчим ти отримаєш індексований формат:

String.Format("{0} has {1} quote types.", "C#", "1");

Є також String.Replace (), якщо ви готові зробити це в кілька кроків і переконаєтесь, що своїх змінних ви не знайдете більше ніде в рядку:

string MyString = "{language} has {n} quote types.";
MyString = MyString.Replace("{language}", "C#").Replace("{n}", "1");

Розгорнувши це, щоб використовувати Список:

List<KeyValuePair<string, string>> replacements = GetFormatDictionary();  
foreach (KeyValuePair<string, string> item in replacements)
{
    MyString = MyString.Replace(item.Key, item.Value);
}

Ви можете це зробити і зі словником <string, string>, ітерацією колекцій. однолінійний:

replacements.ForEach(delegate(KeyValuePair<string,string>) item) { MyString = MyString.Replace(item.Key, item.Value);});

Лямбда була б ще простішою, але я все ще на .Net 2.0. Також зауважте, що продуктивність .Replace () не є зоряною, коли вона використовується ітеративно, оскільки рядки в .Net незмінні. Крім того, це вимагає, щоб MyStringзмінна була визначена таким чином, щоб вона була доступною для делегата, тому вона ще не є ідеальною.


Ну, це не найкрасивіше рішення, але зараз я з цим іду. Єдине, що я робив інакше, це використовувати StringBuilder замість рядка, щоб я не продовжував створювати нові рядки.
Джейсон Бейкер

3

Моя бібліотека з відкритим кодом, Regextra , підтримує форматування з назвою (серед іншого). Наразі він орієнтований на .NET 4.0+ та доступний на NuGet . У мене також є вступний запис про це в блозі: Regextra: допомагає зменшити свої проблеми {2} .

Названий біт форматування підтримує:

  • Основне форматування
  • Форматування вкладених властивостей
  • Форматування словника
  • Уникнення роздільників
  • Стандартне / призначене для користувача / IFormatProvider форматування рядків

Приклад:

var order = new
{
    Description = "Widget",
    OrderDate = DateTime.Now,
    Details = new
    {
        UnitPrice = 1500
    }
};

string template = "We just shipped your order of '{Description}', placed on {OrderDate:d}. Your {{credit}} card will be billed {Details.UnitPrice:C}.";

string result = Template.Format(template, order);
// or use the extension: template.FormatTemplate(order);

Результат:

Ми щойно доставили ваше замовлення віджету, розміщене 28.02.2014. Вашу {кредитну карту буде виставлено рахунок 1500 доларів США.

Перегляньте посилання GitHub проекту (вище) та вікі для інших прикладів.


Ого, це виглядає дивовижно, особливо, коли ми маємо справу з деякими більш складними прикладами формату, з яких стикається.
Ніколас Петерсен

2

Перевірте це:

public static string StringFormat(string format, object source)
{
    var matches = Regex.Matches(format, @"\{(.+?)\}");
    List<string> keys = (from Match matche in matches select matche.Groups[1].Value).ToList();

    return keys.Aggregate(
        format,
        (current, key) =>
        {
            int colonIndex = key.IndexOf(':');
            return current.Replace(
                "{" + key + "}",
                colonIndex > 0
                    ? DataBinder.Eval(source, key.Substring(0, colonIndex), "{0:" + key.Substring(colonIndex + 1) + "}")
                    : DataBinder.Eval(source, key).ToString());
        });
}

Зразок:

string format = "{foo} is a {bar} is a {baz} is a {qux:#.#} is a really big {fizzle}";
var o = new { foo = 123, bar = true, baz = "this is a test", qux = 123.45, fizzle = DateTime.Now };
Console.WriteLine(StringFormat(format, o));

Продуктивність досить гарна в порівнянні з іншими рішеннями.


1

Я сумніваюся, що це буде можливо. Перше, що спадає на думку, - як ви збираєтеся отримати доступ до локальних імен змінних?

Однак це може бути якийсь розумний спосіб, використовуючи вирази LINQ та Lambda.


@leppie: +1, якщо ви можете дати мені LINQ + Lambda, щоб це зробити; D (нормально +1 за відповідні відповіді)
user7116

Я також хотів би це побачити! Можливо, я прийму це виклик!
леппі

Я вважав, що це неможливо зробити з іменами змінних, але вклав це, якщо я помилявся. :) Немає жодного способу зробити це і зі словником?
Джейсон Бейкер

Я спробував, і десь дістався, але я вважав це занадто некрасивим і важким у використанні. Це виглядало б так: string s = format (f => f ("{hello} {world}", привіт, світ));
леппі

1

Ось один я зробив деякий час назад. Він розширює String методом Format, беручи один аргумент. Приємно те, що він використовує стандартний рядок. Формат, якщо ви надаєте простий аргумент, наприклад, int, але якщо ви використовуєте щось на зразок анонімного типу, він також буде працювати.

Приклад використання:

"The {Name} family has {Children} children".Format(new { Children = 4, Name = "Smith" })

Це призвело б до того, що "у сім'ї Сміта четверо дітей".

Це не робить божевільних обов'язкових речей, як масиви та індекси. Але це дуже просто і з високою продуктивністю.

    public static class AdvancedFormatString
{

    /// <summary>
    /// An advanced version of string.Format.  If you pass a primitive object (string, int, etc), it acts like the regular string.Format.  If you pass an anonmymous type, you can name the paramters by property name.
    /// </summary>
    /// <param name="formatString"></param>
    /// <param name="arg"></param>
    /// <returns></returns>
    /// <example>
    /// "The {Name} family has {Children} children".Format(new { Children = 4, Name = "Smith" })
    /// 
    /// results in 
    /// "This Smith family has 4 children
    /// </example>
    public static string Format(this string formatString, object arg, IFormatProvider format = null)
    {
        if (arg == null)
            return formatString;

        var type = arg.GetType();
        if (Type.GetTypeCode(type) != TypeCode.Object || type.IsPrimitive)
            return string.Format(format, formatString, arg);

        var properties = TypeDescriptor.GetProperties(arg);
        return formatString.Format((property) =>
            {
                var value = properties[property].GetValue(arg);
                return Convert.ToString(value, format);
            });
    }


    public static string Format(this string formatString, Func<string, string> formatFragmentHandler)
    {
        if (string.IsNullOrEmpty(formatString))
            return formatString;
        Fragment[] fragments = GetParsedFragments(formatString);
        if (fragments == null || fragments.Length == 0)
            return formatString;

        return string.Join(string.Empty, fragments.Select(fragment =>
            {
                if (fragment.Type == FragmentType.Literal)
                    return fragment.Value;
                else
                    return formatFragmentHandler(fragment.Value);
            }).ToArray());
    }


    private static Fragment[] GetParsedFragments(string formatString)
    {
        Fragment[] fragments;
        if ( parsedStrings.TryGetValue(formatString, out fragments) )
        {
            return fragments;
        }
        lock (parsedStringsLock)
        {
            if ( !parsedStrings.TryGetValue(formatString, out fragments) )
            {
                fragments = Parse(formatString);
                parsedStrings.Add(formatString, fragments);
            }
        }
        return fragments;
    }

    private static Object parsedStringsLock = new Object();
    private static Dictionary<string,Fragment[]> parsedStrings = new Dictionary<string,Fragment[]>(StringComparer.Ordinal);

    const char OpeningDelimiter = '{';
    const char ClosingDelimiter = '}';

    /// <summary>
    /// Parses the given format string into a list of fragments.
    /// </summary>
    /// <param name="format"></param>
    /// <returns></returns>
    static Fragment[] Parse(string format)
    {
        int lastCharIndex = format.Length - 1;
        int currFragEndIndex;
        Fragment currFrag = ParseFragment(format, 0, out currFragEndIndex);

        if (currFragEndIndex == lastCharIndex)
        {
            return new Fragment[] { currFrag };
        }

        List<Fragment> fragments = new List<Fragment>();
        while (true)
        {
            fragments.Add(currFrag);
            if (currFragEndIndex == lastCharIndex)
            {
                break;
            }
            currFrag = ParseFragment(format, currFragEndIndex + 1, out currFragEndIndex);
        }
        return fragments.ToArray();

    }

    /// <summary>
    /// Finds the next delimiter from the starting index.
    /// </summary>
    static Fragment ParseFragment(string format, int startIndex, out int fragmentEndIndex)
    {
        bool foundEscapedDelimiter = false;
        FragmentType type = FragmentType.Literal;

        int numChars = format.Length;
        for (int i = startIndex; i < numChars; i++)
        {
            char currChar = format[i];
            bool isOpenBrace = currChar == OpeningDelimiter;
            bool isCloseBrace = isOpenBrace ? false : currChar == ClosingDelimiter;

            if (!isOpenBrace && !isCloseBrace)
            {
                continue;
            }
            else if (i < (numChars - 1) && format[i + 1] == currChar)
            {//{{ or }}
                i++;
                foundEscapedDelimiter = true;
            }
            else if (isOpenBrace)
            {
                if (i == startIndex)
                {
                    type = FragmentType.FormatItem;
                }
                else
                {

                    if (type == FragmentType.FormatItem)
                        throw new FormatException("Two consequtive unescaped { format item openers were found.  Either close the first or escape any literals with another {.");

                    //curr character is the opening of a new format item.  so we close this literal out
                    string literal = format.Substring(startIndex, i - startIndex);
                    if (foundEscapedDelimiter)
                        literal = ReplaceEscapes(literal);

                    fragmentEndIndex = i - 1;
                    return new Fragment(FragmentType.Literal, literal);
                }
            }
            else
            {//close bracket
                if (i == startIndex || type == FragmentType.Literal)
                    throw new FormatException("A } closing brace existed without an opening { brace.");

                string formatItem = format.Substring(startIndex + 1, i - startIndex - 1);
                if (foundEscapedDelimiter)
                    formatItem = ReplaceEscapes(formatItem);//a format item with a { or } in its name is crazy but it could be done
                fragmentEndIndex = i;
                return new Fragment(FragmentType.FormatItem, formatItem);
            }
        }

        if (type == FragmentType.FormatItem)
            throw new FormatException("A format item was opened with { but was never closed.");

        fragmentEndIndex = numChars - 1;
        string literalValue = format.Substring(startIndex);
        if (foundEscapedDelimiter)
            literalValue = ReplaceEscapes(literalValue);

        return new Fragment(FragmentType.Literal, literalValue);

    }

    /// <summary>
    /// Replaces escaped brackets, turning '{{' and '}}' into '{' and '}', respectively.
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    static string ReplaceEscapes(string value)
    {
        return value.Replace("{{", "{").Replace("}}", "}");
    }

    private enum FragmentType
    {
        Literal,
        FormatItem
    }

    private class Fragment
    {

        public Fragment(FragmentType type, string value)
        {
            Type = type;
            Value = value;
        }

        public FragmentType Type
        {
            get;
            private set;
        }

        /// <summary>
        /// The literal value, or the name of the fragment, depending on fragment type.
        /// </summary>
        public string Value
        {
            get;
            private set;
        }


    }

}

1
private static Regex s_NamedFormatRegex = new Regex(@"\{(?!\{)(?<key>[\w]+)(:(?<fmt>(\{\{|\}\}|[^\{\}])*)?)?\}", RegexOptions.Compiled);

public static StringBuilder AppendNamedFormat(this StringBuilder builder,IFormatProvider provider, string format, IDictionary<string, object> args)
{
    if (builder == null) throw new ArgumentNullException("builder");
    var str = s_NamedFormatRegex.Replace(format, (mt) => {
        string key = mt.Groups["key"].Value;
        string fmt = mt.Groups["fmt"].Value;
        object value = null;
        if (args.TryGetValue(key,out value)) {
            return string.Format(provider, "{0:" + fmt + "}", value);
        } else {
            return mt.Value;
        }
    });
    builder.Append(str);
    return builder;
}

public static StringBuilder AppendNamedFormat(this StringBuilder builder, string format, IDictionary<string, object> args)
{
    if (builder == null) throw new ArgumentNullException("builder");
    return builder.AppendNamedFormat(null, format, args);
}

Приклад:

var builder = new StringBuilder();
builder.AppendNamedFormat(
@"你好,{Name},今天是{Date:yyyy/MM/dd}, 这是你第{LoginTimes}次登录,积分{Score:{{ 0.00 }}}",
new Dictionary<string, object>() { 
    { "Name", "wayjet" },
    { "LoginTimes",18 },
    { "Score", 100.4 },
    { "Date",DateTime.Now }
});

Вихід: 你好, wayjet, 今天 是 2011-05-04, 这 是 你 第 18 次 登录 , 积分 {100.40}


1

ось простий метод для будь-якого об’єкта:

    using System.Text.RegularExpressions;
    using System.ComponentModel;

    public static string StringWithFormat(string format, object args)
    {
        Regex r = new Regex(@"\{([A-Za-z0-9_]+)\}");

        MatchCollection m = r.Matches(format);

        var properties = TypeDescriptor.GetProperties(args);

        foreach (Match item in m)
        {
            try
            {
                string propertyName = item.Groups[1].Value;
                format = format.Replace(item.Value, properties[propertyName].GetValue(args).ToString());
            }
            catch
            {
                throw new FormatException("The format string is not valid");
            }
        }

        return format;
    }

А ось як ним користуватися:

 DateTime date = DateTime.Now;
 string dateString = StringWithFormat("{Month}/{Day}/{Year}", date);

вихід: 27.02.2012


0

Я реалізував це простий клас, який дублює функціональність String.Format (за винятком випадків використання класів). Ви можете використовувати словник або тип для визначення полів.

https://github.com/SergueiFedorov/NamedFormatString

C # 6.0 додає цю функціональність прямо в мовну специфікацію, так NamedFormatStringце і для зворотної сумісності.


0

Я вирішив це дещо по-іншому до існуючих рішень. Це робить ядро ​​заміненого названого елемента (не біт відображення, який деякі з них зробили). Це надзвичайно швидко і просто ... Це моє рішення:

/// <summary>
/// Formats a string with named format items given a template dictionary of the items values to use.
/// </summary>
public class StringTemplateFormatter
{
    private readonly IFormatProvider _formatProvider;

    /// <summary>
    /// Constructs the formatter with the specified <see cref="IFormatProvider"/>.
    /// This is defaulted to <see cref="CultureInfo.CurrentCulture">CultureInfo.CurrentCulture</see> if none is provided.
    /// </summary>
    /// <param name="formatProvider"></param>
    public StringTemplateFormatter(IFormatProvider formatProvider = null)
    {
        _formatProvider = formatProvider ?? CultureInfo.CurrentCulture;
    }

    /// <summary>
    /// Formats a string with named format items given a template dictionary of the items values to use.
    /// </summary>
    /// <param name="text">The text template</param>
    /// <param name="templateValues">The named values to use as replacements in the formatted string.</param>
    /// <returns>The resultant text string with the template values replaced.</returns>
    public string FormatTemplate(string text, Dictionary<string, object> templateValues)
    {
        var formattableString = text;
        var values = new List<object>();
        foreach (KeyValuePair<string, object> value in templateValues)
        {
            var index = values.Count;
            formattableString = ReplaceFormattableItem(formattableString, value.Key, index);
            values.Add(value.Value);
        }
        return String.Format(_formatProvider, formattableString, values.ToArray());
    }

    /// <summary>
    /// Convert named string template item to numbered string template item that can be accepted by <see cref="string.Format(string,object[])">String.Format</see>
    /// </summary>
    /// <param name="formattableString">The string containing the named format item</param>
    /// <param name="itemName">The name of the format item</param>
    /// <param name="index">The index to use for the item value</param>
    /// <returns>The formattable string with the named item substituted with the numbered format item.</returns>
    private static string ReplaceFormattableItem(string formattableString, string itemName, int index)
    {
        return formattableString
            .Replace("{" + itemName + "}", "{" + index + "}")
            .Replace("{" + itemName + ",", "{" + index + ",")
            .Replace("{" + itemName + ":", "{" + index + ":");
    }
}

Він використовується наступним чином:

    [Test]
    public void FormatTemplate_GivenANamedGuid_FormattedWithB_ShouldFormatCorrectly()
    {
        // Arrange
        var template = "My guid {MyGuid:B} is awesome!";
        var templateValues = new Dictionary<string, object> { { "MyGuid", new Guid("{A4D2A7F1-421C-4A1D-9CB2-9C2E70B05E19}") } };
        var sut = new StringTemplateFormatter();
        // Act
        var result = sut.FormatTemplate(template, templateValues);
        //Assert
        Assert.That(result, Is.EqualTo("My guid {a4d2a7f1-421c-4a1d-9cb2-9c2e70b05e19} is awesome!"));
    }

Сподіваюся, хтось вважає це корисним!


0

Навіть незважаючи на те, що прийнята відповідь дає кілька хороших прикладів,. Введення та деякі приклади Хаака не справляються із втечею. Багато людей також значною мірою покладаються на Regex (повільніше) або DataBinder.Eval, який недоступний у .NET Core та в деяких інших середовищах.

Маючи це на увазі, я написав простий аналізатор на основі стану, який протікає через символи, записуючи на StringBuilderвихід, символ за символом. Він реалізований як Stringметод (и) розширення і може приймати як вхідні дані, так Dictionary<string, object>і objectпараметри (використовуючи відображення).

Він обробляє необмежені рівні {{{escaping}}}та кидки, FormatExceptionколи введення містить незбалансовані дужки та / або інші помилки.

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. В кінці StringBufferповертається значення виводу .


-6
string language = "Python";
int numquotes = 2;
string output = language + " has "+ numquotes + " language types.";

Редагувати: Я повинен був сказати: "Ні, я не вірю, що ви хочете зробити, це підтримується C #. Це так близько, як ви збираєтеся отримати".


1
Мені цікаво проголосили голоси. Хтось хоче сказати мені, чому?
Кевін

1
Таким чином, string.format буде виконувати цю операцію 4 / TenThousandths секунди швидше Якщо цю функцію буде називатися тоною, ви можете помітити цю різницю. Але він принаймні відповідає на його запитання, а не просто каже йому зробити так, як він уже сказав, що не хоче цього робити.
Кевін

4
Я не проголосував за вас, але я б не реалізував це головним чином, тому що добре, я вважаю, що робити багато струнних конкатенацій некрасиво. Але це мій особистий погляд.
Джейсон Бейкер

Дивно, що за цей збір проголосували так багато. Подумайте про розширення своєї відповіді, що коли конкатенація не викликається часто, ви можете вважати "someString" + someVariable + "someOtherString"більш читаною. Ця стаття погоджується з вами.
Стівен Євріс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.