编写高质量代码的50条黄金守则-Day 04(首选字符串插值)

从 C# 6.0 开始,微软开始为 .net 引入字符串插值,通过为字符串加 $ 前缀的方式,提供了强大的语法糖,为字符串的处理带来更好的使用体验。相比于传统的字符串处理 string.Format 来说,其使用方式更加的灵活。今天,我们来为大家解密字符串插值的庐山真面目。

编写高质量代码的50条黄金守则-Day 04(首选字符串插值),本文由比特飞原创发布,转载务必在文章开头附带链接:https://www.byteflying.com/archives/6884

该系列文章由比特飞原创发布,计划用半年时间写完全50篇文章,为大家提供编写高质量代码的一般准则。

1、概述

从 C# 6.0 开始,微软开始为 .net 引入字符串插值,通过为字符串加 $ 前缀的方式,提供了强大的语法糖,为字符串的处理带来更好的使用体验。相比于传统的字符串处理 string.Format 来说,其使用方式更加的灵活。今天,我们来为大家解密字符串插值的庐山真面目。

2、通过反编译查看IL,探究字符串插值的本质

接下来,我们先来准备环境:

namespace EffectiveCoding04 {

    public class Program {

        private class User : IFormattable {

            public string Foo { get; set; }

            public string ToString(string format, IFormatProvider formatProvider) {
                return $"My name is {Foo}";
            }

        }

        private static string GetValue() {
            return "foo";
        }

        private static IEnumerable<User> GetValues() {
            yield return new User() { Foo = "foo 1" };
            yield return new User() { Foo = "foo 2" };
            yield return new User() { Foo = "foo 3" };
            yield return new User() { Foo = "foo 4" };
            yield return new User() { Foo = "foo 5" };
        }

        //准备数据
        /* var condition = true;
        var value = (User)null; */

    }

}
编写高质量代码的50条黄金守则-Day 04(首选字符串插值)

User 类实现 IFormattable 接口,提供字符串格式化功能,GetValue 方法返回一个字符串,GetValues 方法返回一个字符串序列 。

3、使用方法

1、基本使用方法

我们先来看看字符串插值的基本用法:

Console.WriteLine($"Value1 is {Math.PI}");
Console.WriteLine($"Value2 is {Math.PI.ToString()}");

以下是输出结果:

Value1 is 3.141592653589793
Value2 is 3.141592653589793
编写高质量代码的50条黄金守则-Day 04(首选字符串插值)

占位说明:。接下来我们看看它们的 IL:

	/* (35,13)-(35,55) C:\Users\Administrator\source\repos\EffectiveCoding04\EffectiveCoding04\Program.cs */
	/* 0x00000281 7209000070   */ IL_0005: ldstr     "Value1 is {0}"
	/* 0x00000286 23182D4454FB210940 */ IL_000A: ldc.r8    3.141592653589793
	/* 0x0000028F 8C1B000001   */ IL_0013: box       [System.Runtime]System.Double
	/* 0x00000294 281600000A   */ IL_0018: call      string [System.Runtime]System.String::Format(string, object)
	/* 0x00000299 281700000A   */ IL_001D: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x0000029E 00           */ IL_0022: nop
	/* (36,13)-(36,66) C:\Users\Administrator\source\repos\EffectiveCoding04\EffectiveCoding04\Program.cs */
	/* 0x0000029F 7225000070   */ IL_0023: ldstr     "Value2 is "
	/* 0x000002A4 23182D4454FB210940 */ IL_0028: ldc.r8    3.141592653589793
	/* 0x000002AD 0C           */ IL_0031: stloc.2
	/* 0x000002AE 1202         */ IL_0032: ldloca.s  V_2
	/* 0x000002B0 281800000A   */ IL_0034: call      instance string [System.Runtime]System.Double::ToString()
	/* 0x000002B5 281900000A   */ IL_0039: call      string [System.Runtime]System.String::Concat(string, string)
	/* 0x000002BA 281700000A   */ IL_003E: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x000002BF 00           */ IL_0043: nop
