1.C#中[],List,Array,ArrayList的区别
[] 是针对特定类型、固定长度的。
List 是针对特定类型、任意长度的。
Array 是针对任意类型、固定长度的。
ArrayList 是针对任意类型、任意长度的。
Array 和 ArrayList 是通过存储 object 实现任意类型的,所以使用时要转换(BOX)。
2.C#不定参
例如:print("a,b,%dc", i, str)之类的,
其实原型是:print(string content, param object[] args)
param声明不定数量参数
3.C#的lambda表达式
Lambda 表达式是一种可用于创建委托或表达式目录树的匿名函数(摘自MSDN),分两种:
a.表达式Lambda:位于 => 运算符右侧的 Lambda 表达式称为“表达式 lambda”。 表达式 lambda 广泛用于表达式树的构造。 表达式 lambda 会返回表达式的结果
仅当 lambda 只有一个输入参数时,括号才是可选的;否则括号是必需的。 括号内的两个或更多输入参数使用逗号加以分隔
eg: (input-parameters) => expression
b.语句Lambda:语句 lambda 与表达式 lambda 表达式类似,只是语句括在大括号中,注意,语句Lambda不会返回结果
eg:(input-parameters) => { statement; }
注:关于匿名函数更深入的理解,参考25条
4.linq
linq就是结构化查询语言,只不过不同于sql,他不局限于关系数据库。
当然也引进了一些特征:如延迟执行,为了智能提示又改写了sql的习惯,入
sql是select * from table ,linq中是from table selec var。
eg:
var someInts = from a in Enumerable.Range(1, 5)
from b in Enumerable.Range(6, 5)
where a < 3 && b < 104 select new { a, b, sum = a + b };
注意:在遍历数组查询的时候,如果可以for,那就最好直接for,如果非要foreach或linq,则斟酌选取,因为两者性能都没有for高,
均会造成多余CG,linq的CG比较大,但稳定在一定数值,而foreach的CG在Linq的值上下波动。
5.lamda与linq混用,进行快速查询
lamda是个表达式,而linq是一种查询语言
Enumerable.Range(2, 5).Select(x => string.Format("_{0}", x)
简单来说这两者都是因为程序员的偷懒而出现的语法糖
6.通过反射调用函数
关于反射的更多研究,请看这里:http://www.cnblogs.com/jeason1997/p/5142840.html
using System; namespace Ref { class Program { static void Main(string[] args) { SayHello obj = new SayHello(); Type type = typeof(SayHello); type.GetMethod("Say").Invoke(obj, new object[0]); // 一般反射调用传入的函数名就是字符串 Console.ReadKey(true); } } class SayHello { public void Say() { Console.WriteLine("Hello World!"); } } }
注:反射调用性能较低,没有直接调用函数高。
7.C#扩展函数
扩展方法举例:
需要特别说明的:
1、扩展方法中的第一个参数"this string str"中的this不可以省略,表示的是类string的 扩展方法,也就是说一个string类型的变量后.是可以调出ToInt方法的。
2、扩展方法中一定要写在静态(static class)类,静态方法中(static method),public private均可。
/// <summary>
/// 扩展类
/// </summary>
public static class ExtensionMethod
{
/// <summary>
/// 将字符串转换成int类型,如果转换失败则返回默认值
/// </summary>
public static int ToInt(this string str, int defaultVal)
{
/* * author:wuxm * date :2014年10月11日9:50:47 * */
int i = 0;
if (!int.TryParse(str, out i))
i = defaultVal; return i;
}
}
调用:
需要特别说明的: 1、扩展方法调用的时候,要using你的扩展方法所在的namespace才行。
2、当扩展方法与原类中的方法冲突(即原类中本来有同名同参方法)的时候,优先执行原类中的方法
eg: "3333".ToInt(0);
8.泛型 与 继承
今天产生了一个误区,以为实现泛型类与继承一样,会将泛型、父类里的静态变量也拓展过来。。。
原因是自己将泛型的原理搞乱了。
首先说下类静态变量,就是诸如:
public class Normal { public static int a = 1; }
像这种,在父类里声明了一个类的静态变量,这个静态变量不仅共享与该类的所有对象,也共享于该类的子类的所有对象。
然后天真的以为,如果在泛型类里也声明了一个静态变量,那么实现该泛型的具体类也能共享这个变量。
我的想法是这样的:
public Generics<T>
{
public static int a = 1;
}
Generics<string>.a = 100;
则: Generics<int>.a == 100;
之所以会产生这种误解,是因为我把泛型当成一个类了,其实他根本就是一个模板而已,class Generics<T>仅仅是一个模板,并不是一个“类”(暂时不管这种说法
对不,反正经过我的测试大概理解方向是对的),也就是说,它里面的静态变量a,仅仅是个摆布,并不会为该模板真实创建这样一个变量,只有待到该模板被具体化的
时候,它才有意义,它才是真正一个类。
例如:Generics<int>是一个类,Generics<string>是另一种类,但Generics<T>只是一个模板,没有意义。
所以Generics<int>的所有对象以及子类共享静态变量a,Generics<string>也一样,两者是两个不同的类,他们的a是两个不同的变量。
只能说他们是同样的模板,不能说他们是同样的类型。
9.C# 与.NET的关系
.net是一套标准,一个平台,包含一整套基础库,相当于一整套运行库的集合。在它之上运行的程序都被编译为IL中间语言。
C#是在这个平台上的一种开发语言,IL语言是先发明的,然后才发明C#、VB等.net语言。
C#编好的程序,要编译成IL文件才能运行。这是一种托管代码,只能运行在.NET虚拟机(CLR)之上。
精确的说,一个.NET应用是一个使用.NET Framework类库(或其他类库,例如Mono framework)来编写,并运行于公共语言运行时Common Language Runtime之上的应用程序。
如果一个应用程序跟.NET Framework或mono无关,它就不能叫做.NET程序
.Net 支持众多的编程语言,所有的编程语言编写的代码都将最终交给 CLR 来执行,因此 .Net 是“语言无关”的。目前微软推出的用于 .Net 开发的语言有:VB.NET J# C# F#,现在的 VC++ 也支持托管 .Net 编程。
因此.net可以多语言混合编程(例如U3D就同时支持C#跟UnityJS同时存在),反正最后都会编译成IL,任何语言编译后的IL都是一样的。
C#与.NET的关系是交集而不是包含,.NET可以用其他语言开发,C#也可以开发其他程序
10.Mono与.Net Framework、.Net Core之间的关系
.NET是一套标准,Mono.framework 与.net framework都是它的具体实现,上面已经解释了.NET framework了。.NET framework只能运行与微软系统,Mono是其他组织开发出来的跨平台框架,与.NET framework类似,
不过可以运行于其他OS,支持.NET的高级语言(C#,VB)等开发程序后要编译成平台无关的中间语言IL(.net framework与mono的都一样),IL再通过对应的的CLR(.NET framework或Mono的CLR)动态JIT解释成对应的机器码执行。
到目前为止(2015.2.17),微软在Windows平台上的.NET Framework的实现最为完整,但是.NET Framework和windows操作系统有很深的绑定,难以跨平台。Xamarin主导的Mono项目在.NET 的基础类库实现上有一些不够完美。
随着2014年 Xamarin和微软发起.NET基金会,微软在2014年11月份 开放.NET框架源代码。在.NET开源基金会的统一规划下诞生了.NET Core 。也就是说.NET Core Framework是参考.NET Framework重新开发的.NET实现,
Mono是.NET Framework的一个开源的、跨平台的实现。.Net Core将会是未来的趋势,用来取代.Net Frameworkd跟Mono。
11.C#中的Attribute
using System; using UnityEngine; /* * Attribute的概念:我们简单的总结为,定制特性attribute,本质上是一个类,其为目标元素(类、函数等)提供关联附加信息, * 并在运行期以反射的方式来获取附加信息。类似但又不同于字段跟属性。 * Attribute类是在编译的时候被实例化的,所以你还可以用外部工具维护这些Attribute信息。 */ // AttributeUsage用来注释这个类是一个特性类 // 它有三个参数,第一个参数AttributeTargets指明它为哪种元素注释 // 类名后一般加“Attribute”并且继承于Attribute [AttributeUsage(AttributeTargets.Class)] public class VersionAttribute : Attribute { public string Name { get; set; } public string Data { get; set; } public string Describtion { get; set; } } // 使用自定义特性“VersionAttribute”来注释我们的MyCode类 // 由于VersionAttribute的后半部分是Attribute,在用作注释的时候可以省略 [Version(Name = "Jeason", Data = "2016-6-29", Describtion = "Jeason's Attribute Class.")] public class MyCode { // Code } public class Test : MonoBehaviour { // Use this for initialization void Start() { // 由于Attribute在编译的时候就被实例化,因此可以不必实例化对象,直接通过反射的方式访问特性 Type info = typeof(MyCode); VersionAttribute versionAttribute = (VersionAttribute)Attribute.GetCustomAttribute(info, typeof(VersionAttribute)); Debug.Log(versionAttribute.Name); Debug.Log(versionAttribute.Data); Debug.Log(versionAttribute.Describtion); } }
12. C# 性能优化——三种字符串拼接效率
字符串拼接主要包括三类:+,String.Format(),StringBuilder.Append()
1)对于少量固定的字符串拼接,如string s= "a" + "b" + "c",系统会优化成s= String.Concat("a","b","c"),不会新建多个字符串。
如果写成string s="a"; s +="b"; s+="c";则会创建三个新的字符串。
2)String.Format的源代码:
public static String Format(
IFormatProvider provider, String format, params Object[] args) {
if (format == null || args == null)
throw new ArgumentNullException((format==null)?"format":"args");
StringBuilder sb = new StringBuilder(format.Length + args.Length * 8);
sb.AppendFormat(provider,format,args);
return sb.ToString();
}
可见,它和StringBuilder有着相似的效率,比用“+”的拼接方式高效,并且代码易于阅读。
string s= String.Format("{0}{1}{2}","a","b","c");
3)StringBuilder可以指定内存空间的容量,但可能需要进行数据类型转化。字符串较少时,可以使用String.Format()代替。
4)少量的字符串操作时,可以使用“+”或者String.Format();大量的字符串操作时,比如在循环体内,必须使用StringBuilder.Append()。
13.C#关键字 partial 局部类
局部类型允许我们将一个类、结构或接口分成几个部分,分别实现在几个不同的.cs文件中。
局部类型的特性:
(1) 局部类类似于预编译处理,在编译后,该类的各个部分仍然会被合并成一个类
(2) 局部类具有累加性,分类上的属性、集成基类、接口会被合并
(3) 局部类型只适用于类、接口、结构,而且各个分类必须位于同个命名空间。
详情参考:http://blog.csdn.net/niemeiquan/article/details/7801803
14.C#关键字 sealed
1.修饰类:当对一个类应用 sealed 修饰符时,此修饰符会阻止其他类从该类继承。类似于Java中final关键字。
2.修饰函数或属性:将某个基类定义的虚函数在override后标记为sealed,则该类的子类将无法再修改该函数
例如:
public class A { protected virtual void M() { Console.WriteLine("A.M()"); } } public class B : A { protected sealed override void M() { Console.WriteLine("B.M()");} } public sealed class C : B { // 错误,C无法修改M protected override void M() { Console.WriteLine("C.M()"); } }
15.C#中的内存
参考:
要点:
- 值类型:int,bool,char,pointer,enum,struct等派生于System.ValueType的,值类型不能为null
- 引用类型:class,string,object等派生于System.Object的(引用对象可以理解为两部分,他们的实际值部分以及指向他们的“引用(指针)”),引用类型可以为null
- 托管堆:存放引用类型对象的实际对象(会被GC清掉)
- 调用栈:存放调用函数的返回地址,函数的传入参数,局部变量(值类型,以及引用类型的引用地址,函数返回后会被清掉)
- 计算栈:存放当前语句执行的计算参数
- 代码区:存放IL指令(以及全局变量,常量),运行到某条指令就把它JIT后压入调用栈
注:引用变量只存在堆里,栈里存放的一定是值类型,但值类型不一定放在栈里,值类型只分配在调用的地方,值类型可以通过box放到堆里。
比如一个类里有一个int成员变量,那么这个变量便是随着这个class的对象一起被分配在堆里的,而在这个类的某个函数里声明的另一个int局部变量,一般情况下就是分配在栈里了
问题:类里的静态(全局)变量,常量,只读变量分配在哪里?(推荐用ILSPY反编译看看一个函数的IL码)
例子:
class object { int c; } void fun(int a) // 开始调用函数后,fun()压入调用栈,同时压入a { int b = 1; // 声明局部b,压入调用栈 a = b + 2; // b,2 压入计算栈,调用add后赋给a object o = new object(); // 在堆上分配o,同时将o的引用压入调用栈 o.c = a; // 将a的值赋给堆上的o里的c }
16.元数据(MetaData)与反射的关系
我们的.NET程序,在编译后生成的dll有两个主要的部分:IL代码(包括一些静态数据啥的)与MetaData表(占50%左右)
那么MD表占这么大的空间是存了些什么东西呢?DnSpy反编译后可以看到MD表储存的数据大概如下:
* 程序集的说明。 o 标识(名称、版本、区域性、公钥)。 o 导出的类型。 o 该程序集所依赖的其他程序集。 o 运行所需的安全权限。 * 类型的说明。 o 名称、可见性、基类和实现的接口。 o 成员(方法、字段、属性、事件、嵌套的类型)。 * 属性。 o 修饰类型和成员的其他说明性元素。
当执行代码时,运行库将元数据加载到内存中,并引用它来发现有关代码的类、成员、继承等信息。
这些数据在我们使用反射功能的时候讲非常有用,比如:
Type t = typeof("TestClass"); MemberInfo[] mis = t.GetMembers();
反射时我们之所以能通过字符串就获取到想要的类,是因为这时候会从MD里查找与我们的字符串所匹配的类,当然,字符串的搜索是很耗时的,而且搜到类后,
还要坚持参数以及其他问题,所以反射是很耗性能的。
MD表还要一个很重要的作用,就是给我们的IDE(VS)提供强大的智能代码提示功能,比如你在某个类上按F12,将会调到定义处,并提示你数据来自元数据。
元数据的存在形式:
- 直接在dll的头部
- 像IOS这种平台,不允许JIT的存在,只有AOT或着IL2CPP,因此没有所谓的dll的概念,那在IOS上如何获取元数据呢?(以Unity程序为例子):
17.C#中打印出当前堆栈
有时候我们想Hack某个游戏,并获取他的执行过程,或者我们想在我们的程序运行过程中,若崩溃了,则将堆栈打印出来,string info = null;
// 设置为true,这样才能捕获到文件路径名和当前行数,当前行数为GetFrames代码的函数,也可以设置其他参数
// 只有程序的exe路径下也存在pdf调试文件时,才能获取到文件名路径名
StackTrace st = new StackTrace(true); //得到当前的所以堆栈 StackFrame[] sf = st.GetFrames(); for (int i = 0; i < sf.Length; ++i) { info = info + " " + " FileName=" + sf[i].GetFileName() + " fullname=" + sf[i].GetMethod().DeclaringType.FullName + " function=" + sf[i].GetMethod().Name + " FileLineNumber=" + sf[i].GetFileLineNumber(); }
18.C#中的指针
参考:C#中指针使用总结
-
C#为了类型安全,默认并不支持指针。但是也并不是说C#不支持指针,我们可以使用unsafe关键词,开启不安全代码(unsafe code)开发模式。在不安全模式下,我们可以直接操作内存,这样就可以使用指针了。在不安全模式下,CLR并不检测unsafe代码的安全,而是直接执行代码。unsafe代码的安全需要开发人员自行检测。
-
C#的指针类型支持:sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool,struct(结构体),结构体中只能包括非托管类型。
- C#默认情况下,分配的内存会在方法结束后自动释放,如果我们想控制类的对象呢?因为类是托管类型,我们知道类受到“垃圾收集”的影响,它的内存地址是不固定的。而且类是引用类型,是不能声明为指针类型的。而指针分配内存后,不受“垃圾收集”影响,地址是固定的。所以为了使用类中的数据,我们需要临时固定类的地址。这就用到fixed关键词,用fixed后,就可以操作类中的值类型了。
- fixed的使用可能产生存储碎片,因为它们不能移动。如果确实需要固定对象,固定对象的时间应该越短越好。
19.AppDomain
参考:
- AppDomain是CLR的运行单元,它可以加载Assembly、创建对象以及执行程序。
- 每一个AppDomain可以单独运行、停止;每个AppDomain有自己默认的异常处理;一个AppDomain的运行失败不会影响到其他的AppDomain。
- AppDomain被创建在进程中,一个进程内可以有多个AppDomain。一个AppDomain只能属于一个进程。
- Assembly是.Net程序的基本部署单元,它可以为CLR提供用于识别类型的元数据等等。Assembly不能单独执行,它必须被加载到AppDomain中,然后由AppDomain创建程序集中的对象。
- 一个Assembly可以被多个AppDomain加载,一个AppDomain可以加载多个Assembly。
-
每个AppDomain引用到某个类型的时候需要把相应的assembly在各自的AppDomain中初始化。因此,每个AppDomain会单独保持一个类的静态变量。
- 任何对象只能属于一个AppDomain。AppDomain用来隔离对象
20. .NET的内存回收机制(代式回收)
参考:C#基础之垃圾回收
在C#中,垃圾回收,是由CLR自动处理的。当我们新建一个对象的时候:Person P = new Person;CLR会进行一下步骤进行处理:
1.根据该对象内部的数据(数据成员,基类等)来计算出需要分配的内存空间。
2.检查托管堆中是否有足够的空间来存放这一个对象实例,如果有足够空间的话,就把它存放带托管堆当前指针所指的可存放位置中,然后调用存放类的构造函数,最后将内存中新对象的引用返回给调用者,就是上面的P。
3.在调用者引用对象之前,移动下一个对象的指针,把这个指针指向下一个可存放的位置这样形成了一个循环。
那么如果在检查托管堆的时候发现内存不足的时候该怎么做呢?很显然,就是要把托管堆中不需要的对象删去,以腾出可分配空间来储存对象。
首先看一下怎么把一个对象设置成不需要的,很简单:p=null;这样就会把P对象所指向的那个储存位置标记为不使用,这里仅仅是标记,
而不会立刻去把那块区域清理。区域清理有它自己的清理机制,下面最后说一下垃圾回收机制中的清理机制:
好了,堆上放置了很多东西,我们想象一下每一天都会有垃圾清理车来查看什么垃圾需要清理,如果查到了,就直接放上车运走。下面就是垃圾清理车需要执行的工作,这里就是垃圾回收机制了。
首先,介绍一下代的概念,在托管堆中的对象,有一个变量专门储存改对象属于哪一代,
在托管堆中只有三个代,分别是第0代,第1代,第2代。最高是第2代,这样的设计的好处就是,在进行垃圾处理的时候,会有选择的查询:
1.最初会查询第0代中所有的对象,看有没有需要删除清理的,把所有需要删除清理的放上垃圾车,然后把剩余的对象升级为第1代
2.如果有一次查看第0代中的所有对象进行删除处理,但是还不能腾出足够的空间,那么垃圾车就会查询第1代中的所有对象,看有没有需要被清除的,如果有就放上垃圾车,其他没有被处理的对象全部升级为第2代
3.如果以后检查第2代以后,没有被删除的对象还是第2代,因为这是最高的了。
由于Unity目前用的mono2.6,而mono2.6的GC机制只是用了一个简易的C++回收机制Boehm-Demers-Weiser,并不是代式回收,内存一旦上涨,是不会再下降的,但它内部仍然有在进行GC控制,
也就是说,比如我加载一个100m的文件,然后释放这个文件,但是总内存占用还是100m,并不会下降,只不过这部分区域可以重新利用,我下次再加载一个50m的文件时,就不会再向系统申请50m的空间了,
而是在这已经开辟的100m里用。所以,写Unity游戏时,要尽量避免加载大文件,尽量分割成小块流。
同时该垃圾回收期还有一个特点,就是"stop-the-world",也就是说它工作的时候,会暂停运行程序,直到它工作完才恢复,这就导致了垃圾回收的时候机器会掉帧。
C# MEMORY AND PERFORMANCE TIPS FOR UNITY
IL2CPP Internals – Garbage collector integration
在Unity将mono替换成.net core之前,只能忍受它这个GC机制了。
好消息是,Unity终于宣布在2019版本改进垃圾回收机,但依旧不是代式回收,而是Boehm的改版,
不过比起之前的回收机器,这次改为“增量式”,也就是将GC由"stop-the-world"改为一个过程,避免由于GC造成的机器突然卡顿。
Feature Preview: Incremental Garbage Collection
21.C#实现C/C++中的Union
C#不像C++,他本身是没有联合Union的,但是可以通过在结构体上添加StructLayout属性来手动控制结构体每个元素的位置来实现结构体是由若干成员组成的,布局有两种:
1.Sequential,顺序布局,比如
[StructLayout(LayoutKind.Sequential)]
struct S1 { int a; int b; }
那么默认情况下在内存里是先排a,再排b
也就是如果能取到a的地址,那么b的地址则和a相差一个int类型的长度4字节,该属性为默认属性,可以不添加
2.Explicit,精确布局
需要用FieldOffset()设置每个成员的位置
这样就可以实现类似c的Union的功能
[StructLayout(LayoutKind.Explicit)] struct S1 { [FieldOffset(0)] int a; [FieldOffset(0)] int b; }
这样a和b在内存中地址相同,都是在该struct开始的内存偏移0的位置,也就是说,如果先对a赋值,再对b赋值,那么b会覆盖掉a
更多的细节参考:C#联合Union的实现方式
22.class和struct的区别
class和struct最本质的区别是class是引用类型,而struct是值类型,它们在内存中的分配情况有所区别。
什么是class?
class(类)是面向对象编程的基本概念,是一种自定义数据结构类型,通常包含字段、属性、方法、构造函数、索引器、操作符等。在.NET中,所有的类都最终继承自System.Object类,因此是一种引用类型,也就是说,new一个类的实例时,在堆栈(stack)上存放该实例在托管堆(managed heap)中的地址,而实例的值保存在托管堆(managed heap)中。
什么是struct?
struct(结构)是一种值类型,用于将一组相关的变量组织为一个单一的变量实体 。所有的结构都继承自System.ValueType类,因此是一种值类型,也就是说,struct实例在创建时分配在线程的堆栈(stack)上,它本身存储了值。所以在使用struct时,我们可以将其当作int、char这样的基本类型类对待。
-
既然class是引用类型,class可以设为null。但是我们不能将struct设为null,因为它是值类型。
-
当你实例化一个class,它将创建在堆上。而你实例化一个struct,它将创建在栈上 (参考15点,这里表达不太正确,struct,也不一定创建在栈上,看是谁创建的它)
- 值类型默认分配在栈上,但可以通过装箱操作将值类型数据复制到堆上;引用类型仅能被CLR分配到堆中,但引用的地址保存在栈上。
-
你使用的是一个对class实例的引用。而你使用的不是对一个struct的引用。(而是直接使用它们)
-
structs 不可以有初始化器,class可以有初始化器。由此更可以看出,struct是单纯的数据结构,而class是行为
-
public struct structA { //public int A = 90; //错误:“structA.A”: 结构中不能有实例字段初始值 public int A;
private int B;
public structA(int a)
{
A = a;
B = a;
}
public structA() // 错误:struct不能包含显式的无参构造函数
{
A = 1; // 错误2:struct构造器里必须初始化所有字段
} } public class classA { public int A = 90; }Classes 可以有显式无参数构造器,但是Struct不可以,Struct不能包含显式的无参构造函数,而且构造器里需要初始化所有字段,不然就别写构造器
-
Class支持继承和多态,Struct不支持. 注意:但是Struct 可以和类一样实现接口,既然Struct不支持继承,其成员不能以protected 或Protected Internal 修饰
-
类的实例只能通过new SomeClass()来创建,struct类型的实例既可以通过new SomeStruct()来创建,也可以通过SomeStruct myStruct;来创建
-
通过SomeStruct myStruct;来创建一个struct的时候,必须手动全部初始化全部字段,因为该步骤没有调用initobj将所有字段置0,若不全部手动赋值,会导致部分字段的值为不确定状态
-
在IL层,new一个class调用的是‘newobj’操作,new一个struct调用的是'initobj‘操作,也可也不initobj,两者区别参考38点
-
由于struct是值类型,所以在复制的时候,它是深拷贝,即 struct2 = struct1,是两个内容一样,但在内存完全独立的对象,而引用对象的赋值,只是浅拷贝。参考33点。
-
由于值类型是分配在栈上的,当一个函数返回退栈时,它自然会被清除,不必等GC。而引用类型是在堆上分配, 栈上保存着一个地址而已, 当栈释放后, 即使对象已经没有用了, 但堆上分配的内存还在,只能等GC收集时才能真正释放。
适用场合:Struct有性能优势,Class有面向对象的扩展优势。
用于底层数据存储的类型设计为Struct类型,将用于定义应用程序行为的类型设计为Class。如果对类型将来的应用情况不能确定,应该使用Class。
23.递归和队列
递归和队列,同样可以用来实现广度搜索,例如迷宫,要找出所有的出路,注意是所有的出路,或者三消游戏,要找出所有连续的且数量大于3的糖果
- 递归的优点
递归代码写起来比较方便简洁,结构层次清晰,可读性比较好。 - 递归的缺点
递归需要调用函数,递归需要系统堆栈,递归空间和时间消耗都比较大,并且如果递归太深,会发生 堆栈溢出,系统会奔溃。 - 如何解决递归太深的问题?
当使用递归遍历的目录可能存在递归太深的时候,我们可以选择用队列来优化递归。
24.装箱和拆箱(boxing and unboxing)
简单来说:
装箱是将值类型转换为引用类型 ;拆箱是将引用类型转换为值类型。
利用装箱和拆箱功能,可通过允许值类型的任何值与Object 类型的值相互转换,将值类型与引用类型链接起来 。
boxing:将值类型转为引用,例如 int 转 object,这时候,会从stack里拷贝一份int到heap上,然后再在stack上添加一个heap上的引用
unboxing:上面反过来,从stack上的地址找到heap上的实际对象,然后拷贝到stack上
由此可以看出,boxing与unboxing会造成额外的计算开销,boxing还会造成heap上的多余分配,造成GC问题
参考:http://blog.sina.com.cn/s/blog_5eb2e54e0100i8d3.html
25.CLR 中匿名函数的实现原理浅析(闭包)
匿名函数有两种语法风格:Lambda表达式和匿名方法表达式。在几乎所有的情况下,Lambda表达式都比匿名方法表达式更为简介具有表现力。但现在C#语言中仍保留了后者,为了向后兼容。
Lambda表达式:
async可选 (匿名的函数签名)=> (匿名的函数体)
匿名方法表达式:
async可选 delegate (显式的匿名函数签名) 可选{代码块}
其中匿名的函数签名可以包括两种,一种是隐式的匿名函数签名另一种是显式的匿名函数签名:
隐式的函数签名:(p)、(p1,p1)
显式的函数签名:(int p)、(int p1,int p2)、(ref int p1,out int p2)
匿名的函数体可以是表达式或者代码块。
- 原理:C#编译器自动将匿名函数代码转移到一个自动命名函数中,将原来需要用户手工完成的工作自动完成。 也就是说,编译后,它仍然是普通函数。
- 作用域:匿名函数使用到的父函数中局部变量,无论是引用类型还是值类型,都必须从栈变量转换为堆变量(即转换为类的成员变量),以便在其作用域外的匿名函数实现代码可以访问并控制生命周期。因为栈变量的生命周期与其所有者函数是一致的,所有者函数退出后,其堆栈自动恢复到调用函数前,也就无法完成变量生命周期与函数调用生命周期的解耦。
- 多个作用域:这时候编译器可能会生成多个类,为每个作用域的变量单独分配一个类来控制,总结其规律就是每个不同的局部变量作用域会有一个单独的类进行封装,子作用域中如果使用到父作用域的局部变量,则子作用域的封装类引用父作用域的封装类。相同作用域的变量和匿名方法由封装类绑定到一起,维护其一致的生命周期。
//比如有段代码如下: delegate void Delegate1(); public void Method1() { int i=0; Delegate1 d1 = delegate() { i++; }; d1(); } //编译后变成: delegate void Delegate1(); private sealed class __LocalsDisplayClass$00000002 { public int i; //局部变量i变成类的成员函数 public void __AnonymousMethod$00000001() { this.i++; } }; public void Method1() { __LocalsDisplayClass$00000002 local1 = new __LocalsDisplayClass$00000002(); local1.i = 0; Delegate1 d1 = new Delegate1(local1.__AnonymousMethod$00000001); d1(); }
26.委托
一个委托是一个指向一个方法的引用,或者说,一个委托的实例就是一个指向某个方法的对象,这是一个简单却十分强大的概念。
C#中的委托是用来处理在其他语言中(如C++、Pascal等)需要用函数指针来处理的情况。不过与C++不同的是:委托是完全面向对象的;C++指针仅仅指向成员函数,而委托同时封装了对象的实例和方法;委托是完全类型安全的,只有当函数的签名与委托的签名匹配的时候,委托才可以指向该方法,当委托没有合法的指向方法的时候不能被调用。
27.静态类 static class
静态类基本上与非静态类相同,但存在一个差异:静态类无法实例化。 换句话说,无法使用 new 关键字创建类类型的变量。
类可以声明为 static 的,以指示它仅包含静态成员。不能使用 new 关键字创建静态类的实例。例如,创建一组不操作实例数据并且不与代码中的特定对象关联的方法是很常见的要求。您应该使用静态类来包含那些方法。
静态类的主要功能如下:
-
它们仅包含静态成员。----函数成员和变量都必须有static修饰
-
它们不能被实例化。
-
它们是密封的。-----------编译器编译时自动生成sealed标记
-
它们不能包含实例构造函数。
因此创建静态类与创建仅包含静态成员和私有构造函数的类大致一样。私有构造函数阻止类被实例化。
使用静态类的优点在于,编译器能够执行检查以确保不致偶然地添加实例成员。编译器将保证不会创建此类的实利。
静态类是密封的,因此不可被继承。静态类不能包含构造函数,但仍可声明静态构造函数以分配初始值或设置某个静态状态。
28.const 与 readonly 的区别
C#语言中两种不同的常量类型:
- 静态常量(compile-time constants): const (性能好)
- 动态常量(runtime constants): readonly(灵活性好)
1)const修饰的常量在声明的时候必须初始化;readonly修饰的常量则可以延迟到构造函数初始化
2)const修饰的常量在编译期间就被解析,即常量值被替换成初始化的值;readonly修饰的常量则延迟到运行的时候
3)此外const常量既可以声明在类中也可以在函数体内,但是static readonly常量只能声明在类中。
4)静态常量只能被声明为简单的数据类型(int以及浮点型)、枚举、布尔或者字符串型,而动态常量则除了这些类型,还可以修饰一些对象类型。
class P { static readonly int A=B*10; static readonly int B=10; const int C=D*10; const int D=10; public static void Main(string[] args) { print(a, b, c, d); } } //输出结果是A is 0,B is 10 //C is 100,D is 10
const是静态常量,所以在编译的时候就将C与D的值确定下来了(即D变量时10,而C=D*10=10*10=100),那么Main函数中的输出当然是C is 100,D is 10啦。
而static readonly则是动态常量,变量的值在编译期间不予以解析,所以开始都是默认值,像A与B都是int类型,故都是0。而在程序执行到A=B*10;所以A=0*10=0,程序接着执行到B=10这句时候,才会真正的B的初值10赋给B。
给定参数化类型 T 的一个变量 t,只有当 T 为引用类型时,语句 t = null 才有效;只有当 T 为数值类型而不是结构时,语句 t = 0 才能正常使用。解决方案是使用 default 关键字,此关键字对于引用类型会返回空,对于数值类型会返回零。对于结构,此关键字将返回初始化为零或空的每个结构成员,具体取决于这些结构是值类型还是引用类型。
例如:
T t = default(T);
当T为int时,t为0
当T为string时,t为null
值类型:
对于值类型来说,两者效果一样,都是比较内容是否相同,"1==1"与"1.Equals(1)"完全一致
引用类型:
对于引用类型,==比较的是引用的地址,Equals比较的是引用的内容,比如两个引用变量,他们的地址肯定不同,但内容,则有可能相同
注意:
- c#中==操作符是可以重载的,例如string类就将它重载,并返回Equals,也就是说,string这个类,==跟Equals是完全一致的
- Equals是Object中的一个虚方法,如果子类没重写,那么调用的仍是父类中的Equals方法。但是父类是无法知道你都有哪些成员字段的,因此返回的是false。要想让他能够比较两个变量的内容是否相同,那就应该重写Equals方法
31.HashTable、HashSet和Dictionary的区别
占坑
32.C#委托Action、Action<T>、Func<T>、Predicate<T>
- 如果要委托的方法没有参数也没有返回值就想到Action
- 有参数但没有返回值就想到Action<T>
- 无参数有返回值、有参数且有返回值就想到Func<T>
- 有bool类型的返回值,多用在比较器的方法,要委托这个方法就想到用Predicate<T>
33.C#中浅拷贝与深拷贝
值类型变量,copy是属于全盘复制;
引用类型变量,一般的copy只是浅copy,相当于只传递一个引用指针一样。
因此 对于引用类型进行真正copy的时候,也是最费事的,具体的说,必须为其实现ICloneable接口中提供的Clone方法
34.静态构造函数
静态构造函数用于初始化任何静态数据,或执行仅需执行一次的特定操作。 将在创建第一个实例或引用任何静态成员之前自动调用静态构造函数。
35.C#中new的三种用法
2)new 修饰符:在用作修饰符时,new 关键字可以显式隐藏从基类继承的成员。
3)new 约束:用于在泛型声明中约束可能用作类型参数的参数的类型。
36.闭包
概念:内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。但该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
反编译后可以看出,类里的成员变量的初始化,编译后,是合并到ctor构造函数里去的,同样的,静态变量,会由一个静态的构造函数,在里面初始化这些变量
所以一个类的构造函数实际上步骤是:
1.初始化所有的成员变量
2.调用System.Object::.ctor()创建一个实例
3.调用开发者之前写在构造函数里的内容
初始化的时候:
- int,float等基础类型,直接ldc.i4.1 之类的压入数值到栈中
- struct类型,如果该struct有new,则调用initobj初始化它,否则,由于没有将所有字段置0,可能会导致某些变量数值不确定,所有需要全部手动stfld给stuct的每个字段赋值
- class类型的初始化是调用newobj
OpCodes.Initobj Field :将位于指定地址的值类型的每个字段初始化为空引用或适当的基元类型的 0。
OpCodes.Newobj Field :创建一个值类型的新对象或新实例,并将对象引用(O
类型)推送到计算堆栈上。
当new一个class的时候,IL层调用的是newobj,不需要初始化(因为拿过来的内存是GC过的,GC的时候会顺便清空)
当new一个struct的时候,调用的是iniobj,将所有字段初始化。
39.父类转子类
可以理解为如果未定义转换操作符的话,子类父类的转换,实际指向的都是同一块内存父类:
Person:
{
name,
age
}
子类
Studuen
{
grade,
}
那么 var p = new Person()的内存空间是 name,age,
将p转为studene是肯定失败的,因为内存空间不存在grade。
则其内存空间是name,age,grade,
虽然当前是Person,但多处的grade并不会被引用到,相当于是隐藏的
这时Studen s = (studne)p,才能正常转换
- 父类不能直接强制转换成子类(除非它原本指向的就是子类的引用)
- 子类可以强制转换成父类,但是在父类中只能取父类的字段与方法
但在代码里,Child child = (Child)father,并不报编译性错误,因为这种情况有可能正常也有可能失败
试一下:
private void OnChangeEquip(EquipType type, IEntity equipEntity, bool equip)
{
var weapon = (WeaponEntity)equipEntity;
吃不吃性能,按照我的想法,应该是不吃的,因为传进来的IEntity,本来在内存上就是一个WeaponEntity,转换应该没有多少开销
1.因为不算拆箱吧?
2.看以下该对象的内存地址是不是还是一样的(看看怎么查对象的内存地址)
- 返回值可以不明显指定ValueTuple,使用新语法(,,)代替,如(string, int, uint)
- ValueTuple是Struct,是值类型,而Tuple是Class,引用类型
- 返回值可以指定元素名字,方便理解记忆赋值和访问
- 可以通过var (x, y)或者(var x, var y)来解析值元组元素构造局部变量,同时可以使用符号”_”来忽略不需要的元素
static (string name, int age, uint height) GetStudentInfo1(string name) { return ("Bob", 28, 175); } static void RunTest1() { var (name, age, height) = GetStudentInfo1("Bob"); Console.WriteLine($"Student Information: Name [{name}], Age [{age}], Height [{height}]"); (var name1, var age1, var height1) = GetStudentInfo1("Bob"); Console.WriteLine($"Student Information: Name [{name1}], Age [{age1}], Height [{height1}]"); var (_, age2, _) = GetStudentInfo1("Bob"); Console.WriteLine($"Student Information: Age [{age2}]"); }
41.this的四种用法
- 表示当前类的实例对象
- 串联构造函数,例如:this()表示无参构造函数,执行顺序是 this() / Test() -> Test(string text)
public Test(string text) : this()
- 作为修饰符扩展原始类方法
public static string ToJson(this object obj)
- 当作类的索引器 C#中的索引器原理
42.使用Async-Await替代 coroutines