Nazwane formatowanie ciągów w C #


156

Czy istnieje sposób formatowania ciągu według nazwy zamiast pozycji w C #?

W Pythonie mogę zrobić coś takiego jak ten przykład (bezwstydnie skradziony stąd ):

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

Czy jest jakiś sposób, aby to zrobić w C #? Powiedz na przykład:

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

Przydałaby się możliwość zrobienia tego przy użyciu nazwy zmiennej, ale słownik też jest akceptowalny.


Tęsknię również za tym w Rubim.
JesperE

Myślę, że twój przykład jest zbyt uproszczony i skłania ludzi do udzielania niepomocnych odpowiedzi. Może użycie zmiennej więcej niż raz w ciągu znaków byłoby bardziej demonstracyjne.
Wedge

Właściwie SZCZEGÓLNY błąd polega na używaniu String.Format. To nadaje się do odpowiedzi, takich jak moja, które nie są pomocne, ponieważ nie są zorientowane na zmienne, ale są dokładne, jeśli chodzi o String.Format.
John Rudy

1
Wywołanie String.Format jest oczywiście wymyślonym przykładem. Chyba że nie byłeś świadomy tego, że wywołanie String.Format z elipsami nie jest możliwe. Problem polegał na tym, że nie powiedziałem, że chcę, aby formatowanie odbywało się według nazwanych parametrów, a nie pozycji, co zostało naprawione.
Jason Baker

FYI: Przesłane do User Voice MS Connect z prośbą o uczynienie tego standardową funkcją frameworka. Wszystkich zainteresowanych prosimy o głosowanie w górę: visualstudio.uservoice.com/forums/121579-visual-studio/ ...
JohnLBevan

Odpowiedzi:


130

Nie ma wbudowanej metody obsługi tego problemu.

Oto jedna metoda

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

Oto kolejny

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

Trzecia ulepszona metoda częściowo oparta na dwóch powyższych , autorstwa Phila Haacka


11
Jestem bardzo zadowolony z używania FormatWith (), ale chciałem wskazać na problem, z którym ostatnio się spotkałem. Implementacja opiera się na DataBinder z System.Web.UI, który nie jest obsługiwany w środowisku SQL CLR. Inject (o) nie opiera się na segregatorze danych, dzięki czemu jest przydatny do zastępowania wielu tokenów w moim obiekcie SQL CLR.
EBarr

1
Może mógłbyś zaktualizować pierwsze zdanie swojej odpowiedzi. Interpolacja ciągów jest obecna w C # i VB przez kilka miesięcy (w końcu ...). Twoja odpowiedź jest na górze, więc może być przydatne dla czytelników, jeśli możesz połączyć ich z niektórymi zaktualizowanymi zasobami .NET.
miroxlav

1
@miroxlav to nie jest to samo. Nie możesz przepuszczać ciągów interpolowanych w okolicy: stackoverflow.com/q/31987232/213725
DixonD

@DixonD - zdecydowanie masz rację, ale nie taki był ich cel. W połączonych pytaniach i odpowiedziach OP próbuje odwołać się do nazwy zmiennej, jeszcze zanim ona istnieje. Niezbyt dobry pomysł, ale jeśli ktoś się na to uprze, może skonstruować specjalistyczny parser. Ale nie zepsułbym tego ogólną koncepcją interpolacji ciągów.
miroxlav

44

Mam implementację, którą właśnie opublikowałem na moim blogu tutaj: http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx

Rozwiązuje pewne problemy, które mają te inne implementacje z ucieczką nawiasów klamrowych. Post zawiera szczegóły. Robi też coś DataBinder.Eval, ale nadal jest bardzo szybki.


3
Kod dostępny do pobrania w artykule 404. Też chciałbym to zobaczyć.
quentin-starin

2
@qes: Zaktualizowany link został opublikowany w komentarzach: code.haacked.com/util/NamedStringFormatSolution.zip
Der Hochstapler

3
@OliverSalzburg: Od jakiegoś czasu używam SmartFormat do wszystkich moich potrzeb związanych z formatowaniem, uwielbiam to. github.com/scottrippey/SmartFormat
quentin-starin

