• C# string为什么可以与int相加? string字符串拼接深入分析


    前言

    最近在研究关于string的一些东西,发现底层实现挺有意思的,想来研究下string字符串拼接底层的实现原理

    字符串拼接

    先来看看下面的代码

    string str = "12";
    int num = 34;
    str = str + num;
    Console.WriteLine(str);
    
    • 1
    • 2
    • 3
    • 4

    输出结果:1234

    按道理来说,在string.cs并没有对 String 类的 + 符号进行重载,int不可以和string进行相加,但是相加的结果确实可以正确输出,所以到底是怎么回事呢?

    分析

    我们来看看下面

    1. string + int
    Console.WriteLine("12" + 34);
    
    • 1

    生成的IL代码为(省略无关部分):

    IL_000d:  call       string [mscorlib]System.String::Concat(object,
                                                                  object)
    IL_0012:  call       void [mscorlib]System.Console::WriteLine(string)
    
    • 1
    • 2
    • 3
    1. int + string
    Console.WriteLine(12 + "34");
    
    • 1

    生成的IL代码为(省略无关部分):

    IL_000d:  call       string [mscorlib]System.String::Concat(object,
                                                                  object)
    IL_0012:  call       void [mscorlib]System.Console::WriteLine(string)
    
    • 1
    • 2
    • 3
    1. int + string + int
    Console.WriteLine(12 + "3" + 45);
    
    • 1

    生成的IL为(省略无关部分):

    IL_0014:  call       string [mscorlib]System.String::Concat(object,
                                                                  object,
                                                                  object)
    IL_0019:  call       void [mscorlib]System.Console::WriteLine(string)
    
    • 1
    • 2
    • 3
    • 4
    1. string + int + int + int
    Console.WriteLine("12" + 34 + 56 + 78);
    
    • 1

    生成的IL为(省略无关部分):

    IL_002f:  call       string [mscorlib]System.String::Concat(object[])
    IL_0034:  call       void [mscorlib]System.Console::WriteLine(string)
    
    • 1
    • 2

    毫无疑问,它们都调用了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 字符串拼接(超详细)

    拓展一:与struct相加

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在这里插入图片描述
    可以发现,输出的时候会自动调用struct的ToString方法

    拓展二:Java的底层实现

    String s1 = new String("123");
    String s2 = new String("456");
    String s3 = s1 + s2;
    
    • 1
    • 2
    • 3

    我们打断点进去,发现底层new了一个StringBuilder,然后调用append方法(C#是Concat)

    可以看看我下面的文章,里面也提及到了Java的StringBuilder
    C# StringBuilder 底层深入原理分析以及使用详解

    在这里插入图片描述
    最后toString new一个字符串出来,再返回
    在这里插入图片描述

  • 相关阅读:
    Himall商城图形码帮助类二维码中生成图片(1)
    RabbitMQ二、RabbitMQ的六种模式
    【Linux系统化学习】进程的父子关系 | fork 进程
    IDEA重新choose source
    Java基础17 java的多线程 Thread类和run方法 Lamda表达式 线程的状态(run、sleep、join、yield))
    【autodl/linux配环境心得:conda/本地配cuda,cudnn及pytorch心得】
    【Linux】:Kafka基础命令
    强化学习章节脉络
    5分钟学会 gRPC
    Linux磁盘分区与挂载
  • 原文地址:https://blog.csdn.net/qq_52855744/article/details/126817090