var str = new string(string)问题

前几日,广州群有小伙伴遇到了这样一道面试题

1
var s = new string("abc");

关于以上语句到底创建了几个对象呢?由于印象里这种使用 string 来初始化 string 的操作并不常见,推测以下,应该是类似于 copy 的操作产生一个新的对象,所以我先盲猜,不考虑有上下文,只针对这一句语句,应该是2个。

下面来试验:

定义

  1. string 属于引用类型,所以我们把单个 string 定义为堆中的对象,两个或多个引用类型变量引用同一个对象时,依然认为只有一个对象。

分析

文档分析

首先,事实证明了不是我对 string 了解太少,而是被公司从策划 diss 了几次的升级计划导致我常用的 .NET Core 版本一直是 1.1 ,而用 string 初始化 string 的用法只有在 .NET Core 2.1 及以上版本才有,确切的说,只有 .NET Core 2.1、.NET Core 2.2、.NET Core 3.0 三个版本才有,以上结论来自官方文档并经过实际测试,文档如下:
** .NET Core 2.0 String Constructors**

** .NET Core 2.1 String Constructors**

由官方文档可知,.NET Core 从 2.0 升级为 2.1 时,增加了一个构造函数String(ReadOnlySpan<Char>)

题目中的 new(“qqq”)使用的正式这一构造函数,入参为 ReadOnlySpan<Char>结构体泛型,而这一类型也正是从 .NET Core 2.1 版本才新引入的。

故,这一题目在 .NET Core 2.1 - .NET Core 3.0 版本成立,其余.NET 版本中均无法编译通过

源码分析

下面开始直接代码寻找答案,查看 String.cs 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
[MethodImpl(MethodImplOptions.InternalCall)]
public extern String(ReadOnlySpan<char> value);

#if !CORECLR
static
#endif
private unsafe string Ctor(ReadOnlySpan<char> value)
{
if (value.Length == 0)
return Empty;

string result = FastAllocateString(value.Length);
Buffer.Memmove(ref result._firstChar, ref MemoryMarshal.GetReference(value), (uint)value.Length);
return result;
}
...
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator ReadOnlySpan<char>(string? value) =>
value != null ? new ReadOnlySpan<char>(ref value.GetRawStringData(), value.Length) : default;

由官方源码可知,当入参为 string 时,首先会调用隐式转换方法将入参转换为ReadOnlySpan<char>类型,而后再使用转换后的值调用构造函数String(ReadOnlySpan<char> value),构造函数中,重新分配了新的内存空间给新字符串,将入参的值 Copy 到新字符串后返回,所以该构造函数会产生一个全新的字符串值

测试

测试代码

以下是试验代码:

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
using System;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
class Program
{

static void Main(string[] args)
{
string str0 = null;
var str1 = new string("123");
var str2 = "123";
var str3 = str2;
var str4 = "123";
Console.WriteLine($"{GetMemoryAddress(str0)} - {GetMemoryAddress(str1)} - {GetMemoryAddress(str2)} - {GetMemoryAddress(str3)} - {GetMemoryAddress(str4)}");
Console.WriteLine($"{object.ReferenceEquals(str0,str1)} - {object.ReferenceEquals(str1,str2)} - {object.ReferenceEquals(str2,str3)} - {object.ReferenceEquals(str3,str4)}");
Console.ReadKey();

}

private static string GetMemoryAddress(object o)
=>
$"0x{GCHandle.Alloc(o, GCHandleType.Pinned).AddrOfPinnedObject().ToString("X")}";
}
}

输出结果为:

IL代码


由 IL 代码可知,的确是先调用了隐式转换函数,而后再使用转换后的值调用构造函数String(ReadOnlySpan<char> value)

结论

  1. 这一题目在 .NET Core 2.1 - .NET Core 3.0 版本成立,其余.NET 版本中均无法编译通过。
  2. 如果传入的字符串已在字符串常量池中,那么该语句只会产生一个新的 string 对象。
  3. 如果传入的字符串未在字符串常量池中,那么该语句会产生两个新的string对象。