编写高质量代码的50条黄金守则-Day 04(首选字符串插值)

IL 的代码有些疑惑,我们利用 dnSpy 反编译看看结果:

Console.WriteLine(string.Format("Value1 is {0}", 3.141592653589793));
Console.WriteLine("Value2 is " + 3.141592653589793.ToString());
编写高质量代码的50条黄金守则-Day 04(首选字符串插值)

我们可以看到 $”Value1 is {Math.PI}”; 被 string.Format(“Value1 is {0}”, 3.141592653589793) 所替换, $”Value2 is {Math.PI.ToString()}” 被 “Value2 is ” + 3.141592653589793.ToString() 所替换。

2、配合格式化参数使用

字符串插值可以配合格式化参数一起使用:

Console.WriteLine($"Value3 is {Math.PI.ToString("F2")}");
Console.WriteLine($"Value4 is {Math.PI:F2}");

以下是 IL 的结果:

/* 0x000002C0 723B000070   */ IL_0044: ldstr     "Value3 is "
	/* 0x000002C5 23182D4454FB210940 */ IL_0049: ldc.r8    3.141592653589793
	/* 0x000002CE 0C           */ IL_0052: stloc.2
	/* 0x000002CF 1202         */ IL_0053: ldloca.s  V_2
	/* 0x000002D1 7251000070   */ IL_0055: ldstr     "F2"
	/* 0x000002D6 281A00000A   */ IL_005A: call      instance string [System.Runtime]System.Double::ToString(string)
	/* 0x000002DB 281900000A   */ IL_005F: call      string [System.Runtime]System.String::Concat(string, string)
	/* 0x000002E0 281700000A   */ IL_0064: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x000002E5 00           */ IL_0069: nop
	/* (39,13)-(39,58) C:\Users\Administrator\source\repos\EffectiveCoding04\EffectiveCoding04\Program.cs */
	/* 0x000002E6 7257000070   */ IL_006A: ldstr     "Value4 is {0:F2}"
	/* 0x000002EB 23182D4454FB210940 */ IL_006F: ldc.r8    3.141592653589793
	/* 0x000002F4 8C1B000001   */ IL_0078: box       [System.Runtime]System.Double
	/* 0x000002F9 281600000A   */ IL_007D: call      string [System.Runtime]System.String::Format(string, object)
	/* 0x000002FE 281700000A   */ IL_0082: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x00000303 00           */ IL_0087: nop

以下是 dnSpy 反编译的结果:

Console.WriteLine("Value3 is " + 3.141592653589793.ToString("F2"));
Console.WriteLine(string.Format("Value4 is {0:F2}", 3.141592653589793));

我们可以看到 $”Value3 is {Math.PI.ToString(“F2″)}” 被 “Value3 is ” + 3.141592653589793.ToString(“F2″) 所替换,$”Value4 is {Math.PI:F2}” 被 string.Format(“Value4 is {0:F2}”, 3.141592653589793) 所替换。

C# 会对字符串插值中的 做特殊处理,认为后面的部分为格式化参数。那如果我们就是想要输出 的话,应该如何处理呢?

3、错误的示例

你可能会使用以下方式输出

Console.WriteLine($"Value5 is {condition ? Math.PI : Math.PI.ToString("F2")}"); //无法编译通过
Console.WriteLine($@"Value6 is {(condition ? Math.PI : Math.PI.ToString("F2"))}"); //无法编译通过

然后以上代码却无法编译通过,因为编译器认为 后面的为格式化参数,导致语法解析错误,那应该如何处理呢?答应是使用 @ 操作符,显式指定不转义。

4、配合条件运算符

字符串插值配合条件运算符一起使用:

Console.WriteLine($@"Value7 is {(condition ? Math.PI.ToString() : Math.PI.ToString("F2"))}");

