Formattazione della stringa denominata in C #


156

Esiste un modo per formattare una stringa in base al nome anziché alla posizione in C #?

In Python, posso fare qualcosa di simile a questo esempio (rubato senza vergogna da qui ):

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

C'è un modo per farlo in C #? Di 'ad esempio:

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

Essere in grado di farlo usando un nome di variabile sarebbe bello, ma è accettabile anche un dizionario.


Mi manca anche questo da Ruby.
JesperE,

Penso che il tuo esempio sia troppo semplicistico e porti le persone a darti risposte inutili. Forse usare una variabile più di una volta nella stringa sarebbe più dimostrativo.
Wedge

In realtà, la confusione SPECIFICA è l'uso di String.Format. Ciò si presta a risposte come la mia, che non sono utili perché non sono orientate alle variabili, ma sono precise per quanto riguarda String.Format.
John Rudy,

1
La chiamata a String.Format è ovviamente un esempio inventato. A meno che, naturalmente, non sapessi che chiamare String.Format con i puntini di sospensione non è possibile. Il problema era che non avevo detto che volevo che la formattazione avvenisse in base ai parametri nominati anziché alla posizione, che è stata modificata.
Jason Baker,

Cordiali saluti: Presentato alla voce utente di MS Connect per richiedere che questo diventi una funzionalità standard del framework. Per chiunque sia interessato, si prega di aggiornare
JohnLBevan

Risposte:


130

Non esiste un metodo integrato per gestirlo.

Ecco un metodo

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

Eccone un altro

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

Un terzo metodo migliorato parzialmente basato sui due sopra , da Phil Haack


11
Sono stato molto contento di usare FormatWith (), ma volevo sottolineare un problema che mi sono imbattuto di recente. L'implementazione si basa su DataBinder da System.Web.UI, che non è supportato in CLR SQL. Inject (o) non si basa sul raccoglitore di dati, che lo ha reso utile per la sostituzione multi-token nel mio oggetto CLR SQL.
EBarr,

1
Forse puoi aggiornare la prima frase della tua risposta. L'interpolazione di stringhe è presente in C # e VB per alcuni mesi (finalmente ...). La tua risposta è in alto, quindi potrebbe essere utile per i lettori se riesci a collegarli ad alcune risorse .NET aggiornate.
miroxlav,

1
@miroxlav non è proprio lo stesso. Non è possibile passare le stringhe interpolate in giro: stackoverflow.com/q/31987232/213725
DixonD

@DixonD - hai sicuramente ragione ma non era il loro scopo. Nelle domande e risposte collegate, OP tenta di fare riferimento al nome della variabile ancora prima che esista. Non è una buona idea, ma se qualcuno insiste su questo, può costruire un parser specializzato. Ma non lo farei con il concetto generale di interpolazione delle stringhe.
miroxlav,

44

Ho un'implementazione che ho appena pubblicato sul mio blog qui: http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx

Risolve alcuni problemi che queste altre implementazioni hanno con la fuga di parentesi graffe. Il post ha dettagli. Fa anche la cosa DataBinder.Eval, ma è ancora molto veloce.


3
Il codice disponibile per il download in quell'articolo 404. Mi piacerebbe davvero vederlo anche io.
Quentin-starin,

2
@qes: nei commenti è stato inserito un link aggiornato: code.haacked.com/util/NamedStringFormatSolution.zip
Der Hochstapler,

3
@OliverSalzburg: Uso SmartFormat da un po 'di tempo per tutte le mie esigenze di formattazione, lo adoro. github.com/scottrippey/SmartFormat
quentin-starin

@qes: ti dispiacerebbe forse scrivere e rispondere a questo proposito e mostrare come funziona? Sembra interessante
Der Hochstapler,

@qes: dovresti assolutamente aggiungere SmartFormat come risposta in quanto è molto bello e supportato attivamente (2015).
Răzvan Flavius ​​Panda,

42

Le stringhe interpolate sono state aggiunte in C # 6.0 e Visual Basic 14

Entrambi sono stati introdotti attraverso il nuovo compilatore Roslyn in Visual Studio 2015 .

  • C # 6.0:

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

  • VB 14:

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