@qes: Czy mógłbyś napisać, odpowiedzieć na ten temat i pokazać, jak to działa? Wygląda interesująco
Der Hochstapler

@qes: Zdecydowanie powinieneś dodać SmartFormat jako odpowiedź, ponieważ jest bardzo ładny i aktywnie obsługiwany (2015).
Răzvan Flavius ​​Panda

42

Ciągi interpolowane zostały dodane do C # 6,0 i Visual Basic 14

Oba zostały wprowadzone za pomocą nowego kompilatora Roslyn w programie Visual Studio 2015 .

  • C # 6.0:

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

  • VB 14:

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

Godne uwagi funkcje (w Visual Studio 2015 IDE):

  • obsługiwane jest kolorowanie składni - zmienne zawarte w łańcuchach są podświetlane
  • obsługiwana jest refaktoryzacja - podczas zmiany nazwy zmienne zawarte w łańcuchach również ulegają zmianie
  • w rzeczywistości obsługiwane są nie tylko nazwy zmiennych, ale i wyrażenia - np. nie tylko {index}działa, ale także{(index + 1).ToString().Trim()}

Cieszyć się! (i kliknij „Wyślij uśmiech” w VS)


2
Pytanie jest oznaczony .NET 3.5 dlatego informacje są ważne, ale to nie jest alternatywą
Douglas Gandini

1
@miroxlav - Masz rację co do wersji frameworka. Interpolacja ciąg po prostu zależy od nowego kompilatora Roslyn stosowanych w VS 2015
Douglas Gandini

2
To również nie zadziała, chyba że ciąg formatu zostanie umieszczony w samym kodzie. tj. nie zadziała, jeśli łańcuch formatu pochodzi z zewnętrznego źródła, takiego jak plik konfiguracyjny lub baza danych.
Craig Brett

40

Możesz także użyć anonimowych typów, takich jak ten:

    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;
    }

Oczywiście wymagałoby to więcej kodu, jeśli chcesz również przeanalizować formatowanie, ale możesz sformatować ciąg przy użyciu tej funkcji, takiej jak:

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

1
Idealny dla tych z nas, którzy nadal korzystają z 2.0. Tak, wiem… To rozwiązanie jest proste i łatwe do zrozumienia. I DZIAŁA !!!
Brad Bruce,

14

Wydaje się, że nie ma sposobu, aby to zrobić po wyjęciu z pudełka. Chociaż wydaje się, że możliwe jest zaimplementowanie własnej, IFormatProviderktóra łączy się IDictionaryz wartościami for.

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);

Wyjścia:

Python ma 2 typy cytatów

Zastrzeżenie polega na tym, że nie można ich mieszać FormatProviders, więc fantazyjne formatowanie tekstu nie może być używane jednocześnie.


1
+1 do tworzenia konspektu, IMHO, najlepsza metoda koncepcyjna, która ma ładną implementację pod adresem mo.notono.us/2008/07/c-stringinject-format-strings-by-key.html - inne posty to obejmują, ale także zaproponować metody oparte na refleksji, które, IMHO, są raczej złe
Adam Ralph

9

Sam framework nie zapewnia sposobu, aby to zrobić, ale możesz spojrzeć na ten post Scotta Hanselmana. Przykładowe użycie:

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); 

Ten kod Jamesa Newtona-Kinga jest podobny i działa z właściwościami podrzędnymi i indeksami,

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

Kod Jamesa opiera się na System.Web.UI.DataBinder do parsowania ciągu i wymaga odwołania do System.Web, czego niektórzy ludzie nie lubią robić w aplikacjach innych niż internetowe.

EDYCJA: Aha i dobrze działają z typami anonimowymi, jeśli nie masz obiektu z gotowymi właściwościami:

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


4

Myślę, że najbliższy, jaki otrzymasz, to indeksowany format:

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

Jest też String.Replace (), jeśli chcesz to zrobić w wielu krokach i wierzyć, że nie znajdziesz swoich `` zmiennych '' nigdzie indziej w ciągu:

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