以下是 IL 的结果:

/* 0x00000304 7279000070   */ IL_0088: ldstr     "Value7 is "
	/* 0x00000309 06           */ IL_008D: ldloc.0
	/* 0x0000030A 2D18         */ IL_008E: brtrue.s  IL_00A8

	/* 0x0000030C 23182D4454FB210940 */ IL_0090: ldc.r8    3.141592653589793
	/* 0x00000315 0C           */ IL_0099: stloc.2
	/* 0x00000316 1202         */ IL_009A: ldloca.s  V_2
	/* 0x00000318 7251000070   */ IL_009C: ldstr     "F2"
	/* 0x0000031D 281A00000A   */ IL_00A1: call      instance string [System.Runtime]System.Double::ToString(string)
	/* 0x00000322 2B11         */ IL_00A6: br.s      IL_00B9

	/* 0x00000324 23182D4454FB210940 */ IL_00A8: ldc.r8    3.141592653589793
	/* 0x0000032D 0C           */ IL_00B1: stloc.2
	/* 0x0000032E 1202         */ IL_00B2: ldloca.s  V_2
	/* 0x00000330 281800000A   */ IL_00B4: call      instance string [System.Runtime]System.Double::ToString()

	/* 0x00000335 281900000A   */ IL_00B9: call      string [System.Runtime]System.String::Concat(string, string)
	/* 0x0000033A 281700000A   */ IL_00BE: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x0000033F 00           */ IL_00C3: nop

以下是 dnSpy 反编译的结果:

Console.WriteLine("Value7 is " + (condition ? 3.141592653589793.ToString() : 3.141592653589793.ToString("F2")));

我们可以看到 $@”Value7 is {(condition ? Math.PI.ToString() : Math.PI.ToString(“F2″))}” 被转换成了 “Value7 is ” + (condition ? 3.141592653589793.ToString() : 3.141592653589793.ToString(“F2”)) ,这样编译顺利进行。

5、配合空值传播运算符使用

字符串插值可以配合空值传播运算符一起使用:

Console.WriteLine($"Value8 is {value?.Foo ?? "value is null"}");

以下是 IL 的结果:

/* 0x00000340 728F000070   */ IL_00C4: ldstr     "Value8 is "
	/* 0x00000345 07           */ IL_00C9: ldloc.1
	/* 0x00000346 2D03         */ IL_00CA: brtrue.s  IL_00CF

	/* 0x00000348 14           */ IL_00CC: ldnull
	/* 0x00000349 2B06         */ IL_00CD: br.s      IL_00D5

	/* 0x0000034B 07           */ IL_00CF: ldloc.1
	/* 0x0000034C 2805000006   */ IL_00D0: call      instance string EffectiveCoding04.Program/User::get_Foo()

	/* 0x00000351 25           */ IL_00D5: dup
	/* 0x00000352 2D06         */ IL_00D6: brtrue.s  IL_00DE

	/* 0x00000354 26           */ IL_00D8: pop
	/* 0x00000355 72A5000070   */ IL_00D9: ldstr     "value is null"

	/* 0x0000035A 281900000A   */ IL_00DE: call      string [System.Runtime]System.String::Concat(string, string)
	/* 0x0000035F 281700000A   */ IL_00E3: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x00000364 00           */ IL_00E8: nop

以下是 dnSpy 反编译的结果:

Console.WriteLine("Value8 is " + (((value != null) ? value.Foo : null) ?? "value is null"));

可以看到 $”Value8 is {value?.Foo ?? “value is null”}” 被编译器替换成 “Value8 is ” + (((value != null) ? value.Foo : null) ?? “value is null”) 。

6、配合方法使用

字符串插值也可以配合方法一起使用:

Console.WriteLine($"Value9 is {GetValue()}");

