写在开头:
http://www.cnblogs.com/luminji 157个建议_勘误表
https://www.iteye.com/blog/dsqiu-2029701 //文章
https://www.cnblogs.com/ricolee/category/1199290.html //字符串深入理解
一:属性
属性和方法一样。也可以是virtual和abstract.
运行时常量优于编译时常量【能正确运行才是关键】。编译时常量比运行时常量稍微块一点,但是缺乏灵活性。性能非常关键,其值永远不变的情况下,我们才应该使用编译时常量。
c# readonly 运行时常量【构造器一旦执行则不能对值进行修改】 const编译时常量
编译时常量编译后会把该常量替换成常量的值,类似于c++的宏【编译时常量只可以用于基元类型(整数浮点数枚举字符串)】。运行时编译后任然是该变量的引用
操作符as和is都只检查被转换对象的运行时类型,并不执行其他的操作。如果被转换对象的运行时类型既不是所转换的目标类型,也不是其派生类型,那么转型将告失败
as操作符不能用于值类型,int值类型,不能为null.
as是直接转
只有当我们不能使用as操作符来进行类型转换时,才应该使用is操作符。
如果我们打算使用as来做转型,那么再使用is检查就没有必要了。直接将as操作符的运算结果和null进行比对就可以了,这样比较简单
c# is和as的区别
is就是处于对类型的判断。返回true和false。如果一个对象是某个类型或是其父类型的话就返回为true,否则的话就会返回为false。另外is操作符永远不会抛出异常。代码如下:
System.Boolean b1 = (o is System.Object);//b1 为true
System.Boolean b2 = (o is Employee);//b2为false
如果对象引用为null,那么is操作符总是返回为false,因为没有对象可以检查其类型,就像下面代码一样
if(o is Employee)
{
Employee e = (Employee) o;
//在if语句中使用e
}
as :as必须和可以为NUll类型使用。转int则不行
Employee e = o as Employee;
if(e != null)
{
//在if语句中使用e
}
注意父类转子类的情况,父类转子类转不了的;
这种as操作即便等同于上面代码,同时只进行了1次的类型检查,所以提高了性能。如果类型相同就返回一个非空的引用,否则就返回一个空引用。
条款4:使用Conditional特性代替#if条件编译
条款5:总是提供ToString()方法
条款6:明辨值类型和引用类型的使用场合
值类型用于存储数据,引用类型用于定义行为
条款7:将值类型尽可能实现为具有常量性和原子性的类型
枚举将None=0声明出来
ReferenceEquals(): 不管比较的是引用类型还是值类型,该方法都判断的是“引用相等”,而非“值相等”,意味着如果我们使用此来比较两个值类型,其结果永远返回false。即使我们将一个值类型和自身进行比较,ReferenceEquals()的返回值仍是false。导致这种结果的原因在于装箱
Object.Equals() 默认是引用判断,但是值类型例外,判断值类型时需要重写Equals()方法。如果两个值类型变量的类型相同,并且内容一致,这两个变量才被认为相等。
判断是否引用的同一个对象时的注意点: string a="aa"; string b = "aa"; 两个比较都是相等的。这是因为系统并没有给字符串b分配内存,只是将"aa"指向了b。所以a和b指向的是同一个字符串(字符串在这种赋值的情况下做了内存的优化)
//静态Object.Equals()方法的实现
public static bool Equals( object left, object right )
{
// 检查是否引用相等。
if (left == right ) //System.object类型的==实现的是引用相等
return true;
// 两者同时为null引用的情况在上面已经处理。
if ((left == null) || (right == null))
return false;
return left.Equals (right);
}
总结:
<1. 引用类型中Equals是和ReferenceEquals()是一样的,比较的是引用。如果是string的比较( string a="aa"; string b = "aa";),因为有内存优化,所以Equals比较出来的引用一样的。
<2. 值类型中Equals和==是一样的。比较的是值。
相等的数学属性:自反(任何对象都与其自身相等)、对称(相等判断的顺序是无关紧要的)和可传递(a=b b=c 则a=c)
2019/10/28复习:
"==" 和 “Equals” :两者都是如果是比较的是值类型,即比较的是值,如果比较的引用类型(自定义),则比较的是引用。
所以为了明确有种方法是比较的引用相等性,则Object.ReferenceEquals
FCL:框架类库(Framework Class Library)
对于string类型,微软觉着它的作用更加接近于值类型,因此在FCL中,string的比较被重载为针对 "类型的值" 的比较,而不是针对 “引用本身” 的比较。
[ 在需要与默认实现不同的比对行为时就需要重写 ]。
https://www.cnblogs.com/michaelxu/archive/2008/05/06/1184385.html //字符串赋值问题和比较问题解释:
https://gameinstitute.qq.com/community/detail/114056 //字符串拘留在系统字符串的解释
<1.字符串是引用类型。
<2.字符串的string a="abc" string b=a; 两个的引用指向的是同一个对象,但是和其他的引用类型不同,改变a不会改变b,因为在改变a的时候,又创建了一个新的字符串(这也是效率低的问题),所以变a,不会改变b.
<3.比较。string a="abc" string b="abc",比较时
当我们用System.Object.Equals(a,b)比较时,返回值是true;按理说str1和str2应该指向不同的空间,应该返回false才对啊。原来Equals有三个版本:
前两个实例方法内部会调用CompareOrdinal静态方法,它会比较字符串中的各个字符,如果相等就返回true。第三个首先会检查两个引用指向的是否是同一个对象,如果是,就返回true,不再去比较各个字符了。在检查是否是同一个对象时,因为CLR使用了一种叫字符串驻留的技术,对于
string str1="abc";
string str2="abc";
当CLR初始化时,会创建一个内部的散列表,其中的键为字符串,值为指向托管堆中字符串的引用。刚开始,散列表为空,JIT编译器编译方法时,会在散列表中查找每一个文本常量字符串,首先会查找"abc"字符串,并且因为没有找到,编译器会在托管堆中构造一个新的指向"abc"的String对象引用,然后将"abc"字符串和指向该对象的引用添加到散列表中。
接着,在散列表中查找第二个"abc",这一次由于找到了该字符串,所以编译器不会执行任何操作,代码中再没有其它的文本常量字符串,编译器的任务完成,代码开始执行。执行时,CLR发现第一个语句需要一个"abc"字符串引用,于是,CLR会在内部的散列表中查找"abc",并且会找到,这样指向先前创建的String对象的引用就被保存在变量s1中,执行第二条语句时,CLR会再一次在散列表中查找"abc",并且仍然会找到,指向同一个String对象的引用会被保存在变量s2中,到此s1和s2指向了同一个引用,所以System.Object.Equals(s1,s2)就会返回true了。
条款10:理解GetHashCode()方法的缺陷
注意; a="aa" b="aa" unity中可以通过散列码GetHashCode()间接的查看两个变量的地址是否相等,但是数组的地址是连续存储的,但是输出的散列码确实一样的???。
条款11:优先采用foreach循环语句
int [] foo = new int[100];
// 循环1:
foreach ( int i in foo)
Console.WriteLine( i.ToString( ));
// 循环2:
for ( int index = 0; index < foo.Length; index++ )
Console.WriteLine( foo[index].ToString( ));
// 循环3:
int len = foo.Length;
for ( int index = 0; index < len; index++ )
Console.WriteLine( foo[index].ToString( ));
c#1.0以上则第一个最好(c#1.0的化 第二个最好【因为第一个由装箱】)。
在1.0版本的编译器产生的代码中,在数组上使用foreach语句实际上是通过IEnumerator接口来遍历数组,而这会导致装箱与拆箱操作:遍历类型=(遍历类型)Current(接口类型); 1.0以后用的是for来遍历的
foreach语法简洁 自带finally{ dispose() } 释放内存 。
扩展: Unity5.5版本之后修复了foreach的GC http://www.mamicode.com/info-detail-2103245.html
第三个最差:
原因解析:安全托管环境中每个内存都会检查,而通过将Length变量放到循环之外,实际上阻碍了JIT编译器移除循环中的范围检查。
反编译后:
int len = foo.Length;
for ( int index = 0; index < len; index++ )
{
if ( index < foo.Length )
Console.WriteLine( foo[index].ToString( ));
else
throw new IndexOutOfRangeException( );
}
CLR 会在访问每一个特定数组元素之前,产生一个数组界限(并非上面的len变量)测试。C#编译器和JIT编译器可以确保循环中的数组界限是安全的。只要循环变量不是数组的Length属性,每一次迭代时都会执行数组界限检查。破坏了JIT本身的优化
《编写高质量代码改善C#程序的157个建议》
string:
1. 值类型转string时,需要重写ToString(),使其调用值类型中的ToString()方法。因为值类型中的ToString()是非托管代码,可以直接操作内存来完成操作,效率高很多。
Debug.LogError("wyj "+9); //需要装箱
Debug.LogError("wyj " + 9.ToString()); //效率高,因为调用的是值类型中的ToString()方法
2. 字符串拼接时,使用StringBuilder。如果没有先定义长度的话,则默认分配长度未16。当字符串小于16时,不会重写分配。32>=str>=16时,则重写分配,使之成为16的倍数。注意指定的长度摇合适,太小需要频繁分配内存。
StringBuilder
3. Format格式化,内部是使用的stringbuilder.
StringBuilder和string的性能测试对比:
固定长度的StringBuilder和每次创建新的字符串的相比:StringBuilder性能强于string.即对一个字符串频繁的操作用StringBuilder.
StringBuilder sb = new StringBuilder(1000);
void Update () {
if (Input.GetMouseButtonDown(0))
{
string name = "";
for (int row = 0; row < 1000; row++)
{
sb.Append("2");
}
Debug.Log(sb.ToString());
}
if (Input.GetMouseButtonDown(1))
{
string name = "";
for (int row = 0; row < 1000; row++)
{
name += "2";
}
Debug.Log(name);
}
}
并不是把所有的字符串都换个StringBuilder,如果过程中创建了很多的StringBuilderd对象,那样也不好。
枚举:最好不要赋值 , 如果赋值的话最好从0开始
4.重载运算符:
用户自己定义的运算方式,一般用于对几个对象之间内部进行的一些操作。
5. 重写Equals时也要重写GetHashCode
如果自定义对象被用作基于散列集合的键,则建议重写Equals方法。查询时是基于key值的HashCode来查找键值的。【如果需要所有new的对象当成一个key,即需要重写HashCode(),包装一个int值的HashCode来当作该对象创建的所有对象的HashCode();
字符串不同到那时产生的HashCode()是一样的情况和原因。得到的哈希值是int型,而如果是字符串,字符串的长度和这个值的大小是正比,过长的字符串会导致这个值超过int.max,所以会哈希值一样的情况,解决方案是在这个哈希值的前边把方法名加上。
string str1 = "ABCDEa123abc";
string str2 = "ABCDFB123abc";
Debug.Log(str1.GetHashCode()+" "+ str2.GetHashCode());
}
public int hashCode()
{
int h = hash; // hash默认值为0
int len = count;// count是字符串的长度
if (h == 0 && len > 0)
{
int off = offset; // offset 是作为String操作时作为下标使用的
char val[] = value;// value 是字符串分割成一个字符数组
for (int i = 0; i < len; i++)
{
// 这里是Hash算法体现处, 可以看到H是一个哈希值,每次是将上次一算出的hash值乘以31 然后再加上当前字符编码值,
//由于这里使用的是int肯定会有一个上限,当字符长时产生的数值过大int放不下时会进行截取,一旦截取HashCode的正确性就无法保证了,
//所以这点可以推断出HashCode存在不相同字符拥有相同HashCode。
h = 31 * h + val[off++];
}
hash = h;
}
return h;
}
14. 正确实现浅拷贝和深拷贝
浅拷贝: 值类型拷贝的是值 引用类型拷贝的是引用
深拷贝: 值类型拷贝的是值 引用类型拷贝的是引用指向的值
第2章 集合和LINQ
16. 元素数量可变的情况下不应该使用数组
不要让数组成为大对象【>85000字节数】,大对象的回收效率低,有性能瓶颈。
17.多数情况下使用foreach遍历
理由:语法简洁 自带finally{ dispose() } 释放内存 。
for[索引器实现的] foreach(迭代器实现)
foreach不该修改内部元素的原因: foreach对集合版本进行判断,任何对集合的增删改查都会使版本号+1 . MoveNext() 会进行版本号的检查,有变动时会抛出异常【System.InvalidOperationException】。
foreach (int value in list)
{
Console.WriteLine("值: " + value);
list.Remove(value);
}
一般使用匿名函数或者lambda 来对数据进行查询
第三章 泛型、委托和事件
32 总是优先使用泛型
若T指向的数据类型是一致的,那么泛型对象间可以共享静态成员。但是为了规避混淆,泛型中要避免申明静态成员。
private void Start()
{
A_Books aint = new A_Books();
A_Books astr = new A_Books();
Debug.Log(A_Books.num+" " + A_Books.num); //6 6
A_Books aint_1 = new A_Books();
Debug.Log(A_Books.num); //7
}
}
public class A_Books
{
public static int num=5;
public A_Books()
{
num++;
}
}
泛型方法: 非泛型类型中的泛型方法,并不会在运行时的本地代码中生成不同的类型。
泛型参数增加该泛型参数的行为。编码时多考虑对泛型进行约束
使用default为泛型类型变量指定初始值:
当返回值是一个泛型类型时,则
37 . 使用lambda表达式代替匿名方法:
38. 小心闭包中的陷阱。
闭包: 指能够读取其他函数内部变量的函数。
这样即使代码执行后离开了局部变量i的作用域【如for循环】,包含该闭包对象的作用域也还存在。即i最后执行++操作后的值。
下述代码避免闭包:在委托函数外部定义局部变量,相当于把5个局部变量的对象引用提升到闭包对象中
闭包的实现过程: 通过捕获变量来实现的闭包。通过闭包才能访问到变量的预期值。
猜测这个委托类的创建是在调用时才生成的,所以在执行委托函数时,成员变量(下边的row=5了,在输出时,执行5次函数都是输出5)
List list = new List ();
void TestFun()
{
for(int row=0;row<5;row++)
{
int count=row*10;
list.Add(delegate
{
print(count);
count=count+1;
});
}
//每个元素都是一个委托,每个委托都是创建了一个新的count
//如果把count换成是row,捕获变量从5开始,猜测这个委托类的创建是在调用时才生成的
foreach(MethodInvoke t in list)
{
t();
}
print("调用同一个委托");
//此时count是上次foreach中 list[0]委托中已经创建过委托实例了,即该实例类中也有了count变量,
//所以再次调用是上次调用后得到的值=1。
list[0]();//1
list[0]();//2
list[0]();//3
print("调用另一个委托");
list[1]();//11
}
40. 泛型参数兼容泛型接口的不可变型 泛型的可变性// 基础不够以后再研究 ???
协变: 让返回值类型返回比声明的类型派生程度【子类比父类派生程度大】更大的类型,就是协变。
逆变:方法的参数可以是委托或者泛型接口的参数类型的基类。
out在c#4.0 新增功能,可以在泛型接口和委托中使用,用来让类型支持协变。
除非考虑到该委托声明肯定不会用于可变性,否则为委托中的泛型参数指定out关键字将会扩展该委托的应用。
public delegate TResult Func
第四章 资源管理
托管资源: 由CLR管理和分配
非托管:不受CLR管理的对象,套接字,文件,数据库链接,windows内核,com对象
53.必要时应将不再使用的对象的引用赋值为null
引用赋值为null 没 必要的情况:
局部变量和方法的参数变量,无论我们是否在方法内部将局部变量赋值为null,a=null该语句会被忽略。这也说明JIT编译器是一个优化过的编译器。如果是Release模式,则a=null都不会编译进运行时。
引用赋值为null必要的情况:
静态字段,比如创建一个对象,该对象中有静态字段,当该局部变量对象被释放后,该对象中的静态字段不会被释放。因为静态字段创建后,该“根”就一直存在。所以手动置为null. 这也是最好少用静态字段的原因。
54. 为无用字段标注不可序列化
55. 利用定制特性减少可序列化的字段
第6章 异步 多线程 任务 并行
https://blog.csdn.net/qq_36936155/article/details/78991050
71. 区分异步和多线程应用场景
DMA(Direct Memory Access): 直接内存访问,是一种不经过CPU而直接进行内存数据存储的数据交换模式。几乎不损耗CPU. CLR异步编程模型就是充分利用DMA功能释放CPU压力。
多线程本质: 创建一个线程,一直等待获取数据,一直占着CPU资源。线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。
异步本质:开始异步操作时,CLR把工作交给线程池中的某个线程进行完成。当开始IO操作时,异步会把工作线程还给线程池。相当于获取工作不会再占用CPU资源,直到异步完成,获取数据结束后,异步才会通知回调的方式通知线程池。先干别的事,当它需要的数据准备完毕后,再会来干这件事。
计算 密集型工作: 多线程,(例如耗时较长的图形处理和算法执行)
IO 密集型工作: 采用异步机制。(文件,网络数据修改,数据库操作、Web Service、HttpRequest以及.Net Remoting等跨进程的调用)
多线程创建线程,一直等待,获取数据,获取完毕。异步线程池中的线程,等待。开始IO操作时,还给线程池,获取完毕后回调。
异步,让线程池中的一个线程获取网页,获取后开始IO操作(读取网页),此时把线程还给线程池,直到异步完成,即获取网页完毕后,异步才会通过回调的方式通知线程池。
72. 在线程同步中使用的信号量
EventWaitHandle 维护一个内核产生的布尔类型对象(“阻滞状态”),如果值=false,那么在上边等待的线程就阻塞【应用程序域内的线程同步】
Semaphore: 维护一个内核产生的整形变量。值=0,则在上边等待的线程就阻塞。>0解除阻塞,每解除一个其值减1.【应用程序域内的线程同步】
Mutex : 可以跨域阻塞和解除阻塞。
lock锁注意点:
1. 主要是锁对象,不能锁值类型【值是以拷贝方式】 ,
2. 不能锁字符串,没有必要而且很危险【如果两个变量分配了相同内容的字符串,那么两个引用指的同一个内存,用了锁后,实际锁的是同一个对象,会导致程序崩溃,即另一个也会被锁住】
3. 不能写成lock(this) 会new几个对象,达不到锁定的目的。
同步锁时很耗费时的。线程池中的线程默认是后台线程。 创建的线程默认是前台线程【默认isbackground=false 前台线程不退出,应用程序的进程则一直存在,要杀死】
75. 线程并不是实时立即启动,也不是实时立即关闭的。
76.警惕线程的优先级
77. 正确停止线程
问题: 和启动线程一样,不是想停就立刻停的。得干完手头要紧的活,比如现在在执行非托管代码,引发异常得回到托管代码中。
线程停止主要取决于工作线程是否能主动访问调用者的停止请求。
标准的取消模式:协议式取消。
CancellationTokenSource cts=new CancellationTokenSource();
cts.Token.Register(fun()); //线程停止时的回调
cts.Cancel(); //发送Cancel信号 线程停止
socket 1000台客户端异步技术 只需几个线程就可以了(取决于心跳频率)
79. 使用TreadPool或BackgroundWorker代替Thread
80. Task代替ThreadPool
ThreadPool: 不支持线程的取消,完成,失败通知。不支持线程执行的先后顺序。
81. Parallel简化同步状态
82. 并行
第二部分 架构篇
第7章 成员设计
90.
<1 . 不要为抽象类提供公开的构造方法,抽象类设计只是为了继承,而不是用于生成实例对象
<2. 可见字段应该重构为属性,属性和字段的区别:一个是方法,一个是字段
<3. 谨慎把数组或者集合作为属性
因为数据和集合都是引用类型,通过list2来初始化list1, 所以程序员1对 list进行修改,程序员2对list2修改会直接导致list的变化。
<4. 构造方法应初始化主要属性和字段。【一个猫生下来就已经具备尾巴了】
<5. 区别对待override和new.[new 重写覆盖了父类方法,相当于该类中的一个新方法,和父类中的方法没有一点关系]
<6. 避免在构造方法中调用虚方法
<7. 成员优先考虑公开基类型或者接口
<8 . 用params减少重复参数
<9. 重写时不应该使用子类参数
建议100: 静态方法和实例方法没有区别
101. 使用扩展方法,向现有类型“添加”方法
第八章 类型设计
103. 区分组合和继承的应用场合
组合; 在新类A中声明 类B,C,D的实例。【有一个的概念】
107. 区分静态类和单例
静态类不是一个真正的对象,但是单例类时一个对象。
109. 谨慎使用嵌套类
当某一个类需要访问另一个类型的私有成员时,才实现为嵌套类
111. 避免双重耦合【常见的解耦就是提炼接口】
112. 把现实世界中的对象抽象为类【猫,狗】,将可复用对象圈起来就是命名空间【植物,动物】
第9章 安全性设计
考虑可能出现的最大值:定义加工资,最大值。checked{} 关键字i行核实,会主动抛出异常
114. MD5 不再安全 【穷举法破解】
115. HASH 检验文件是否被纂改
116. 避免非对称算法加密
117. ......
<1. Company.Component 命名空间命名
<2. 考虑命名空间使用复数,System.Books 不要System.AllBook
<3. 用名词和名词组给类型命名 推荐ScoreManager 不要SoreManage
<4. 考虑让派生类的名字以基类名字作为后缀
<5. 泛型类型参数要以T作为前缀
<6. 以复数命名枚举类型,以单数命名枚举元素【Week 不要Day】
<7. 用camelCasing命名私有字段和局部变量
<8. 常量以下划线的方式 TASK_STATE_CANCELED s_ 静态变量
<9. 考虑使用肯定性的短语命名bool属性 IsEnabled
<10.优先使用后缀作为一个类型的信版本,不到不得已并不推荐 Book1 Book2
<11. 委托和事件加上上级后缀 HttpDelegate()
<12. 事件处理器函数采用组合式命名: Button_SizeChanged()
代码整洁的要求之一,就是尽量减少代码。如省略默认的访问修饰符
<1. 使用表驱动法避免过长的if和switch分支
<2. 使用匿名方法,Lambda表达式代替方法 如果方法体小于3行
<3. 使用事件访问器替换公开的事件成员变量
get 属性:
在c#get属性中,不要进行赋值操作,或者其他操作,只用作返回数据使用。
因为:如果int b=Student.age; 如果在年龄get里进行了加减操作 ,那么当b分配了内存但是没有用到,有的程序员可能会删除
int b=Student.age; 那么在get里的操作也会失效。
bug继承关系注意点:
【不能从父类调用子类的方法,因为子类的细节比父类多,父类并不知道子类的细节。【游戏开发中需要注意这种问题】
【重写虚函数时的调用流程: 先构造父类,因为父类保存的基本的信息,再构造子类构造器。都构造完毕后,根据构造顺序先执行父类的方法,再执行子类的方法】
隐藏父类方法:关键字new
对修饰符的理解:
在继承中,子类会继承父类所有的成员,和父类成员的修饰符private等无关,修饰符只是决定该成员是否可以在本类/类外部/培生类中访问。
unity中父类有OnDestroy() 方法,在此方法中调用了父类的虚方法Destroy(),而子类重写了父类的Destroy方法。如下图:
解析:
此时删除了go物件,则执行UIBase.Destroy() 【因为子类也是继承了父类的Destroy的,执行子类的重写方法】,此时如果在子类中写了go.OnDestroy(),则需要使用new关键字来隐藏父类基因,使用新的基因。就不会走老的基因了。
协程的一些理解:
整理得到:通过设置MonoBehaviour脚本的enabled对协程是没有影响的,但如果 gameObject.SetActive(false) 则已经启动的协程则完全停止了,即使在Inspector把gameObject 激活还是没有继续执行。也就说协程虽然是在MonoBehvaviour启动的(StartCoroutine)但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受gameObject 控制,也应该是和MonoBehaviour脚本一样每帧“轮询” yield 的条件是否满足。
<1. 协程不受自身脚本的enable/disEable的影响;
<2. 虽然协程是在MonoBehaviour中开启的,但是协程和MonoBehaviour协程和脚本是同级的,都受gameObject的影响;
<3. 由2知,所以在inspecter面板里,把gameObject.active=false,即协程也会停止,但是再gameObject.active=true,协程不会开启;
https://www.cnblogs.com/dotnet261010/p/9034594.html
C#中List集合使用Exists方法判断是否存在符合条件的元素对象
https://blog.csdn.net/CAO11021/article/details/100213362
判断testList中是否存在t.index==7的对象,存在true,否则false.
字典的内部实现;
https://blog.csdn.net/zhaoguanghui2012/article/details/88105715