具有布尔格式的字符串插值

发布于 2025-01-10 01:23:53 字数 672 浏览 1 评论 0原文

如何为布尔值指定与其他类型的其他格式字符串一致的格式字符串?

给出以下代码:

double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;

string result = $"{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}";

我可以为每个基本类型指定一种格式,除了 bool 之外。我知道我可以这样做:

string result = $"{d:0.0}, {now:HH:mm}, time to party? {(isPartyTime ? "yes!" : "no")}";

但这仍然与其他类型不一致。

有没有一种方法可以使插值字符串中的布尔值保持一致?

PS我确实搜索了包含此链接的答案:

https://stackoverflow.com/questions/tagged /c%23+string-interpolation+boolean

令人惊讶的是结果为零。

How can I specify a format string for a boolean that's consistent with the other format strings for other types?

Given the following code:

double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;

string result = 
quot;{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}";

I can specify a format for every primitive type, except for bool it seems. I know I can do:

string result = 
quot;{d:0.0}, {now:HH:mm}, time to party? {(isPartyTime ? "yes!" : "no")}";

However this is still inconsistent with the other types.

Is there a way of formatting booleans in interpolated strings that is consistent?

P.S. I did search for an answer including this link:

https://stackoverflow.com/questions/tagged/c%23+string-interpolation+boolean

And surprisingly had zero results.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

来日方长 2025-01-17 01:23:53

不幸的是,不,没有。

根据 Microsoft ,具有格式字符串的唯一数据类型是:

  • 日期和时间类型 (DateTime, DateTimeOffset)
  • 枚举类型(从 System.Enum 派生的所有类型)
  • 数字类型(BigInteger、Byte、Decimal、Double、Int16、Int32、Int64、SByte、Single、UInt16、UInt32、UInt64)
  • Guid
  • TimeSpan

Boolean.ToString() 只能返回“True”或“False”。它甚至说,如果需要将其写入 XML,则需要手动执行 ToLowerCase() (由于缺乏字符串格式)。

Unfortunately, no, there isn't.

According to Microsoft, the only data types with format strings are:

  • Date and time types (DateTime, DateTimeOffset)
  • Enumeration types (all types derived from System.Enum)
  • Numeric types (BigInteger, Byte, Decimal, Double, Int16, Int32, Int64, SByte, Single, UInt16, UInt32, UInt64)
  • Guid
  • TimeSpan

Boolean.ToString() can only return "True" or "False". It even says, if you need to write it to XML, you need to manually perform ToLowerCase() (from the lack of string formatting).

诗化ㄋ丶相逢 2025-01-17 01:23:53

使用 C# 10.0?只需使用字符串插值处理程序

自定义字符串插值处理程序记录在此处这里

(我还没有任何 C# 10.0 功能的经验,但我将来会扩展这一部分 - 现在我仍然停留在 C# 上7.3 土地,因为我的日常工作项目依赖于 .NET Framework 4.8)

使用 C# 1.0 到 C# 9.0?

快速修复:Boolean 包装结构

如果您控制字符串格式调用站点,则只需更改要使用的 bool/Boolean 类型的值相反,可隐式转换的零开销值类型,例如:

public readonly struct YesNoBoolean : IEquatable<YesNoBoolean>
{
    // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators
    public static implicit operator Boolean  ( YesNoBoolean self ) => self.Value;
    public static implicit operator YesNoBoolean( Boolean value ) => new MyBoolean( value );

    // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/true-false-operators
    public static Boolean operator true( YesNoBoolean self ) => self.Value == true;
    public static Boolean operator false( YesNoBoolean self ) => self.Value == false;

    public YesNoBoolean( Boolean value )
    {
        this.Value = value;
    }

    public readonly Boolean Value;

    public override String ToString()
    {
        return this.Value ? "Yes" : "No";
    }

    // TODO: Override Equals, GetHashCode, IEquatable<YesNoBoolean>.Equals, etc.
}

因此您的示例调用站点变为:

double d = Math.PI;
DateTime now = DateTime.Now;
YesNoBoolean isPartyTime = true;  // <-- Yay for implicit conversion.

string result = $"{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}";

并且结果将是“3.1,21:03,聚会时间到了?是”

气泡破裂:不,你不能覆盖 Boolean.TrueStringFalseString

因为 Booleanstatic readonly String TrueString = "True"; 也被标记为 initonly 你不能使用反射覆盖它,所以这样做:

typeof(Boolean).GetField( "TrueString" )!.SetValue( obj: null, value: "Yes" );

...会给你一个运行时异常:

初始化类型“System.Boolean”后,无法设置 initonly 静态字段TrueString”。

通过操纵原始内存仍然是可能的,但这超出了这个问题的范围。

使用 IFormatProviderICustomFormatter

始终可以覆盖 String.Format 和内插字符串的方式(例如 $"Hello, { world}") 通过提供自定义的 IFormatProvider 进行格式化;尽管 String.Format 通过公开 Format 重载参数可以轻松实现这一点,但插值字符串却不然,相反,它会迫使您在某种程度上丑化您的代码。

  • 令人惊讶的是,.NET 中实现 IFormatProvider 的文档(仍然)不足。
    • 要记住的主要事情是,使用以下 3 个 formatType 参数之一调用 IFormatProvider.GetFormat(Type)
      • typeof(DateTimeFormatInfo)
      • typeof(NumberFormatInfo)
      • typeof(ICustomFormatter)
    • 在整个 .NET BCL 中,没有其他 typeof() 类型被传递到 GetFormat(至少就 ILSpy 和 RedGate 而言)反射器告诉我)。