Rozwiń to, aby użyć listy:

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

Możesz to zrobić również za pomocą Dictionary <string, string>, iterując jego kolekcje .Keys, ale używając List <KeyValuePair <string, string >> możemy skorzystać z metody .ForEach () listy i skondensować ją z powrotem do jednowierszowy:

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

Lambda byłaby jeszcze prostsza, ale nadal korzystam z .Net 2.0. Należy również zauważyć, że wydajność .Replace () nie jest znakomita, gdy jest używana iteracyjnie, ponieważ ciągi znaków w .Net są niezmienne. Wymaga to MyStringrównież zdefiniowania zmiennej w taki sposób, aby była dostępna dla delegata, więc nie jest jeszcze doskonała.


Cóż, to nie jest najładniejsze rozwiązanie, ale na razie to właśnie zamierzam. Jedyną rzeczą, którą zrobiłem inaczej, było użycie StringBuildera zamiast ciągu, aby nie tworzyć nowych ciągów.
Jason Baker

3

Moja biblioteka Open Source, Regextra , obsługuje między innymi nazwane formatowanie. Obecnie jest przeznaczony dla platformy .NET 4.0+ i jest dostępny w NuGet . Mam również wprowadzający wpis na blogu na ten temat: Regextra: pomagam zmniejszyć (problemy) {2} .

Nazwany bit formatowania obsługuje:

  • Podstawowe formatowanie
  • Zagnieżdżone formatowanie właściwości
  • Formatowanie słownika
  • Ucieczka z ograniczników
  • Standardowe / niestandardowe / IFormatProvider formatowanie ciągu

Przykład:

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);

Wynik:

Właśnie wysłaliśmy Twoje zamówienie „Widżetu” złożone 28.02.2014. Twoja karta {credit} zostanie obciążona kwotą 1500,00 USD.

Sprawdź łącze GitHub projektu (powyżej) i wiki, aby uzyskać inne przykłady.


Wow, wygląda to niesamowicie, szczególnie gdy mamy do czynienia z niektórymi z trudniejszych przykładów formatów, które można napotkać.
Nicholas Petersen

2

Sprawdź ten:

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());
        });
}

Próba:

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));

Wydajność jest całkiem dobra w porównaniu z innymi rozwiązaniami.


1

Wątpię, żeby to było możliwe. Pierwszą rzeczą, która przychodzi na myśl, jest to, w jaki sposób zamierzasz uzyskać dostęp do lokalnych nazw zmiennych?

Jednak może istnieć sprytny sposób wykorzystania wyrażeń LINQ i Lambda, aby to zrobić.


@leppie: +1 jeśli możesz dać mi LINQ + Lambda, aby to zrobić; D (ok +1 za odpowiednią odpowiedź)
user7116

Też chciałbym to zobaczyć! Może podejmę to wyzwanie!
leppie

Pomyślałem, że nie da się tego zrobić z nazwami zmiennych, ale umieść to tam na wypadek, gdyby się myliłem. :) Nie ma też sposobu, aby to zrobić ze słownikiem?
Jason Baker

Próbowałem i trochę się gdzieś dostałem, ale uznałem to za zbyt brzydkie i trudne w użyciu. Wyglądałoby to następująco: string s = format (f => f ("{hello} {world}", hello, world));
leppie

1

Oto jeden, który zrobiłem jakiś czas temu. Rozszerza String o metodę Format pobierającą pojedynczy argument. Fajną rzeczą jest to, że użyje standardowego string.Format, jeśli podasz prosty argument, taki jak int, ale jeśli użyjesz czegoś takiego jak typ anonimowy, to też zadziała.

Przykładowe użycie:

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

Powoduje to „Rodzina Smithów ma czworo dzieci”.

Nie robi szalonych rzeczy wiążących, takich jak tablice i indeksatory. Ale to jest super proste i wydajne.

    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);
}

Przykład:

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 }
});

Wynik: 你好, wayjet, 今天 是 2011-05-04, 这 是 你 第 18 次 登录 , 积分 {100.40}


