动态字符串插值

38
"管理员的待办工作"
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(ReplaceMacro("{job.Name} job for admin", new Job { Id = 1, Name = "Todo", Description="Nothing" }));
        Console.ReadLine();
    }

    static string ReplaceMacro(string value, Job job)
    {
        return value; //Output should be "Todo job for admin"
    }
}

class Job
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

9
基本上是不行的。这不是插值字符串文字的工作方式。字符串格式化(或转换为 FormattableString)会立即完成。 - Jon Skeet
虽然没有什么阻止你使用字符串替换来实现一个朴素的解决方案。根据情况,它可能会有性能问题,但你可以这样做。 - Lasse V. Karlsen
你需要自己解析字符串。 - juharr
我认为你所要求的是一个FormattableString,它是由编译器输出的插值字符串对象。搜索“Interpolated strings: advanced usages - Meziantou's blog”可以找到一种技术,该技术从FormattableString派生出一个类来实现你所要求的功能。 - Suncat2000
15个回答

0

如果你真的需要这个,你可以使用Roslyn来创建字符串-类实现,例如:

var stringToInterpolate = "$@\"{{job.Name}} job for admin\"";
var sourceCode = $@"
using System;
class RuntimeInterpolation(Job job) 
{{
    public static string Interpolate() =>
        {stringToInterpolate};
}}";

然后制作一个汇编

var assembly = CompileSourceRoslyn(sourceCode);
var type = assembly.GetType("RuntimeInterpolation");
var instance = Activator.CreateInstance(type);
var result = (string) type.InvokeMember("Interpolate",
BindingFlags.Default | BindingFlags.InvokeMethod, null, instance, new object[] {new Job { Id = 1, Name = "Todo", Description="Nothing" }});
Console.WriteLine(result);

你将在运行时获取此代码,并获得结果(还需要将您的作业类链接附加到该程序集)

using System;
class RuntimeInterpolation(Job job) 
{
    public static string Interpolate() =>
        $@"{job.Name} job for admin";
}

您可以在这里阅读有关如何实现CompileSourceRoslyn的信息 Roslyn-编译简单类:“找不到字符串类型或命名空间名称…”


修复:var stringToInterpolate = "$@"{job.Name} 管理员的工作""; - user13960138
或者你可以将名称传递给方法,这样它将是var stringToInterpolate = "$@"{name} job for admin""; 类RuntimeInterpolation(字符串名称) { public static string Interpolate() => {stringToInterpolate}; } - user13960138
如果您有额外的信息或代码要添加到您的答案中,请单击答案下方的 [编辑] 按钮进行修改。在评论中编写代码很困难,而且评论可能会被删除。 - David Buck

0

@ThePerplexedOne的答案更好,但如果你真的需要避免字符串插值,那么

static string ReplaceMacro(string value, Job job)
{
    return value?.Replace("{job.Name}", job.Name); //Output should be "Todo job for admin"
}

0

你应该将你的函数改为:

static string ReplaceMacro(Job obj, Func<dynamic, string> function)
{
    return function(obj);
}

然后调用它:

Console.WriteLine(
    ReplaceMacro(
        new Job { Id = 1, Name = "Todo", Description = "Nothing" },
        x => $"{x.Name} job for admin"));

0
我想到的实现与giganoide非常相似,允许调用者替换模板中使用的名称以插值数据。这可以适应像提供匿名类型之类的事情。如果提供了interpolateDataAs名称,则将使用该名称;否则,如果它不是匿名类型,则默认为类型名称;否则,它期望"data"(或特定属性的名称)。它被编写为可注入依赖项,但仍应作为扩展方法工作。
public interface ITemplateInterpolator
{
    /// <summary>
    /// Attempt to interpolate the provided template string using 
    /// the data provided.
    /// </summary>
    /// <remarks>
    /// Templates may want to use a meaninful interpolation name
    /// like "enquiry.FieldName" or "employee.FieldName" rather than 
    /// "data.FieldName". Use the interpolateDataAs to pass "enquiry" 
    /// for example to substitute the default "data" prefix.
    /// </remarks>
    string? Interpolate<TData>(string template, TData data, string? interpolateDataAs = null) where TData : class;
}

public class TemplateInterpolator : ITemplateInterpolator
{
    /// <summary>
    /// <see cref="ITemplateInterpolator.Interpolate(string, dynamic, string)"/>
    /// </summary>
    string? ITemplateInterpolator.Interpolate<TData>(string template, TData data, string? interpolateDataAs)
    {
        if (string.IsNullOrEmpty(template))
            return template;

        if (string.IsNullOrEmpty(interpolateDataAs))
        {
            interpolateDataAs = !typeof(TData).IsAnonymousType() ? typeof(TData).Name : nameof(data);
        }

        var parsed = Regex.Replace(template, @"{(?<exp>[^}]+)}", match =>
        {
            var param = Expression.Parameter(typeof(TData), interpolateDataAs);
            var e = System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(new[] { param }, null, match.Groups["exp"].Value);
            return (e.Compile().DynamicInvoke(data) ?? string.Empty).ToString();
        });
        return parsed;
    }

用于检测匿名类型:

    public static bool IsAnonymousType(this Type type)
    {
        if (type.IsGenericType)
        {
            var definition = type.GetGenericTypeDefinition();
            if (definition.IsClass && definition.IsSealed && definition.Attributes.HasFlag(TypeAttributes.NotPublic))
            {
                var attributes = definition.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false);
                return (attributes != null && attributes.Length > 0);
            }
        }
        return false;
    }

测试套件:

[TestFixture]
public class WhenInterpolatingATemplateString
{

    [TestCase("")]
    [TestCase(null)]
    public void ThenEmptyValueReturedWhenNoTemplateProvided(string? template)
    {
        ITemplateInterpolator testInterpolator = new TemplateInterpolator();
        var testData = new TestData { Id = 14, Name = "Test" };

        var result = testInterpolator.Interpolate(template!, testData);
        Assert.That(result, Is.EqualTo(template));
    }

    [Test]
    public void ThenTheTypeNameIsUsedForTheDataReferenceForDefinedClasses()
    {
        ITemplateInterpolator testInterpolator = new TemplateInterpolator();
        var testData = new TestData { Id = 14, Name = "Test" };
        string template = "This is a record named \"{TestData.Name}\" with an Id of {testdata.Id}."; // case insensitive.
        string expected = "This is a record named \"Test\" with an Id of 14.";

        var result = testInterpolator.Interpolate(template, testData);
        Assert.That(result, Is.EqualTo(expected));
    }

    [Test]
    public void ThenTheDefaultNameIsUsedForTheDataReferenceForAnonymous()
    {
        ITemplateInterpolator testInterpolator = new TemplateInterpolator();
        var testData = new { Id = 14, Name = "Test" };
        string template = "This is a record named \"{data.Name}\" with an Id of {data.Id}.";
        string expected = "This is a record named \"Test\" with an Id of 14.";

        var result = testInterpolator.Interpolate(template, testData);
        Assert.That(result, Is.EqualTo(expected));
    }

    [Test]
    public void ThenTheProvidedDataReferenceNameOverridesTheTypeName()
    {
        ITemplateInterpolator testInterpolator = new TemplateInterpolator();
        var testData = new TestData { Id = 14, Name = "Test" };
        string template = "This is a record named \"{otherData.Name}\" with an Id of {otherData.Id}.";
        string expected = "This is a record named \"Test\" with an Id of 14.";

        var result = testInterpolator.Interpolate(template, testData, "otherData");
        Assert.That(result, Is.EqualTo(expected));
    }

    [Test]
    public void ThenExceptionIsThrownWhenTemplateReferencesUnknownDataValues()
    {
        ITemplateInterpolator testInterpolator = new TemplateInterpolator();
        var testData = new TestData { Id = 14, Name = "Test" };
        string template = "This is a record named \"{testData.Name}\" with an Id of {testData.Id}. {testData.ExtraDetails}";

        Assert.Throws<ParseException>(() => { var result = testInterpolator.Interpolate(template, testData, "testData"); });
    }

    [Test]
    public void ThenDataFormattingExpressionsAreApplied()
    {
        ITemplateInterpolator testInterpolator = new TemplateInterpolator();
        var testData = new { Id = 14, Name = "Test", IsActive = true, EffectiveDate = DateTime.Today };
        string template = "The active state is {data.IsActive?\"Yes\":\"No\"}, Effective {data.EffectiveDate.ToString(\"yyyy-MM-dd\")}";
        string expected = "The active state is Yes, Effective " + DateTime.Today.ToString("yyyy-MM-dd");

        var result = testInterpolator.Interpolate(template, testData);
        Assert.That(result, Is.EqualTo(expected));
    }

    private class TestData
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

}

-1

我真的不明白你的ReplaceMacro方法的意义...

但这是它应该工作的方式:

class Program
{
    static void Main(string[] args)
    {
        var job = new Job { Id = 1, Name = "Todo", Description = "Nothing" };
        Console.WriteLine($"{job.Name} job for admin");
        Console.ReadLine();
    }
}

但是如果你真的想要它具有动态感觉,你的ReplaceMacro方法应该只接受一个参数,即工作:

static string ReplaceMacro(Job job)
{
    return $"{job.Name} job for admin.";
}

并像这样使用:

var job = new Job { Id = 1, Name = "Todo", Description = "Nothing" };
Console.WriteLine(ReplaceMacro(job));

或类似的东西。


3
我认为这个想法是将字符串从配置文件中读取,然后应用动态插值。 - juharr
@orenrevenge 为什么你要回复一个五年前的问题? - ThePerplexedOne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接