.net 技术栈之 C#基础

.net 技术栈之 C#基础
1:string 类型的恒等性,与驻留机制

2:装箱与拆箱

3:集合

4:反射

5:序列化 与 深拷贝 浅拷贝

6:  泛型

7:扩展方法

8:lamda 与 Expression 与 数据仓储 

9:委托与事件

10:线程

关于string类型的恒等性,一次性声明,其后对其有所修改,都会重新声明开辟新的对象。

实例验证:

通过重新赋值的方式来验证字符串恒等性


string a = "123";

string b = a;

a = "789";

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


因为string类型在.net 里面所属的是引用类型,如果是一般化的引用类型,必然继承与Object对象,是开辟在引用堆上面的数据,所以a在改变之后,b必然后收到影响,然而现在得出的结论是与引用属性向背离的,a的改变,没有影响到b的结果,以此来验证字符串的恒等性。当然这个也不足以说明字符串就是恒等性了。

接下来再来反编译一下看看最终的结果如何 具体的IL语言参考:http://www.jb51.net/article/39635.htm



ldstr “123” 将123 推送至Managed Heap,将Reference 地址 放入 Evaluation Stack(计算堆栈)

stloc.0 从 Evaluation Stack中弹出值(地址)并将其存储到(Call Stack) V.0 里面 即完成 a="123"

ldLoc.0 从Call Stack 里面把 0位置的Reference 压栈到 Evaluation Stack 里面

Stloc.1 弹出栈,赋值到Call Stack V.1 里面,即完成 b=a的操作

ldstr "789" 从新申请789 推送到 Managed Heap,Reference 放入 Evaluation Stack(计算堆栈)

stloc.0 弹出(Evaluation Stack) ,并将其赋值到Call Stack V.0 的位置,即完成a=“789”操作。

所以 这个时候Call Stack里面会有2个地址,即Managed Heap 里面 "123" 的地址赋值给了b,“789”的地址赋值给了 a

接下来在验证一下恒等性,通过sting里面的方法来改变数据看看是否会产生新的对象,来通过这个角度来验证string的恒等机制。

示例代码:

string a = "123456abc    ";
string bSubstring=a.Substring(0,2);
Console.WriteLine("a value is:{0} bSubstring value is:{1}", a, bSubstring);
 string cTrim=a.Trim();
 Console.WriteLine("a value is:{0} cTrim value is:{1}", a, cTrim);
        string dUpper=a.ToUpper();
        Console.WriteLine("a value is:{0} dUpper value is:{1}", a, dUpper);
       string eLower = a.ToLower();
       Console.WriteLine("a value is:{0} eLower value is:{1}", a, eLower);
       string fRemove = a.Remove(0, 1);
       Console.WriteLine("a value is:{0} fRemove value is:{1}", a, fRemove);
      string gReplace = a.Replace('1', 'x');

     Console.WriteLine("a value is:{0} gReplace value is:{1}", a, gReplace);

运行结果:



反编译图:


看到调用的是String里面的方法,先贴出SubString的源码 ,源码参考地址:https://referencesource.microsoft.com/#mscorlib/system/string.cs,8281103e6f23cb5c

 public String Substring(int startIndex, int length) {
                    
            //Bounds Checking.
            if (startIndex < 0) {
                throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex"));
            }
 
            if (startIndex > Length) {
                throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndexLargerThanLength"));
            }
 
            if (length < 0) {
                throw new ArgumentOutOfRangeException("length", Environment.GetResourceString("ArgumentOutOfRange_NegativeLength"));
            }
 
            if (startIndex > Length - length) {
                throw new ArgumentOutOfRangeException("length", Environment.GetResourceString("ArgumentOutOfRange_IndexLength"));
            }
            Contract.EndContractBlock();
 
            if( length == 0) {
                return String.Empty;
            }
 
            if( startIndex == 0 && length == this.Length) {
                return this;
            }
 
            return InternalSubString(startIndex, length);
        }
 

   [System.Security.SecurityCritical]  // auto-generated
        unsafe string InternalSubString(int startIndex, int length) {
            Contract.Assert( startIndex >= 0 && startIndex <= this.Length, "StartIndex is out of range!");
            Contract.Assert( length >= 0 && startIndex <= this.Length - length, "length is out of range!");            
            
            String result = FastAllocateString(length);
 
            fixed(char* dest = &result.m_firstChar)
                fixed(char* src = &this.m_firstChar) {
                    wstrcpy(dest, src + startIndex, length);
                }
 
            return result;
        }
 [System.Security.SecurityCritical]  // auto-generated
        internal static unsafe void wstrcpy(char *dmem, char *smem, int charCount)
        {
            Buffer.Memcpy((byte*)dmem, (byte*)smem, charCount * 2); // 2 used everywhere instead of sizeof(char)
        }

