欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

学习笔记-String为值类型还是引用类型

程序员文章站 2022-07-15 09:40:20
...

 转载

String为值类型还是引用类型

//值类型

            int a = 1;

            int b = a;

            a = 2;

            Console.WriteLine("a is {0},b is {1}", a, b);

            //字符串

            string str1 = "ab";

            string str2 = str1;

            str1 = "abc";

            Console.WriteLine("str1 is {0},str2 is {1}", str1, str2);

            Console.Read();

根据上面的例子:你觉得输出结果应该是什么?

输出结果:

            //结果:
            //a is 2,b is 1
            //str1 is abc,str2 is ab

str2依然是ab,并没有随str1的改变而改变。

如果string是引用类型,按理Str1Str指针都指向同一内存地址,如果Str的内容发生改变,Str1应该也会相应变化。

此例子,看着string更像是值类型。 

但是MSDN却说String是引用类型,

 

引用类型包括: 
String

所有数组,即使其元素是值类型

类类型,如 Form

委托

查看具体引用是否相同

如果Net能够查看内存地址就容易了,但不允许,只能通过间接方法来实现,看下面:

       static void TestRefAddress()
        {
            String str1 = "abc";
            String str2 = "abc";
            int a = 1;
            int b = 1;
            StringBuilder strb1 = new StringBuilder("abc");
            StringBuilder strb2 = new StringBuilder("abc");
            Console.WriteLine("Reference equal for string: " + Object.ReferenceEquals(str1, str2)); //结果true
            Console.WriteLine("Reference equal for int: " + Object.ReferenceEquals(a, b)); //结果false
            Console.WriteLine("Reference equal for StringBuilder: " + Object.ReferenceEquals(strb1, strb2)); //结果false
            Console.WriteLine("Value equal for string: " + str1.Equals(str2)); //结果true,类似于值类型
            Console.Read();
        }

结果为何出现如此情况,分析如下:

    Console.WriteLine("Reference equal for string: " + Object.ReferenceEquals(str1, str2)); //结果true,不同对象,但引用地址相同
    Console.WriteLine("Reference equal for int: " + Object.ReferenceEquals(a, b)); //结果false,值类型装箱操作造成
    Console.WriteLine("Reference equal for StringBuilder: " + Object.ReferenceEquals(strb1, strb2)); //结果false,不同对象,引用地址不同
    Console.WriteLine("Value equal for string: " + str1.Equals(str2)); //结果true,类似于值类型

由第一条结果,可以判定不同的String的,相同的值,其引用地址相同,再由第四条结果,str1.Equals(str2),两者结合,可得出结论,两个String,如果赋值为同一个值,在内存中只有一个字符串存在,两个引用的地址相同。由此引出String的不变性。

String的不变性

string最为显著的一个特点就是它具有恒定不变性:我们一旦创建了一个string,在managed heap 上为他分配了一块连续的内存空间,我们将不能以任何方式对这个string进行修改使之变长、变短、改变格式。所有对这个string进行各项操作(比如调用ToUpper获得大写格式的string)而返回的string,实际上另一个重新创建的string,其本身并不会产生任何变化。 
string  
对象称为不可变的(只读),因为一旦创建了该对象,就不能修改该对象的值。有的时候看来似乎修改了,实际是string经过了特殊处理,每次改变值时都会建立一个新的string对象,变量会指向这个新的对象,而原来的还是指向原来的对象,所以不会改变。这也是string效率低下的原因。

String的不变,并非说string不能改变,而是其值不能改变。

在例子中str1="ab",这时在内存中就将“ab”存下来,如果再创建字符串对象,其值也等于“ab”str2="ab",则并非再重新分配内存空间,而是将之前保存的“ab”的地址赋给str2的引用,这就能印证例子2中的结果。而当str1="abc"其值发生改变时,这时检查内存,发现不存在此字符串,则重新分配内存空间,存储“abc”,并将其地址赋给str1,而str2依然指向“ab”的地址。可以印证例子1中的结果。

String是引用类型,只是编译器对其做了特殊操作

个人总结:

赋值为同一个值,内存地址相同。String的驻留性

对string的各项操作实际上创建了新的string,本身不改变。不变性

string   对象称为不可变的(只读)

String的不变,并非说string不能改变,而是其值不能改变。

 

