最近在研究关于string的一些东西,发现底层实现挺有意思的,想来研究下string字符串拼接底层的实现原理
先来看看下面的代码
string str = "12";
int num = 34;
str = str + num;
Console.WriteLine(str);
输出结果:1234
按道理来说,在string.cs并没有对 String 类的 + 符号进行重载,int不可以和string进行相加,但是相加的结果确实可以正确输出,所以到底是怎么回事呢?
我们来看看下面
Console.WriteLine("12" + 34);
生成的IL代码为(省略无关部分):
IL_000d: call string [mscorlib]System.String::Concat(object,
object)
IL_0012: call void [mscorlib]System.Console::WriteLine(string)
Console.WriteLine(12 + "34");
生成的IL代码为(省略无关部分):
IL_000d: call string [mscorlib]System.String::Concat(object,
object)
IL_0012: call void [mscorlib]System.Console::WriteLine(string)
Console.WriteLine(12 + "3" + 45);
生成的IL为(省略无关部分):
IL_0014: call string [mscorlib]System.String::Concat(object,
object,
object)
IL_0019: call void [mscorlib]System.Console::WriteLine(string)
Console.WriteLine("12" + 34 + 56 + 78);
生成的IL为(省略无关部分):
IL_002f: call string [mscorlib]System.String::Concat(object[])
IL_0034: call void [mscorlib]System.Console::WriteLine(string)
毫无疑问,它们都调用了Concat方法
但是为什么在没有进行运算符重载(或者我没发现?)的情况下编译器知道执行 String.Concat 函数呢?以及为什么编译器能够同时将表达式中的多个变量同时视为参数呢?以及如果需要对自己定义的一个类型实现这样的操作应该怎么做?
答案是
C#的string连接运算是语言规范里特别规定的,而不是一个普通的重载运算符,所以它的实现在C#编译器里,而在string.cs源码里就没出现。
例如说,object与string做+的话,算出的运算符类型就是BinaryOperatorKind.StringAndObject。
经过这一步,C#编译器就知道某个+运算是不是涉及字符串拼接的了。
然后有一步叫做Lowering,主要是把一些语法糖功能转换为用更基础的功能的AST节点代替——也就是解除语法糖。其中有一步是专门针对字符串拼接的,把原本的二元运算表达式节点转换为合适的String.Concat()方法调用节点:
roslyn/LocalRewriter_StringConcat.cs at master · dotnet/roslyn · GitHub
经过这一步,AST里就不复存在涉及字符串拼接的+运算了,全部替换成String.Concat()调用。
最后到代码生成时,代码生成器根本不需要关心如何处理涉及字符串拼接的+运算,因为前面都已经统一转换成方法调用了。
string之所以可以与int相加,根本上是调用了Concat方法。
首先int转object需要装箱,然后Concat内部调用了所有object的ToString方法,然后再new一个字符串返回。
我们发现Concat方法其实接受的是object类型的对象,这也就是说,string在与int相加的时候,会造成装箱操作。
我们知道装箱是非常耗性能的,所以我们需要避免下。
我单独写了一篇文章来讲解和分析在进行字符串拼接时所造成的性能消耗
性能优化–string 字符串拼接(超详细)
public struct A
{
public int age;
public string name;
public A(int age, string name)
{
this.age = age;
this.name = name;
}
public override string ToString()
{
return age.ToString();
}
}
public struct B
{
public int age;
public string name;
public B(int age, string name)
{
this.age = age;
this.name = name;
}
}
private void Update()
{
A a = new A(5, "A");
B b = new B(6, "B");
print(a);
print(b);
}
可以发现,输出的时候会自动调用struct的ToString方法
String s1 = new String("123");
String s2 = new String("456");
String s3 = s1 + s2;
我们打断点进去,发现底层new了一个StringBuilder,然后调用append方法(C#是Concat)
可以看看我下面的文章,里面也提及到了Java的StringBuilder
C# StringBuilder 底层深入原理分析以及使用详解