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

186 阅读6分钟

本文由 比特飞 原创发布,欢迎大家踊跃转载。
转载请注明本文地址:www.byteflying.com/archives/68…

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

该系列文章由比特飞原创发布,计划用半年时间写完全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(首选字符串插值)](https://cdn.jsdelivr.net/gh/byteflying/blogimage/image/2020-08-21-20-14-04.png)](https://cdn.jsdelivr.net/gh/byteflying/blogimage/image/2020-08-21-20-14-04.png)

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

3、使用方法

1、基本使用方法

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

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

以下是输出结果:

Value1 is 3.141592653589793Value2 is 3.141592653589793
[![编写高质量代码的50条黄金守则-Day 04(首选字符串插值)](https://cdn.jsdelivr.net/gh/byteflying/blogimage/image/2020-08-21-20-15-47.png)](https://cdn.jsdelivr.net/gh/byteflying/blogimage/image/2020-08-21-20-15-47.png)

占位说明:。接下来我们看看它们的 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
IL 的代码有些疑惑,我们利用 dnSpy 反编译看看结果:
Console.WriteLine(string.Format("Value1 is {0}", 3.141592653589793));Console.WriteLine("Value2 is " + 3.141592653589793.ToString());
我们可以看到 $”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));

我们可以看到 Value3isMath.PI.ToString(F2)”被“Value3is+3.141592653589793.ToString(F2)所替换,”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.IEnumerable<code data-enlighter-language="generic" class="EnlighterJSRAW">1<class EffectiveCoding04.Program/User> EffectiveCoding04.Program::GetValues()/* 0x00000384 7E06000004 */ IL_0108: ldsfld class [System.Runtime]System.Func</code>2<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.Func<code data-enlighter-language="generic" class="EnlighterJSRAW">2<class EffectiveCoding04.Program/User, bool>::.ctor(object, native int)/* 0x0000039D 25 */ IL_0121: dup/* 0x0000039E 8006000004 */ IL_0122: stsfld class [System.Runtime]System.Func</code>2<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.IEnumerable<code data-enlighter-language="generic" class="EnlighterJSRAW">1<!!0>, class [System.Runtime]System.Func</code>2<!!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篇文章,为大家提供编写高质量代码的一般准则。

本文由 比特飞 原创发布,欢迎大家踊跃转载。
转载请注明本文地址:www.byteflying.com/archives/68…