https://www.cnblogs.com/peterpan0707007/p/6531194.html转载

string是一个引用类型,其对象值存储在托管堆中。string的内部是一个char集合,他的长度Length就是字符char数组的字符个数。string不允许使用new string()的方式创建实例,而是另一种更简单的语法,直接赋值(string aa= “000”这一点也类似值类型)。

public void DoStringTest()
{
    var aa = "000";
    SetStringValue(aa);
    Console.WriteLine(aa);
}

private void SetStringValue(string aa)
{
    aa += "111";
}

上面的输出结果为“000”

通过前面的值类型与引用类型的文章,我们知道string是一个引用类型,既然是一个引用类型,参数传递的是引用地址,那为什么不是输出“000111”呢?是不是很有值类型的特点呢!这一切的原因源于string类型的两个重要的特性:恒定性驻留性

 

 String的恒定性(不变性)

字符串是不可变的,字符串一经创建,就不会改变,任何改变都会产生新的字符串。比如下面的代码,堆上先创建了字符串s1=”a”,加上一个字符串“b”后,堆上会存在三个个字符串实例,如下图所示。

string s1 = "a";
string s2 = s1 + "b";

学习笔记-String为值类型还是引用类型

上文中的任何改变都会产生新的字符串,包括字符串的一些操作函数,如str1.ToLowerTrim()Remove(int startIndex, int count)ToUpper()等,都会产生新的字符串,因此在很多编程实践中,对于字符串忽略大小的比较:

if(str1.ToLower()==str2.ToLower()) //这种方式会产生新的字符串,不推荐
if(string. Compare(str1,str2,true)) //这种方式性能更好

String的驻留性

由于字符串的不变性,在大量使用字符串操作时,会导致创建大量的字符串对象,带来极大的性能损失。因此CLR又给string提供另外一个法宝,就是字符串驻留,先看看下面的代码,字符串s1s2竟然是同一个对象!

 

var s1 = "123";
var s2 = "123";
Console.WriteLine(System.Object.Equals(s1, s2));  //输出 True
Console.WriteLine(System.Object.ReferenceEquals(s1, s2));  //输出 True

 

相同的字符串在内存(堆)中只分配一次,第二次申请字符串时,发现已经有该字符串是,直接返回已有字符串的地址,这就是驻留的基本过程。

字符串驻留的基本原理:

  • CLR初始化时会在内存中创建一个驻留池,内部其实是一个哈希表,存储被驻留的字符串和其内存地址。
  • 驻留池是进程级别的,多个AppDomain共享。同时她不受GC控制,生命周期随进程,意思就是不会被GC回收(不回收!难道不会造成内存爆炸吗?不要急,且看下文)
  • 当分配字符串时,首先会到驻留池中查找,如找到,则返回已有相同字符串的地址,不会创建新字符串对象。如果没有找到,则创建新的字符串,并把字符串添加到驻留池中。

如果大量的字符串都驻留到内存里,而得不到释放,不是很容易造成内存爆炸吗,当然不会了?因为不是任何字符串都会驻留,只有通过IL指令ldstr创建的字符串才会留用

 

字符串创建的有多种方式,如下面的代码:

var s1 = "123";
var s2 = s1 + "abc";
var s3 = string.Concat(s1, s2);
var s4 = 123.ToString();
var s5 = s2.ToUpper();

IL代码如下

学习笔记-String为值类型还是引用类型

 

在上面的代码中,出现两个字符串常量,“123”“abc”,这个两个常量字符串在IL代码中都是通过IL指令ldstr创建的,只有该指令创建的字符串才会被驻留,其他方式产生新的字符串都不会被驻留,也就不会共享字符串了,会被GC正常回收

 

 

那该如何来验证字符串是否驻留呢,string类提供两个静态方法:

  • String.Intern(string str) 可以主动驻留一个字符串;
  • String.IsInterned(string str);检测指定字符串是否驻留,如果驻留则返回字符串,否则返回NULL

学习笔记-String为值类型还是引用类型

 

var s1 = "123";
var s2 = s1 + "abc";
Console.WriteLine(s2);   //输出:123abc
Console.WriteLine(string.IsInterned(s2) ?? "NULL");   //输出:NULL。因为“123abc”没有驻留
 
