布尔格式的字符串插值
String interpolation with boolean formatting
如何为布尔值指定与其他类型的其他格式字符串一致的格式字符串?
给定以下代码:
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")}";
然而,这仍然与其他类型不一致。
有没有一种方法可以在一致的内插字符串中格式化布尔值?
P.S。我确实搜索了一个答案,包括这个 link:
https://whosebug.com/questions/tagged/c%23+string-interpolation+boolean
令人惊讶的是结果为零。
很遗憾,没有。
According to Microsoft,唯一具有格式字符串的数据类型是:
- 日期和时间类型(
DateTime, DateTimeOffset
)
- 枚举类型(所有派生自
System.Enum
的类型)
- 数值类型 (
BigInteger, Byte, Decimal, Double, Int16, Int32, Int64, SByte, Single, UInt16, UInt32, UInt64
)
Guid
TimeSpan
Boolean.ToString()只能return“真”或“假”。它甚至说,如果你需要将它写入 XML,你需要手动执行 ToLowerCase()
(由于缺少字符串格式)。
这可能是显而易见的,但要减少重复,您始终可以创建扩展方法。它至少让你成功了一半。
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());
}
使用 C# 10.0?只需使用字符串插值处理程序
Custom String Interpolation Handlers are documented here and here
(我还没有使用任何 C# 10.0 功能的经验,但我会在未来扩展这部分 - 现在我仍然停留在 C# 7.3 土地 由于我的 day-job 项目依赖于 .NET Framework 4.8)
使用 C# 1.0 到 C# 9.0?
Quick-fix: Boolean
包装结构
如果您控制 string-formatting call-sites,则只需更改 bool
/Boolean
类型的值以使用 implicitly-convertible zero-overhead value-type 而不是,例如:
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.
}
因此您的示例 call-site 变为:
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"
Bubble-bursting:不,你不能覆盖Boolean.TrueString
和FalseString
因为Boolean
的static readonly String TrueString = "True";
也标有initonly
你不能用反射覆盖它,所以这样做:
typeof(Boolean).GetField( "TrueString" )!.SetValue( obj: null, value: "Yes" );
...会给你一个运行时异常:
Cannot set initonly static field
'TrueString
' after type 'System.Boolean
' is initialized.
通过操纵原始内存仍然是可能的,但这是out-of-scope这个问题。
使用 IFormatProvider
和 ICustomFormatter
:
始终可以通过提供自定义 IFormatProvider
来覆盖 String.Format
和内插字符串(例如 $"Hello, {world}"
)的格式化方式;虽然 String.Format
通过公开 Format
重载参数使它变得容易,但内插字符串不会,而是它迫使你稍微 uglify 你的代码。
IFormatProvider
的实现在 .NET 中(仍然)出人意料地不足。
- 要记住的主要事情是
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
所以所有值都使用它们的默认格式(通常是 Culture-specific),并且它们没有提供任何方法来 轻松地 指定自定义 IFormatProvider
,即使有decades-old 静态代码分析rule against relying on implicit use of CurrentCulture
(虽然微软打破自己的规则并不少见...)。
- 不幸的是,覆盖
CultureInfo.CurrentCulture
将不起作用,因为 Boolean.ToString()
根本不使用 CultureInfo
。
困难源于 C# $""
内插字符串 (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.
这里 唯一 可以做的事情是调用 String MyFormat( this FormattableString fs, ... )
方法作为(语法上“正常”的)静态方法调用,尽管使用 using static MyFormattableStringExtensions
在某种程度上减少了人体工程学问题 - 即使 more-so 如果您使用 global-usings(这需要 C# 10.0,它已经支持自定义 interpolated-string 处理程序,所以这有点没有实际意义)。
但是像这样:
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( $"" )
),有一个更简单的替代方法来实现 IFormatProvider
和 ICustomFormatter
:直接编辑 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" );
如何为布尔值指定与其他类型的其他格式字符串一致的格式字符串?
给定以下代码:
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")}";
然而,这仍然与其他类型不一致。
有没有一种方法可以在一致的内插字符串中格式化布尔值?
P.S。我确实搜索了一个答案,包括这个 link:
https://whosebug.com/questions/tagged/c%23+string-interpolation+boolean
令人惊讶的是结果为零。
很遗憾,没有。
According to Microsoft,唯一具有格式字符串的数据类型是:
- 日期和时间类型(
DateTime, DateTimeOffset
) - 枚举类型(所有派生自
System.Enum
的类型) - 数值类型 (
BigInteger, Byte, Decimal, Double, Int16, Int32, Int64, SByte, Single, UInt16, UInt32, UInt64
) Guid
TimeSpan
Boolean.ToString()只能return“真”或“假”。它甚至说,如果你需要将它写入 XML,你需要手动执行 ToLowerCase()
(由于缺少字符串格式)。
这可能是显而易见的,但要减少重复,您始终可以创建扩展方法。它至少让你成功了一半。
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());
}
使用 C# 10.0?只需使用字符串插值处理程序
Custom String Interpolation Handlers are documented here and here
(我还没有使用任何 C# 10.0 功能的经验,但我会在未来扩展这部分 - 现在我仍然停留在 C# 7.3 土地 由于我的 day-job 项目依赖于 .NET Framework 4.8)
使用 C# 1.0 到 C# 9.0?
Quick-fix: Boolean
包装结构
如果您控制 string-formatting call-sites,则只需更改 bool
/Boolean
类型的值以使用 implicitly-convertible zero-overhead value-type 而不是,例如:
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.
}
因此您的示例 call-site 变为:
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"
Bubble-bursting:不,你不能覆盖Boolean.TrueString
和FalseString
因为Boolean
的static readonly String TrueString = "True";
也标有initonly
你不能用反射覆盖它,所以这样做:
typeof(Boolean).GetField( "TrueString" )!.SetValue( obj: null, value: "Yes" );
...会给你一个运行时异常:
Cannot set
initonly static field
'TrueString
' after type 'System.Boolean
' is initialized.
通过操纵原始内存仍然是可能的,但这是out-of-scope这个问题。
使用 IFormatProvider
和 ICustomFormatter
:
始终可以通过提供自定义 IFormatProvider
来覆盖 String.Format
和内插字符串(例如 $"Hello, {world}"
)的格式化方式;虽然 String.Format
通过公开 Format
重载参数使它变得容易,但内插字符串不会,而是它迫使你稍微 uglify 你的代码。
IFormatProvider
的实现在 .NET 中(仍然)出人意料地不足。- 要记住的主要事情是
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
所以所有值都使用它们的默认格式(通常是 Culture-specific),并且它们没有提供任何方法来 轻松地 指定自定义 IFormatProvider
,即使有decades-old 静态代码分析rule against relying on implicit use of CurrentCulture
(虽然微软打破自己的规则并不少见...)。
- 不幸的是,覆盖
CultureInfo.CurrentCulture
将不起作用,因为Boolean.ToString()
根本不使用CultureInfo
。
困难源于 C# $""
内插字符串 $""
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.
这里 唯一 可以做的事情是调用 String MyFormat( this FormattableString fs, ... )
方法作为(语法上“正常”的)静态方法调用,尽管使用 using static MyFormattableStringExtensions
在某种程度上减少了人体工程学问题 - 即使 more-so 如果您使用 global-usings(这需要 C# 10.0,它已经支持自定义 interpolated-string 处理程序,所以这有点没有实际意义)。
但是像这样:
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( $"" )
),有一个更简单的替代方法来实现IFormatProvider
和ICustomFormatter
:直接编辑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" );