值类型 引用类型值类型表示存储在栈上的类型,包括简单类型(int、long、double、short)、枚举、struct定义; 引用类型表示存在堆上的类型,包括数组、接口、委托、class定义; string 是引用类型 字符特殊性
留存性。.NET运行时有个字符串常量池的概念,在编译时,会将程序集中所有字符串定义集中到一个内存池中,新定义的字符串会优先去常量池中查看是否已存在,如果存在,则直接引用已存在的字符串,否则会去堆上重新申请内存创建一个字符串。 下面是关于字符串的一些单元测试,仔细观察下各个不同: [Fact] public void Base_Test() { string a = "abc"; string b = "abc"; //字符串的留存性,初始化后会放入常量池,b直接引用a的对象 Assert.True(string.ReferenceEquals(a, b)); string c = new String("abc"); string d = new String("abc"); //直接new的话,会重新分配内存 Assert.False(string.ReferenceEquals(c, d)); Assert.False(string.ReferenceEquals(a, c)); string e = "abc"; //这里e还是使用字符串的留存性,且使用的还是a的地址。证明c分配的内存引用并没有放入常量池替换 Assert.True(string.ReferenceEquals(a, e)); Assert.False(string.ReferenceEquals(c, e)); string f = "abc" + "abc"; string g = a + b; string h = "abcabc"; //f在编译期间确定,实际还是从常量池中获取 //IsInterned 表示从常量池中获取对应的字符串,获取失败返回null //a+b实际上是发生了字符串组合运算,内部重新new了一个新的字符串,所以f,g引用地址不同 Assert.False(string.ReferenceEquals(f, g)); Assert.True(string.ReferenceEquals(string.IsInterned(f), h)); Assert.True(string.ReferenceEquals(f, h)); } Stringbuilder字符串拼接是一个非常耗资源的操作,例如 那么StringBuilder是如何实现的呢? 实际上StringBuilder内部维护了一个char数组,所有的appned类的操作都是将字符串转化为char存入数组。最后ToString()的时候才去组装string,减少了大量中间string的创建,是非常高效的字符串组装工具。 StringBuilder内部还有一个 equals ==
首先有个前提,我们所看到的equals,==,来自于System.Object对象,几乎所有的原生对象都对其进行了重写,才构成了我们目前的认知。重写equals必须重写GetHashCode。官方给出重写的实现约定如下: Equals每个实现都必须遵循以下约定:
GetHashCode:
请慎重重写Equals和GetHashCode!!重写Equals方法必须要重写GetHashCode!! 关于equals方法参数 public enum StringComparison { // // 摘要: // 使用区分区域性的排序规则和当前区域性比较字符串。 CurrentCulture = 0, // // 摘要: // 通过使用区分区域性的排序规则、当前区域性,并忽略所比较的字符串的大小写,来比较字符串。 CurrentCultureIgnoreCase = 1, // // 摘要: // 使用区分区域性的排序规则和固定区域性比较字符串。 InvariantCulture = 2, // // 摘要: // 通过使用区分区域性的排序规则、固定区域性,并忽略所比较的字符串的大小写,来比较字符串。 InvariantCultureIgnoreCase = 3, // // 摘要: // 使用序号(二进制)排序规则比较字符串。 Ordinal = 4, // // 摘要: // 通过使用序号(二进制)区分区域性的排序规则并忽略所比较的字符串的大小写,来比较字符串。 OrdinalIgnoreCase = 5 } 通常情况下最好使用 Ordinal或者OrdinalIgnoreCase,性能上最为高效。 除非有特殊的需要,不要使用 InvariantCulture或者InvariantCultureIgnoreCase,因为它要考虑所有Culture的字符转化对比情况,性能是极差的。 CurrentCulture和CurrentCultureIgnoreCase由于只有本地Culture对比,所以性能还可以接受。 参数传递首先关于参数的存储,参数是存在栈上的。传递参数时,会将对象的“值”在栈copy一份,然后将副本的值传给方法。对象参数的传递分为两种 “值传递”和“引用传递”。(注意这里的引号)
这里string虽然是引用类型,但是产生的效果缺和值类型参数传递一样的。大家参考上面关于string的特性思考下原因。 静心慢慢回味下列单元测试 [Fact] public void Base_Test() { //引用类型参数 TestClass s = new TestClass(); s.Tag = "abc"; TestMethod m = new TestMethod(); m.ReNew(s); //参数s 实际是对象 s的 地址拷贝。两者在栈上不同,但是指向的堆地址相同 //在ReNew方法中 "参数s" 重新指向了一个新的对象,但是不影响旧的对象s Assert.True(string.Equals("abc", s.Tag)); m.Change(s, "123"); //Change方法是直接修改 参数s 指向的堆对象内的字段数据,所有对象s字段也发生了变化 Assert.True(string.Equals("123", s.Tag)); m.ReNew2(ref s); //注意和ReNew的区别,因为是ref 引用传递,所有原对象引用地址指向了新new的对象地址 Assert.False(string.Equals("abc", s.Tag)); Assert.True(string.Equals("cba", s.Tag)); //值类型参数 int val = 100; //Change方法内部改变了val的值,但不影响val原来的值 m.Change(val); Assert.True(val == 100); m.Change(out val); //使用out标记,改变了val原来的值 Assert.True(val == 123); } } public class TestMethod { public void ReNew(TestClass c) { c = new TestClass() { Tag = "cba" }; } public void ReNew2(ref TestClass c) { c = new TestClass() { Tag = "cba" }; } public void Change(TestClass c, string tag) { c.Tag = tag; } public void Change(int a) { a = 123; } public void Change(out int a) { a = 123; } } public class TestClass { public string Tag { get; set; } } ref outref out都是用来标识通过引用传递方式传参。不同的是,ref 需要参数在方法调用前初始化,out 则要求参数在方法体内赋值。 装箱 拆箱装箱,即值类型转化为引用类型;从内存存储角度,将值类型从栈的值copy,然后放到堆上,并附加额外的引用类型功能内存占用(如类型指针、同步块索引等)。 拆箱,即引用类型转化为值类型。从内存存储角度,获取引用类型的指针,得到值copy,放到栈上。 从性能角度上,装箱的性能损耗>拆箱的性能损耗。在实际运用中,我们要尽量避免装箱和拆箱,这也是泛型类型出现后,一个非常大的作用就是避免了装箱拆箱的大量操作。 |
|