string.Intern(s2);   //主动驻留字符串
Console.WriteLine(string.IsInterned(s2) ?? "NULL");   //输出:123abc

 

 认识StringBuilder

大量的编程实践和意见中,都说大量字符串连接操作,应该使用StringBuilder。相对于string的不可变,StringBuilder代表可变字符串,不会像字符串,在托管堆上频繁分配新对象,StringBuilder是个好同志。

 

首先StringBuilder内部同string一样,有一个char[]字符数组,负责维护字符串内容。因此,与char数组相关,就有两个很重要的属性:

  • public int CapacityStringBuilder的容量,其实就是字符数组的长度。
  • public int LengthStringBuilder中实际字符的长度,>=0<=容量Capacity

StringBuilder之所以比string效率高,主要原因就是不会创建大量的新对象,StringBuilder在以下两种情况下会分配新对象:

  • 追加字符串时,当字符总长度超过了当前设置的容量Capacity,这个时候,会重新创建一个更大的字符数组,此时会涉及到分配新对象。
  • 调用StringBuilder.ToString(),创建新的字符串。

 

追加字符串的过程:

  • StringBuilder的默认初始容量为16
  • 使用stringBuilder.Append()追加一个字符串时,当字符数大于16StringBuilder会自动申请一个更大的字符数组,一般是倍增;
  • 在新的字符数组分配完成后,将原字符数组中的字符复制到新字符数组中,原字符数组就被无情的抛弃了(会被GC回收);
  • 最后把需要追加的字符串追加到新字符数组中;

 

简单来说,当StringBuilder的容量Capacity发生变化时,就会引起托管对象申请、内存复制等操作,带来不好的性能影响,因此设置合适的初始容量是非常必要的,尽量减少内存申请和对象创建。代码简单来验证一下:

StringBuilder sb1 = new StringBuilder();

Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //输出:Capacity=16; Length=0;   //初始容量为16

sb1.Append('a', 12);    //追加12个字符

Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //输出:Capacity=16; Length=12; 

sb1.Append('a', 20);    //继续追加20个字符,容量倍增了

Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //输出:Capacity=32; Length=32; 

sb1.Append('a', 41);    //追加41个字符,新容量=32+41=73

Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //输出:Capacity=73; Length=73; 

 

StringBuilder sb2 = new StringBuilder(80); //设置一个合适的初始容量

Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //输出:Capacity=80; Length=0;

sb2.Append('a', 12);

Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //输出:Capacity=80; Length=12;

sb2.Append('a', 20);

Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //输出:Capacity=80; Length=32;

sb2.Append('a', 41);

Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //输出:Capacity=80; Length=73;

为什么少量字符串不推荐使用StringBuilder呢?因为StringBuilder本身是有一定的开销的,少量字符串就不推荐使用了,使用String.ConcatString.Join更合适。

 

4-8连接使用stringbulid

高效的使用字符串

  • 在使用线程锁的时候,不要锁定一个字符串对象,因为字符串的驻留性,可能会引发不可以预料的问题;
  • 理解字符串的不变性,尽量避免产生额外字符串,如:
if(str1.ToLower()==str2.ToLower()) //这种方式会产生新的字符串,不推荐
if(string. Compare(str1,str2,true)) //这种方式性能更好
  • 在处理大量字符串连接的时候,尽量使用StringBuilder,在使用StringBuilder时,尽量设置一个合适的长度初始值;
  • 少量字符串连接建议使用String.ConcatString.Join代替。

https://www.cnblogs.com/anding/p/5240313.html转载

String.Join

在指定 String 数组的每个元素之间串联指定的分隔符 String,从而产生单个串联的字符串。

String.Concat

连接 String 的一个或多个实例,或 Object 的一个或多个实例的值的 String 表示形式。

String.Join可以添加自己的连接符 a+b+c

String.Concat单纯的只是连接        abc

C#中字符串大小比较函数--CompareTo与Compare方法

字符串比较的原理是什么?

原理:

  从两个字符串的第一个字符开始逐个进行比较(按字符的ASCII值进行大小比较),直到出现不同的字符或遇到‘\0’为止。

  如果全部字符都相同,就认为两字符串相等,返回0

  若出现了不相同的字符,则以第一个不相同的字符比较结果为准,若前者字符大于后者,则返回1,否则返回-1.

相关标签: C# .net String