魔法发生在 ICustomFormatter.Format 内部,并且实现它很简单:

public class MyCustomFormatProvider : IFormatProvider
{
    public static readonly MyCustomFormatProvider Instance = new MyCustomFormatProvider();

    public Object? GetFormat( Type? formatType )
    {
        if( formatType == typeof(ICustomFormatter) )
        {
            return MyCustomFormatter.Instance;
        }
        
        return null;
    }
}

public class MyCustomFormatter : ICustomFormatter
{
    public static readonly MyCustomFormatter Instance = new MyCustomFormatter();

    public String? Format( String? format, Object? arg, IFormatProvider? formatProvider )
    {
        // * `format` is the "aaa" in "{0:aaa}"
        // * `arg` is the single value 
        // * `formatProvider` will always be the parent instance of `MyCustomFormatProvider` and can be ignored.

        if( arg is Boolean b )
        {
            return b ? "Yes" : "No";
        }

        return null; // Returning null will cause .NET's composite-string-formatting code to fall-back to test `(arg as IFormattable)?.ToString(format)` and if that fails, then just `arg.ToString()`.
    }

    public static MyFormat( this String format, params Object?[] args )
    {
        return String.Format( Instance, format: format, arg: args );
    }
}

...所以只需以某种方式将 MyCustomFormatProvider.Instance 传递到 String.Format 中,就像以下。

double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;

string result1 = String.Format( MyCustomFormatProvider.Instance, "{0:0.0}, {1:HH:mm}, time to party? {2}", d, now, isPartyTime );

// or add `using static MyCustomFormatProvider` and use `MyFormat` directly:
string result2 = MyFormat( "{0:0.0}, {1:HH:mm}, time to party? {2}", d, now, isPartyTime );

// or as an extension method:
string result3 = "{0:0.0} {1:HH:mm}, time to party? {2}".MyFormat( d, now, isPartyTime );

// Assert( result1 == result2 == result3 );

这适用于 String.Format,但是我们如何将 MyCustomFormatProvider 与 C# $"" 内插字符串一起使用...?

...非常困难,因为设计插值字符串功能的 C# 语言团队使其始终传递 provider: null,因此所有值都使用他们的默认(通常是特定于文化的)格式,并且他们没有提供任何方法来轻松指定自定义IFormatProvider,即使有数十年历史的静态代码分析反对隐式使用 CurrentCulture< 的规则/code>(尽管微软打破自己的规则并不罕见......)。

  • 不幸的是,覆盖 CultureInfo.CurrentCulture 不起作用,因为 Boolean.ToString() 根本不使用 CultureInfo

困难源于 C# $"" 内插字符串 始终隐式转换为 String(即立即格式化)除非 $"" 字符串表达式被直接分配给一个变量或参数输入为 FormattableStringIFormattable,但令人愤怒 这不会扩展到扩展方法(因此 public static String MyFormat( this FormattableString fs, ... ) 不起作用