发现最后调用的是FastAllocateString,之后调用wstrcpy 做了内存赋值的工作。wstrcpy做的只是简单的内存复制工作,但它的实现却有将近300行代码。原因在于,假如要获得最好的性能,不同平台(x86/x64/IA64/ARM),当前地址是否对齐,每次复制多少字节等等,都是需要考虑的因素,因此这个方法用到了大量条件编译选项。事实上整个String类型都是这样,对于这种被大量使用的底层类库,.NET内部可谓进行了不遗余力的优化。

从中可以看出,无论是字符串连接还是取部分字符串,CPU和内存的消耗都与目标字符串的长度线性相关。换句话说,字符串越长,代价越高,假如要反复大量地操作一个大型的字符串,则会对性能产生很大影响。

关于这点可以参考老赵的文章:http://blog.zhaojie.me/2013/03/string-and-rope-1-string-in-dotnet-and-java.html

Trim,SubString,Remove 都用到了 Wstrcpy 函数来创建,具体的可以去参考源码。

至于这个研究下去,就看不到里面最核心的代码了,还是java开源的string好看,在trim,SubString,Remove,Replace,toLower,toUpper 可以看到很明显的重新创建的痕迹。



这个是java 的源码可以很明显看到,在字符串改变的时候,会重新去new对象。

结论:那么这个恒等性,immutable属性就验证到这。

接下来去验证字符串的驻留机制

用例设计:

在这里先将一下== 与equal在string里面的区别哈

==在string的时候是在IL的时候调用的是Equal,equal是字符串里面重写了,只比较值完全相等就算相等。


这个是equal源码C#

可以参考java Equal 源码,然而java里面的==与equal是不一致的,==比较的是引用,equal比较的是值在string的时候。


所以要比较测试字符串的驻留机制,所以要用ReferenceEquals来测试

string a = "helloWorld";

string b = "helloWorld";

Console.WriteLine("a ==b is :{0}",ReferenceEquals(a,b));


可以明显的看出 a 与b是相等的


这里可以看到ldstr压了2次栈,但是他们栈内存储的是同一地址。

string a = "hello"+"World";
string b = "helloWorld";

Console.WriteLine("a ==b is :{0}",ReferenceEquals(a,b)); 

这种情况,a是经国编译器优化过的,所以返回的结果为true



这种情况,在编译阶段,编译器帮我们做了优化,所以还是相等的。

string a = "hello";

string b = "world";

string c = a + b;

string d = "helloworld";

Console.WriteLine("c ==d is :{0}",ReferenceEquals(c,d));

像这种返回的就是false,


看到反编译出来的结果,不是静态编译的字符,是动态Concat拼接出来的新的string对象,凡是涉及到改变都会形成一个新的字符串,抛弃了驻留机制.

看java的代码清晰点

 public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);

    }

可以看到很明显的重新创建的痕迹


const string a = "hello";
const string b = "world";
string c = a + b;
string d = "helloworld";

Console.WriteLine("c ==d is :{0}", ReferenceEquals(c, d));


然而这样返回的结果就是true

因为const 是静态编译的概念,所以可以确定数据,不用动态运行时来确定数据。


这个是反编译出来的结果,此结果足以说明情况。


但是有一种方法可以强制驻留

string a = "hello";
string b = "world";
string c = string.Intern(a + b);
string d = "helloworld";

Console.WriteLine("c ==d is :{0}", ReferenceEquals(c, d));

这样返回的结果为true


源码


强制从Domain上面去取

这边到此就验证出来驻留机制

针对字符串这个内容,交代了其比较重要的2大特性,但是写这么多,代码该如何去有效的利用呢?提高效率呢?如何去优化代码?

建议:

1:涉及到字符串连接的时候,反复操作字符串的时候,要用StringBuilder来替代(至于StringBuilder为什么可以提高效率,这个涉及到StringBuilder的内容)默认的初始大小为16,一旦超过即需要Resize一次并增加GC压力。建议根据经验值为其指定初始大小。

2:涉及到多次连接的时候,不要用+来操作,因为+会用到Concat,可用String.format 来替代输出

3:用String.Empty是一个指代,而””是具体的实现

