【问题标题】:String interpolation with boolean formatting带布尔格式的字符串插值
【发布时间】:2022-03-10 00:46:15
【问题描述】:

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

给定以下代码:

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

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

有没有一种方法可以在一致的插值字符串中格式化布尔值?

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

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

令人惊讶的是结果为零。

【问题讨论】:

  • 排序:如果你在辅助方法中改变 FormattableString 的 Arguments 数组是可行的。很糟糕,您也不能将扩展方法与 FormattableString 一起使用。
  • @Dai 谢谢,我知道有多种方法可以做到这一点,但它们似乎都需要某种解决方法。我认为我的问题的关键部分是一致性。
  • 我认为“不一致”的原因是 C# 中布尔值的“字符串”表示实际上是关键字。实际值(如果我没记错的话)是 0 表示“假”或非零表示“真”。并不是“FormattableString”不一致,而是规范对类型值的实际表示不一致。还有什么其他类型有值的关键字?我想不出来。
  • @dmedine 好吧,null 是另一个。
  • @dmedine 我的意思是,在 .NET 的类型系统中,“null”是一个代表对象引用的特定值的关键字(我坚决反对“null 是不存在价值”的思想流派。那些人是怪物:他们给了我们 SQL!)

标签: c# boolean string-interpolation


【解决方案1】:

很遗憾,没有。

According to 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()(由于缺少字符串格式)。

【讨论】:

  • 谢谢; obvs 我会暂时保留这个问题,以防安德斯本人出现,但我认为你是对的。
  • 如果确实如此,我会感到惊讶。上面提到的其他数据类型有一个 ToString(string) 重载,这是布尔值所缺乏的。
  • @NPras 您要注意的是IFormattable,而不仅仅是public 方法(因为许多显式接口实现的实例没有记录,但仍然有效)
  • 所以您知道发布了另一个答案,其中包含 C# 10 的解决方案,所以我认为这是正确答案并切换了“勾选”
  • @IanNewson 这是一个多么好的答案!更值得拥有。
【解决方案2】:

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

Custom String Interpolation Handlers are documented herehere

(我还没有任何 C# 10.0 功能的经验,但我会在未来扩展这部分 - 现在我仍然困在 C# 7.3 领域 -job 的项目对 .NET Framework 4.8 的依赖)

使用 C# 1.0 到 C# 9.0?

快速修复:Boolean 包装结构

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

public readonly struct YesNoBoolean : IEquatable<YesNoBoolean>
{
    // https://docs.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://docs.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}";

result 将是"3.1, 21:03, time to party? Yes"

冒泡:不,你不能覆盖Boolean.TrueStringFalseString

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

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

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

在类型 'System.Boolean' 初始化后无法设置 initonly static field 'TrueString'。

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

使用IFormatProviderICustomFormatter

始终可以通过提供自定义IFormatProvider 来覆盖String.Format 和插值字符串(例如$"Hello, {world}")的格式;虽然String.Format 通过暴露Format 重载参数使其变得容易,但内插字符串不会,而是迫使您丑化您的代码。

  • 在 .NET 中实现 IFormatProvider(仍然)出人意料地不足。
    • 要记住的主要事情是 IFormatProvider.GetFormat(Type) 使用以下 3 个 formatType 参数之一调用:
      • typeof(DateTimeFormatInfo)
      • typeof(NumberFormatInfo)
      • typeof(ICustomFormatter)
    • 在整个 .NET BCL 中,没有其他 typeof() 类型被传递到 GetFormat(至少 ILSpy 和 RedGate Reflector 告诉我)。

魔法发生在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 的方法,即使有几十年前的静态代码分析 rule against relying on implicit use of CurrentCulture(尽管 Microsoft打破自己的规则...)。

  • 很遗憾,覆盖 CultureInfo.CurrentCulture 将不起作用,因为 Boolean.ToString() 根本不使用 CultureInfo

困难在于 C# $"" 插值字符串 are always implicitly converted to String(即立即格式化)除非 $"" 字符串表达式直接分配给类型化的变量或参数就像FormattableStringIFormattable,但真气 this does not extend to extension methods (so public static String MyFormat( this FormattableString fs, ... ) won't work

唯一这里可以做的事情是调用 String MyFormat( this FormattableString fs, ... ) 方法作为(语法上“正常”)静态方法调用,尽管使用 using static MyFormattableStringExtensions 在某种程度上减少了人体工程学问题 -如果您使用 global-usings(这需要 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( $"" ))之外别无选择,有一个更简单的替代方法来实现IFormatProviderICustomFormatter:只需编辑FormattableString's直接值参数数组。
  • 因为这种方法要简单得多,如果您还不需要在 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" );

【讨论】:

【解决方案3】:

这可能很明显,但为了减少重复,您总是可以创建一个扩展方法。它至少能让你走到一半。

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

【讨论】:

    猜你喜欢
    • 2011-02-05
    • 1970-01-01
    • 2015-11-28
    • 1970-01-01
    • 2011-01-16
    • 1970-01-01
    • 2017-07-15
    • 2014-03-30
    • 2022-07-05
    相关资源
    最近更新 更多