唯一这里可以做的就是调用 String MyFormat( this FormattableString fs, ... ) 方法作为(语法上“正常”)静态方法调用,不过使用 using static MyFormattableStringExtensions 在某种程度上减少了人体工程学问题 - 如果您使用全局使用(这需要 C# 10.0,它已经支持自定义插值字符串处理程序,所以这有点没有实际意义)。

但像这样:

public static class MyFormattableStringExtensions
{
    // The `this` modifier is unnecessary, but I'm retaining it just-in-case it's eventually supported.
    public static String MyFmt( this FormattableString fs )
    {
        return fs.ToString( MyCustomFormatProvider.Instance );
    }
}

并像这样使用:

using static MyFormattableStringExtensions;

// ...

double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;

string result = MyFmt( $"{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}" );
Assert.AreEqual( result, "3.1, 23:05, time to party? Yes" );

或者只是改变 FormattableString 的参数数组

  • 似乎没有其他选择可以在函数调用中包装内插字符串(例如 MyFmt( $"" )< /code> 上面),有一个更简单的替代方法来实现 IFormatProviderICustomFormatter:只需编辑 FormattableString 的值参数数组 直接地。
  • 由于此方法要简单得多,因此如果您不需要在 String.Format(IFormatProvider, String format, ...) 中格式化 Boolean 值,则更可取。
  • 就像这样:
public static class MyFormattableStringExtensions
{
    public static String MyFmt( this FormattableString fs )
    {
        if( fs.ArgumentCount == 0 ) return fs.Format;
        Object?[] args = fs.GetArguments();
        for( Int32 i = 0; i < args.Length; i++ )
        {
            if( args[i] is Boolean b )
            {
                args[i] = b ? "Yes" : "No";
            }
        }
        return String.Format( CultureInfo.CurrentCulture, fs.Format, arg: args  );
    }
}

并像以前一样使用以获得相同的结果:

using static MyFormattableStringExtensions;

// ...

double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;

string result = MyFmt( $"{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}" );
Assert.AreEqual( result, "3.1, 23:05, time to party? Yes" );

Using C# 10.0? Just use a String Interpolation Handler

Custom String Interpolation Handlers are documented here and here

(I don't have any experience with any C# 10.0 features yet, but I'll expand this section in future - right now I'm still stuck in C# 7.3 land due to my day-job's projects' dependencies on .NET Framework 4.8)

Using C# 1.0 through C# 9.0?

Quick-fix: Boolean wrapper struct

If you control the string-formatting call-sites, then just change bool/Boolean-typed values to use an implicitly-convertible zero-overhead value-type instead, e.g.:

public readonly struct YesNoBoolean : IEquatable<YesNoBoolean>
{
    // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators
    public static implicit operator Boolean  ( YesNoBoolean self ) => self.Value;
    public static implicit operator YesNoBoolean( Boolean value ) => new MyBoolean( value );

    // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/true-false-operators
    public static Boolean operator true( YesNoBoolean self ) => self.Value == true;
    public static Boolean operator false( YesNoBoolean self ) => self.Value == false;

    public YesNoBoolean( Boolean value )
    {
        this.Value = value;
    }

    public readonly Boolean Value;

    public override String ToString()
    {
        return this.Value ? "Yes" : "No";
    }

    // TODO: Override Equals, GetHashCode, IEquatable<YesNoBoolean>.Equals, etc.
}

So your example call-site becomes:

double d = Math.PI;
DateTime now = DateTime.Now;
YesNoBoolean isPartyTime = true;  // <-- Yay for implicit conversion.

string result = 
quot;{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}";

And result will be "3.1, 21:03, time to party? Yes"

Bubble-bursting: No, you can't overwrite Boolean.TrueString and FalseString

Because Boolean's static readonly String TrueString = "True"; is also marked with initonly you cannot overwrite it using reflection, so doing this:

typeof(Boolean).GetField( "TrueString" )!.SetValue( obj: null, value: "Yes" );

...will give you a runtime exception:

Cannot set initonly static field 'TrueString' after type 'System.Boolean' is initialized.

It is still possible by manipulating raw memory, but that's out-of-scope for this question.

Using IFormatProvider and ICustomFormatter:

It's always been possible to override how both String.Format and interpolated strings (e.g. $"Hello, {world}") are formatted by providing a custom IFormatProvider; though while String.Format makes it easy by exposing a Format overload parameter, interpolated strings do not, instead it forces you to uglify your code somewhat.

  • Implementing IFormatProvider is (still) surprisingly underdocumented in .NET.
    • The main thing to remember is that IFormatProvider.GetFormat(Type) is only ever invoked with one of these 3 formatType arguments:
      • typeof(DateTimeFormatInfo)
      • typeof(NumberFormatInfo)
      • typeof(ICustomFormatter)
    • Throughout the entire .NET BCL, no other typeof() types are passed into GetFormat (at least as far as ILSpy and RedGate Reflector tell me).

The magic happens inside ICustomFormatter.Format and implementing it is straightforward:

public class MyCustomFormatProvider : IFormatProvider
{
    public static readonly MyCustomFormatProvider Instance = new MyCustomFormatProvider();

    public Object? GetFormat( Type? formatType )
    {
        if( formatType == typeof(ICustomFormatter) )
        {
            return MyCustomFormatter.Instance;
        }
        
        return null;
    }
}

public class MyCustomFormatter : ICustomFormatter
{
    public static readonly MyCustomFormatter Instance = new MyCustomFormatter();

    public String? Format( String? format, Object? arg, IFormatProvider? formatProvider )
    {
        // * `format` is the "aaa" in "{0:aaa}"
        // * `arg` is the single value 
        // * `formatProvider` will always be the parent instance of `MyCustomFormatProvider` and can be ignored.

        if( arg is Boolean b )
        {
            return b ? "Yes" : "No";
        }

        return null; // Returning null will cause .NET's composite-string-formatting code to fall-back to test `(arg as IFormattable)?.ToString(format)` and if that fails, then just `arg.ToString()`.
    }

    public static MyFormat( this String format, params Object?[] args )
    {
        return String.Format( Instance, format: format, arg: args );
    }
}

...so just pass MyCustomFormatProvider.Instance into String.Format somehow, like below.

double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;

string result1 = String.Format( MyCustomFormatProvider.Instance, "{0:0.0}, {1:HH:mm}, time to party? {2}", d, now, isPartyTime );

// or add `using static MyCustomFormatProvider` and use `MyFormat` directly:
string result2 = MyFormat( "{0:0.0}, {1:HH:mm}, time to party? {2}", d, now, isPartyTime );

// or as an extension method:
string result3 = "{0:0.0} {1:HH:mm}, time to party? {2}".MyFormat( d, now, isPartyTime );

// Assert( result1 == result2 == result3 );

So that works for String.Format, but how can we use MyCustomFormatProvider with C# $"" interpolated strings...?

...with great difficulty, because the C# langauge team who designed the interpolated strings feature made it always pass provider: null so all values use their default (usually Culture-specific) formatting, and they didn't provide any way to easily specify a custom IFormatProvider, even though there's decades-old Static Code Analysis rule against relying on implicit use of CurrentCulture (though it's not uncommon for Microsoft to break their own rules...).

  • Unfortunately overwriting CultureInfo.CurrentCulture won't work because Boolean.ToString() doesn't use CultureInfo at all.

The difficulty stems from the fact that C# $"" interpolated strings are always implicitly converted to String (i.e. they're formatted immediately) unless the $"" string expression is directly assigned to a variable or parameter typed as FormattableString or IFormattable, but infuriatingly this does not extend to extension methods (so public static String MyFormat( this FormattableString fs, ... ) won't work.

The the only thing that can be done here is to invoke that String MyFormat( this FormattableString fs, ... ) method as a (syntactically "normal") static method call, though using using static MyFormattableStringExtensions somewhat reduces the ergonomics problems - even more-so if you use global-usings (which requires C# 10.0, which already supports custom interpolated-string handlers, so that's kinda moot).

But like this:

public static class MyFormattableStringExtensions
{
    // The `this` modifier is unnecessary, but I'm retaining it just-in-case it's eventually supported.
    public static String MyFmt( this FormattableString fs )
    {
        return fs.ToString( MyCustomFormatProvider.Instance );
    }
}

And used like this:

using static MyFormattableStringExtensions;

// ...

double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;

string result = MyFmt( 
quot;{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}" );
Assert.AreEqual( result, "3.1, 23:05, time to party? Yes" );

Or just mutate FormattableString's arguments array

  • Seeming as there's no alternative to wrapping an interpolated string in a function call (like MyFmt( $"" ) above), there's a simpler alternative approach to having to implement IFormatProvider and ICustomFormatter: just edit the FormattableString's value arguments array directly.
  • Because this approach is significantly simpler it is preferable if you don't also need to format Boolean values in String.Format(IFormatProvider, String format, ...).
  • Like so:
public static class MyFormattableStringExtensions
{
    public static String MyFmt( this FormattableString fs )
    {
        if( fs.ArgumentCount == 0 ) return fs.Format;
        Object?[] args = fs.GetArguments();
        for( Int32 i = 0; i < args.Length; i++ )
        {
            if( args[i] is Boolean b )
            {
                args[i] = b ? "Yes" : "No";
            }
        }
        return String.Format( CultureInfo.CurrentCulture, fs.Format, arg: args  );
    }
}

And used just like before to get the same results:

using static MyFormattableStringExtensions;

// ...

double d = Math.PI;
DateTime now = DateTime.Now;
bool isPartyTime = true;

string result = MyFmt( 
quot;{d:0.0}, {now:HH:mm}, time to party? {isPartyTime}" );
Assert.AreEqual( result, "3.1, 23:05, time to party? Yes" );
绿光 2025-01-17 01:23:53

这可能是显而易见的,但为了减少重复,您始终可以创建一个扩展方法。它至少能让你成功一半。

public static class MyExtensions
{
    public static string ToYesNo(this bool boolValue)
    {
        return boolValue ? "Yes" : "No";
    }
}

static void Main(string[] args)
{
    var booleanValue = true;

    Console.WriteLine(booleanValue.ToYesNo());
}

This may be obvious but to cut down the repetition, you could always create an extension method. It gets you half way there at least.

public static class MyExtensions
{
    public static string ToYesNo(this bool boolValue)
    {
        return boolValue ? "Yes" : "No";
    }
}

static void Main(string[] args)
{
    var booleanValue = true;

    Console.WriteLine(booleanValue.ToYesNo());
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文