Funzionalità degne di nota (nell'IDE di Visual Studio 2015):

  • la colorazione della sintassi è supportata: le variabili contenute nelle stringhe sono evidenziate
  • il refactoring è supportato: durante la ridenominazione, anche le variabili contenute nelle stringhe vengono rinominate
  • in realtà non sono supportati solo i nomi delle variabili, ma sono supportate le espressioni , ad esempio non solo {index}funziona, ma anche{(index + 1).ToString().Trim()}

Godere! (e fai clic su "Invia un sorriso" nel VS)


2
La domanda è taggata con .net 3.5 quindi le tue informazioni sono valide ma non è un'alternativa
Douglas Gandini,

1
@miroxlav - Hai ragione sulla versione del framework. L'interpolazione delle stringhe dipende solo dal nuovo compilatore Roslyn utilizzato in VS 2015.
Douglas Gandini,

2
Anche questo non funzionerà a meno che la stringa di formato non venga inserita nel codice stesso. cioè non funzionerà se la tua stringa di formato proviene da una fonte esterna, come un file di configurazione o un database.
Craig Brett,

40

Puoi anche usare tipi anonimi come questo:

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

Ovviamente richiederebbe più codice se si desidera analizzare anche la formattazione, ma è possibile formattare una stringa utilizzando questa funzione come:

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

1
Perfetto per quelli di noi ancora su 2.0. Sì, lo so .... Questa soluzione è semplice e facile da capire. E FUNZIONA !!!
Brad Bruce,

14

Non sembra esserci un modo per farlo immediatamente. Tuttavia, sembra possibile implementare il proprio IFormatProviderche si collega a IDictionaryvalori 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);

Uscite:

Python ha 2 tipi di quotazioni

L'avvertenza è che non è possibile combinare FormatProviders, quindi la formattazione del testo di fantasia non può essere utilizzata contemporaneamente.


1
+1 per delineare, IMHO, il miglior metodo concettuale, che ha una buona implementazione su mo.notono.us/2008/07/c-stringinject-format-strings-by-key.html - gli altri post lo includono ma hanno anche proporre i metodi basati sulla riflessione che, secondo me, sono piuttosto malvagi
Adam Ralph,

9

Il framework stesso non fornisce un modo per farlo, ma puoi dare un'occhiata a questo post di Scott Hanselman. Esempio di utilizzo:

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

Questo codice di James Newton-King è simile e funziona con proprietà secondarie e indici,

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

Il codice di James si basa su System.Web.UI.DataBinder per analizzare la stringa e richiede il riferimento a System.Web, che ad alcune persone non piace fare in applicazioni non web.

EDIT: Oh e funzionano bene con tipi anonimi, se non hai un oggetto con proprietà pronte per questo:

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


4

Penso che il più vicino che otterrai sia un formato indicizzato:

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

C'è anche String.Replace (), se sei disposto a farlo in più passaggi e credi che non troverai le tue 'variabili' da nessun'altra parte nella stringa:

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

Espandendo questo per usare un elenco:

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

Potresti farlo anche con un dizionario <stringa, stringa> ripetendo le sue raccolte .Keys, ma utilizzando un elenco <KeyValuePair <stringa, stringa >> possiamo sfruttare il metodo List of .ForEach () e rimetterlo a un liner:

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

Una lambda sarebbe ancora più semplice, ma sono ancora su .Net 2.0. Si noti inoltre che le prestazioni .Replace () non sono stellari se utilizzate in modo iterativo, poiché le stringhe in .Net sono immutabili. Inoltre, ciò richiede che la MyStringvariabile sia definita in modo tale da essere accessibile al delegato, quindi non è ancora perfetta.


Bene, questa non è la soluzione più carina, ma è quello con cui sto andando per ora. L'unica cosa che ho fatto diversamente è stato usare StringBuilder invece di una stringa in modo da non continuare a creare nuove stringhe.
Jason Baker,

3

La mia libreria open source, Regextra , supporta la formattazione denominata (tra le altre cose). Attualmente è destinato a .NET 4.0+ ed è disponibile su NuGet . Ho anche un post di blog introduttivo al riguardo: Regextra: aiutarti a ridurre i tuoi (problemi) {2} .

Il bit di formattazione denominato supporta:

  • Formattazione di base
  • Formattazione delle proprietà nidificate
  • Formattazione del dizionario
  • Escaping dei delimitatori
  • Formattazione stringa standard / personalizzata / IFormatProvider

Esempio:

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

Risultato:

Abbiamo appena spedito il tuo ordine di "Widget", effettuato il 28/02/2014. La tua carta di credito verrà fatturata a $ 1.500,00.

Controlla il link GitHub del progetto (sopra) e la wiki per altri esempi.


Wow, questo sembra sorprendente, in particolare quando si ha a che fare con alcuni degli esempi di formato più difficili che si incontrano.
Nicholas Petersen,

2

Controlla questo:

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

Campione:

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

Le prestazioni sono abbastanza buone rispetto ad altre soluzioni.


1

Dubito che questo sarà possibile. La prima cosa che mi viene in mente è come hai accesso ai nomi delle variabili locali?

Tuttavia, potrebbe esserci un modo intelligente di usare le espressioni LINQ e Lambda per farlo.


