使用字符串插值的重载字符串方法

15
为什么字符串插值偏好使用带有 string 的方法重载而不是 IFormattable
想象以下情景:
static class Log {
    static void Debug(string message);
    static void Debug(IFormattable message);
    static bool IsDebugEnabled { get; }
}

我有一些对象的ToString()函数非常耗费资源。以前,我做了以下的处理:

if (Log.IsDebugEnabled) Log.Debug(string.Format("Message {0}", expensiveObject));
现在,我希望在Debug(IFormattable)内部有IsDebugEnabled逻辑,并且仅在必要时对消息中的对象调用ToString()。
现在,我想要在Debug(IFormattable)函数内加入IsDebugEnabled逻辑,仅在必要时才对消息中的对象调用ToString()。
Log.Debug($"Message {expensiveObject}");

然而,这调用了Debug(string)重载。


插值字符串解析为 string,但具有对 IFormattable 的隐式类型转换。因此,如果您使用 IFormattable msg = $"Message {expensiveObject}"; Log.Debug(msg);,则应该可以正常工作。请参见 https://msdn.microsoft.com/en-gb/library/dn961160.aspx#Anchor_0 - spender
查看TryRoslyn上的示例,IFormattable只是幕后黑手,真正起作用的是Format() :) - PTwr
1
你应该在这里真正使用 ConditionalAttribute - leppie
有一个非常好的解决方法,可以使用扩展方法http://pvlerick.github.io/2016/01/poking-the-csharp-compiler-overload-resolution-for-string-and-formattablestring - tykovec
4个回答

12
这是Roslyn团队的一个深思熟虑的决定
我们普遍认为,库通常会使用不同的API名称来表示执行不同任务的方法。因此,FormattableString和String之间的重载解析差异无关紧要,所以string可能会胜出。因此,我们应该坚持一个简单的原则:插值字符串就是字符串。故事结束了。
在链接中还有更多讨论,但重点是他们希望您使用不同的方法名称。
一些库API确实希望消费者使用FormattableString,因为它更安全或更快。接受string和接受FormattableString的API实际上执行不同的操作,因此不应在相同的名称上进行重载。

5
频繁使用时,这真的很烦人。 - Felix K.
3
很遗憾构造函数不能有不同的名称。考虑到这些人是编写语言的人,他们应该理解这个问题,因此这些辩解并不充分。 - tukra

11

看到您问为什么不能这样做,我想指出您实际上可以这样做。

您只需要欺骗编译器,使其优先使用FormattableString重载。我在这里详细解释了:https://robertengdahl.blogspot.com/2016/08/how-to-overload-string-and.html

以下是测试代码:

public class StringIfNotFormattableStringAdapterTest
{
    public interface IStringOrFormattableStringOverload
    {
        void Overload(StringIfNotFormattableStringAdapter s);
        void Overload(FormattableString s);
    }

    private readonly IStringOrFormattableStringOverload _stringOrFormattableStringOverload =
        Substitute.For<IStringOrFormattableStringOverload>();

    public interface IStringOrFormattableStringNoOverload
    {
        void NoOverload(StringIfNotFormattableStringAdapter s);
    }

    private readonly IStringOrFormattableStringNoOverload _noOverload =
        Substitute.For<IStringOrFormattableStringNoOverload>();

    [Fact]
    public void A_Literal_String_Interpolation_Hits_FormattableString_Overload()
    {
        _stringOrFormattableStringOverload.Overload($"formattable string");
        _stringOrFormattableStringOverload.Received().Overload(Arg.Any<FormattableString>());
    }

    [Fact]
    public void A_String_Hits_StringIfNotFormattableStringAdapter_Overload()
    {
        _stringOrFormattableStringOverload.Overload("plain string");
        _stringOrFormattableStringOverload.Received().Overload(Arg.Any<StringIfNotFormattableStringAdapter>());
    }

    [Fact]
    public void An_Explicit_FormattableString_Detects_Missing_FormattableString_Overload()
    {
        Assert.Throws<InvalidOperationException>(
            () => _noOverload.NoOverload((FormattableString) $"this is not allowed"));
    }
}

这里是让这个功能生效的代码:

public class StringIfNotFormattableStringAdapter
{
    public string String { get; }

    public StringIfNotFormattableStringAdapter(string s)
    {
        String = s;
    }

    public static implicit operator StringIfNotFormattableStringAdapter(string s)
    {
        return new StringIfNotFormattableStringAdapter(s);
    }

    public static implicit operator StringIfNotFormattableStringAdapter(FormattableString fs)
    {
        throw new InvalidOperationException(
            "Missing FormattableString overload of method taking this type as argument");
    }
}

1
缺点是每次调用方法并传递字符串时都会产生额外的对象分配。对于一些使用情况,比如密集记录日志,这可能是不可取的。 我没有进行分析,但在这种情况下使用“struct”来避免GC压力可能是有意义的。 - angularsen
看起来博客已经不再可用了,但是有这个答案,它解释了隐式转换(从FormattableString到StringIfNotFormattableStringAdapter,但反过来不行)如何使FormattableString成为首选的解析方式。 - drizin

7

你无法强制编译器选择 IFormattable/FormattableString 而不是 String,但你可以让它选择 IFormattable/FormattableString 而不是 Object:

static class Log
{
    static void Debug(object message);
    static void Debug(IFormattable message);
    static void Debug(FormattableString message);
    static bool IsDebugEnabled { get; }
}

这种解决方案的成本是在接受 Object 参数的方法中多做一次 ToString() 调用。 (额外为 FormattableString 添加重载并不是必需的,但会简化查找使用插值字符串的位置。)

https://sharplab.io/#v2:D4AQTAjAsAUCEDYAE4kBkD2BzWS9IG9d8liS8QBmFRFAFiQBEBTAIwFcsAKDVgK2YBjAC5IAtswDOkgIZZmASjLkiMcupoBOLhOlzmAOgAqGAMrCATgEsAdtwUKA3MpIBfWC/xUayEAxYc3ACSAGIYFmIywsIyrAA2zOJSsvJKahqqGuRWAGZIXEGSAZwAojaxCQAmaVkkmbXq8Nq6KYYm5tZ2XA7O6Q3ufeQDnhTU8L7+bJxcYRFRMfHMHbZYSXqpI4SbJLn5hcVYZRXM1dv49Q0kTTrJ+sZmlivdTmd4A1nDg1djtKwYGHEkPspodyotKoQkPJhI4kO8NN5xvQkABZbqbC61WyiZgADwADswbJIrAA3ZgAeX4QlEAF4kAB2XqXdDYAwHLgAEgARCjbvJCHjCcSyZTqSJXNyXl8NJgsOyQVx4AAGAyzSLCLi8/mJAjKyUAGiQQqJJPJVIEIh6m0+biAA=== - Rune

3
你需要将它转换为 IFormattableFormattableString
Log.Debug((IFormattable)$"Message {expensiveObject}");

你可以使用一个巧妙的技巧作为将类型转换为 IFormattable 的简写:

public static class FormattableExtensions
{
    public static FormattableString FS(FormattableString formattableString)
    {
        return formattableString;
    }
}

并这样使用它:

Log.Debug(FS($"Message {expensiveObject}"));

我希望在生产环境中,JIT编译器可以将FS内联。

不错的技巧。现在它调用了正确的方法:TryRoslyn(检查IL以查看调用哪个重载)。注意:字符串->可格式化字符串使用FormattableStringFactory.Create完成。 - PTwr

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