以下是 IL 的结果:

	/* 0x00000365 72C1000070   */ IL_00E9: ldstr     "Value9 is "
	/* 0x0000036A 2801000006   */ IL_00EE: call      string EffectiveCoding04.Program::GetValue()
	/* 0x0000036F 281900000A   */ IL_00F3: call      string [System.Runtime]System.String::Concat(string, string)
	/* 0x00000374 281700000A   */ IL_00F8: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x00000379 00           */ IL_00FD: nop

以下是 dnSpy 反编译的结果:

Console.WriteLine("Value9 is " + Program.GetValue());

直接被编译器转换成传统的字符串连接操作。

7、配合 Linq 使用

字符串插值配合 Linq 一起使用的示例:

Console.WriteLine($"Value10 is {GetValues().FirstOrDefault(r => r.Foo == "foo 3")}");

以下是 IL 的结果:

	/* 0x0000037A 72D7000070   */ IL_00FE: ldstr     "Value10 is {0}"
	/* 0x0000037F 2802000006   */ IL_0103: call      class [System.Runtime]System.Collections.Generic.IEnumerable1<class EffectiveCoding04.Program/User> EffectiveCoding04.Program::GetValues()
	/* 0x00000384 7E06000004   */ IL_0108: ldsfld    class [System.Runtime]System.Func2<class EffectiveCoding04.Program/User, bool> EffectiveCoding04.Program/'<>c'::'<>9__3_0'
	/* 0x00000389 25           */ IL_010D: dup
	/* 0x0000038A 2D17         */ IL_010E: brtrue.s  IL_0127

	/* 0x0000038C 26           */ IL_0110: pop
	/* 0x0000038D 7E05000004   */ IL_0111: ldsfld    class EffectiveCoding04.Program/'<>c' EffectiveCoding04.Program/'<>c'::'<>9'
	/* 0x00000392 FE0613000006 */ IL_0116: ldftn     instance bool EffectiveCoding04.Program/'<>c'::'<Main>b__3_0'(class EffectiveCoding04.Program/User)
	/* 0x00000398 731B00000A   */ IL_011C: newobj    instance void class [System.Runtime]System.Func2<class EffectiveCoding04.Program/User, bool>::.ctor(object, native int)
	/* 0x0000039D 25           */ IL_0121: dup
	/* 0x0000039E 8006000004   */ IL_0122: stsfld    class [System.Runtime]System.Func2<class EffectiveCoding04.Program/User, bool> EffectiveCoding04.Program/'<>c'::'<>9__3_0'

	/* 0x000003A3 280100002B   */ IL_0127: call      !!0 [System.Linq]System.Linq.Enumerable::FirstOrDefault<class EffectiveCoding04.Program/User>(class [System.Runtime]System.Collections.Generic.IEnumerable1<!!0>, class [System.Runtime]System.Func2<!!0, bool>)
	/* 0x000003A8 281600000A   */ IL_012C: call      string [System.Runtime]System.String::Format(string, object)
	/* 0x000003AD 281700000A   */ IL_0131: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x000003B2 00           */ IL_0136: nop

以下是 dnSpy 反编译的结果:

Console.WriteLine(string.Format("Value10 is {0}", Program.GetValues().FirstOrDefault((Program.User r) => r.Foo == "foo 3")));

有意思的是, Linq 也被编译器转换成了 string.Format 的调用方式。

4、总结

1、字符串插值会被编译器转换,而传统的 string.Format 仅仅是方法的调用 :

2、字符串插值更加灵活,也更加强大,推荐使用字符串插值的方式操作字符串。

开发人员应牢记以上开发守则,否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你、唾弃你

该系列文章由比特飞原创发布,计划用半年时间写完全50篇文章,为大家提供编写高质量代码的一般准则。

本文由 .Net中文网 原创发布,欢迎大家踊跃转载。

转载请注明本文地址:https://www.byteflying.com/archives/6884

发表评论

登录后才能评论

评论列表(2条)