4:避免不必要的字符串ToUpper、ToLower类操作,用String.Compare(a,b,true) 来替代,避免不必要的新生成字符串

5:涉及比较的时候建议使用equal来替代==

6:String.IsNullOrWhiteSpace 来判断空值

 

接下来谈谈第二大主题

2:装箱与拆箱

装箱与拆箱,涉及到的是2大类型的转换问题。

值类型>引用类型 会存在Box 过程

引用类型>值类型 会存在unBox过程


因为值类型是存放在Evaluation Stack 上面的,引用类型数值是存放在Managed Heap上面的

ArrayList array = new ArrayList();
array.Add(1);

int a = (int)array[0];

为了解决数组多态传输的问题,用ArrayList

你会发现这个里面会存在装箱box,与unbox的概念


所以涉及到到2次开辟释放,装箱拆箱的操作。

string a = "111" + 10 + "dddd" + "eeee" + 100;


这个操作,将会面临2次装箱操作



stelem。ref  标示把,地址弹出放入到数组所对应的槽里面

所以看到这种情况面临这2次的装箱操作


所以验证了

值类型>引用类型 会存在Box 过程

引用类型>值类型 会存在unBox过程

那么从实际出发如何来避免这种情况呢?优化的方案有那些呢?

1:如果涉及到值类型,转换为引用类型可以用1.ToString() 来替代哈,因为它返回的就是一个String类型,所以不需要转换


至于说Number.FormatInt32 里面如何创建的看不见。

不过可以用java的源码可以推到


这里面是一个建立String的过程哈。

2:.net 里面涉及到装箱的对象,ArrayList因为里面存储的是Object类型,所以在传输值类型的时候都会,有一个Box的过程。

    Stack,Queue,SortedList,Hashtable 这个命名空间下面的System.Collections,替换为泛型操作 System.Collections.Generic.Queue《T》等来替换,通过编译器语法糖,来最终解决类型转换的问题。


3:.net 集合

首先来一个完整的概念,.net 集合继承关系图



其次谈谈简答理解,为什么要涉及到集合这个操作帮助类。我认为的原因简单归类一下:

1:动态的添加元素,因为数组作为基本的操作类型,在实际利用的时候,要在静态编译的时候,或者构造函数里面要开辟内存空间的,然而有很多场景是需要实现动态扩展,对于存储的数量属于未知数的,这样集合的一个显著特点就呈现出来了,当然这个会涉及到一个效率的问题,在后面的分析面试的时候都会问到,比如hashtable 是如何来开辟空间的,是根据什么算法来设计的

2:集合元素种类的未知性,决定了集合的使用场景。当然这一部分有点牵强,比如说我完全可以用 Object 【】 来替代,这样就是一个无敌类型了

3:当然是基于效率性能,比如说,因为集合实现了Iterator,所以集合在迭代的时候可以用foreach来操作哈,这样在很多时候对于提升效率是有帮助的哈,比如说封装了专门针对插入,搜索的一些算法。

4:当然是基于复用,现实的场景,实现了几大集合类型,比若说有针对快速搜索的hashtable,有针对插入元素有序的sortedlist,有针对key-value存在的dictionary,有针对先进先出的Queue,后进先出的Stack。针对无重复数据的Set。针对并发的一些Concurrent集合。

5:便捷方便性,因为集合都实现对lamda表达式,委托的调用,静态方法的扩展,所以以一种更加方便的方式来暴露给我们使用

就总结这么多心得


其次,来谈谈看到的看到的设计思路

1::面向接口的开发

2:实现了Iterator 的模式

3:用迭代器模式实现了延迟加载的思路

4:用了序列,链表,双向链表,二叉树,hash算法来实现高效率


接下来就是分析每个集合的代码,使用场景,我是按照4大类来学习划分的

首先来确认接口,明白接口的实际用途,设计思路

第二步进行分类学习,分为三类list,set,map 三大类 ,每一大类进行分别深入学习

第三步研究其中涉及到的算法

第四步回归设计,重写集合


那接下来说明我理解的接口编程,接口的设计用途

第一个接口就是最上层的接口就是一个IEnumerator 接口,因为所有集合都有一个特性,就是需要去搜索元素,那就少不了去循环,很合理的出现一个迭代IEnumerator接口,当然会出现2个方法

public interface IEnumerator {

    object Current{get;} //因为是取得元素,所以不需要设计set

    bool MoveNext(); //用来循环的指向的

}

那么接下来顺其自然的应该去实现这个接口

public class Enumerable :IEnumerator{   


}

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值