@leppie: +1 se puoi darmi qualche LINQ + Lambda per farlo; D (ok +1 per avere una risposta pertinente)
user7116

Mi piacerebbe vederlo anche io! Forse accetterò quella sfida!
leppie,

Ho pensato che sarebbe stato impossibile avere a che fare con i nomi delle variabili, ma inseriscilo nel caso in cui mi sbagliassi. :) Non c'è modo di farlo nemmeno con un dizionario?
Jason Baker,

Ho provato, e ho ottenuto un po 'da qualche parte, ma l'ho ritenuto troppo brutto e difficile da usare. Sarebbe sembrato: stringa s = formato (f => f ("{ciao} {mondo}", ciao, mondo));
leppie,

1

Eccone uno che ho fatto tempo fa. Estende String con un metodo Format prendendo un singolo argomento. La cosa bella è che utilizzerà la stringa standard. Formatta se fornisci un argomento semplice come un int, ma se usi qualcosa di simile al tipo anonimo, funzionerà anche.

Esempio di utilizzo:

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

Si tradurrebbe in "La famiglia Smith ha 4 figli".

Non fa cose pazze come array e indicizzatori. Ma è super semplice e ad alte prestazioni.

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

Esempio:

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

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


1

ecco un metodo semplice per qualsiasi oggetto:

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

E qui come usarlo:

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

uscita: 27/02/2012


0

Ho implementato questa è una semplice classe che duplica la funzionalità di String.Format (tranne quando si usano le classi). È possibile utilizzare un dizionario o un tipo per definire i campi.

https://github.com/SergueiFedorov/NamedFormatString

C # 6.0 sta aggiungendo questa funzionalità direttamente nelle specifiche della lingua, così NamedFormatStringcome per la compatibilità con le versioni precedenti.


0

Ho risolto questo problema in modo leggermente diverso rispetto alle soluzioni esistenti. Fa il nucleo della sostituzione dell'oggetto nominato (non il bit di riflessione che alcuni hanno fatto). È estremamente veloce e semplice ... Questa è la mia soluzione:

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

Viene utilizzato nel modo seguente:

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

Spero che qualcuno lo trovi utile!


0

Anche se la risposta accettata fornisce alcuni buoni esempi, il .Inject e alcuni degli esempi di Haack non gestiscono la fuga. Molti si basano anche su Regex (più lento) o DataBinder.Eval che non è disponibile su .NET Core e in alcuni altri ambienti.

Con questo in mente, ho scritto un semplice parser basato su una macchina a stati che scorre attraverso i caratteri, scrivendo su un StringBuilderoutput, carattere per carattere. È implementato come Stringmetodo (i) di estensione e può richiedere sia a Dictionary<string, object>che aobject con parametri come input (usando la riflessione).

Gestisce livelli illimitati di {{{escaping}}}e genera FormatExceptionquando l'input contiene parentesi sbilanciate e / o altri errori.

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

Alla fine, tutta la logica si riduce in 10 stati principali: quando la macchina a stati si trova all'esterno di una parentesi e allo stesso modo all'interno di una parentesi, il personaggio successivo è un controvento aperto, un controvento aperto sfuggito, un controvento chiuso, un controvento chiuso sfuggito, o un personaggio ordinario. Ognuna di queste condizioni viene gestita individualmente mentre il ciclo procede, aggiungendo caratteri a un output StringBufferoa una chiave StringBuffer. Quando un parametro viene chiuso, il valore della chiave StringBufferviene utilizzato per cercare il valore del parametro nel dizionario, che viene quindi inserito nell'output StringBuffer. Alla fine, StringBufferviene restituito il valore dell'output .


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

Modifica: Quello che avrei dovuto dire era: "No, non credo che ciò che vuoi fare sia supportato da C #. Questo è il più vicino possibile."


1
Sono curioso dei voti negativi. Qualcuno vuole dirmi perché?
Kevin,

1
Quindi string.format eseguirà questa operazione 4 / TenThousandthsths di secondo più velocemente Se questa funzione verrà chiamata una tonnellata potresti notare quella differenza. Ma almeno risponde alla sua domanda invece di dirgli semplicemente di farlo nello stesso modo in cui ha già detto che non voleva farlo.
Kevin,

4
Non ti ho votato, ma non lo implementerei principalmente perché, trovo brutto fare molte concatenazioni di stringhe. Ma questa è la mia visione personale.
Jason Baker,

Strano che questo sia sceso ha votato così tanto. Considera di espandere la tua risposta, che quando la concatenazione non viene chiamata spesso potresti considerare "someString" + someVariable + "someOtherString"più leggibile. Questo articolo è d' accordo con te.
Steven Jeuris,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.