1

oto prosta metoda dla dowolnego obiektu:

    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;
    }

A oto jak z niego korzystać:

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

wyjście: 27.02.2012


0

Zaimplementowałem to jest prostą klasą, która powiela funkcjonalność String.Format (z wyjątkiem sytuacji, gdy używam klas). Aby zdefiniować pola, możesz użyć słownika lub typu.

https://github.com/SergueiFedorov/NamedFormatString

C # 6.0 dodaje tę funkcjonalność bezpośrednio do specyfikacji języka, więc NamedFormatStringjest to zgodne z poprzednimi wersjami.


0

Rozwiązałem to w nieco inny sposób niż dotychczasowe rozwiązania. Zajmuje się rdzeniem wymienionego elementu (a nie fragmentem odbicia, który niektórzy zrobili). Jest to niezwykle szybkie i proste ... Oto moje rozwiązanie:

/// <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 + ":");
    }
}

Jest używany w następujący sposób:

    [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!"));
    }

Mam nadzieję, że ktoś uzna to za przydatne!


0

Mimo że zaakceptowana odpowiedź zawiera kilka dobrych przykładów, .Inject, jak również niektóre przykłady Haacka nie obsługują ucieczki. Wiele z nich również w dużym stopniu polega na Regex (wolniej) lub DataBinder.Eval, które nie są dostępne w .NET Core oraz w niektórych innych środowiskach.

Mając to na uwadze, napisałem prosty parser oparty na maszynie stanów, który przesyła strumieniowo przez znaki, pisząc na StringBuilderwyjście, znak po znaku. Jest implementowany jako Stringmetoda (y) rozszerzenia i może przyjmować zarówno plik, jak Dictionary<string, object>iobject jako dane wejściowe parametry parametry (przy użyciu odbicia).

Obsługuje nieograniczone poziomy {{{escaping}}}i wyrzuca, FormatExceptiongdy dane wejściowe zawierają niezrównoważone nawiasy klamrowe i / lub inne błędy.

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;
    }
}

Ostatecznie cała logika sprowadza się do 10 głównych stanów - bo kiedy automat stanów znajduje się poza nawiasem, a także wewnątrz nawiasu, następny znak jest albo otwartym nawiasem, uciekającym otwartym nawiasem, zamkniętym nawiasem, uciekającym zamkniętym nawiasem, lub zwykła postać. Każdy z tych warunków jest obsługiwany indywidualnie w miarę postępu pętli, dodając znaki do wyjścia StringBufferlub klucza StringBuffer. Gdy parametr jest zamknięty, wartość klucza StringBufferjest używana do wyszukania wartości parametru w słowniku, która następnie jest umieszczana na wyjściu StringBuffer. Na koniec zwracana jest wartość wyjścia StringBuffer.


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

Edycja: Powinienem był powiedzieć: „Nie, nie wierzę, że to, co chcesz zrobić, jest obsługiwane przez C #. To jest tak bliskie, jak zamierzasz”.


1
Jestem ciekawa głosów negatywnych. Czy ktoś chce mi powiedzieć, dlaczego?
Kevin

1
Więc string.format wykona tę operację szybciej o 4 / Dziesięć tysięcy. Jeśli ta funkcja ma zostać nazwana toną, możesz zauważyć tę różnicę. Ale przynajmniej odpowiada na jego pytanie, zamiast po prostu powiedzieć mu, żeby zrobił to w ten sam sposób, w jaki już powiedział, że nie chce tego robić.
Kevin

4
Nie głosowałem na ciebie, ale nie zaimplementowałbym tego głównie dlatego, że cóż, uważam robienie wielu konkatenacji ciągów za brzydkie. Ale to mój osobisty pogląd.
Jason Baker

Dziwne, że tak dużo głosów spadło. Rozważ rozszerzenie swojej odpowiedzi, że gdy konkatenacja nie jest wywoływana często, możesz uznać ją za "someString" + someVariable + "someOtherString"bardziej czytelną. Ten artykuł zgadza się z tobą.
Steven Jeuris
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.