是一个多变的时代,一次又一次的浪潮将不同的人推上了巅峰。新的人想搭上这一波,同时老的人也不想死在沙滩上。这些年新的浪潮又一次推开,历史不停地重复上演,那便是移动互联网。它的兴起无人抗拒,而在这一波浪潮中移动互联网游戏更是重中之重。
在这个混沌时期,手机游戏以几倍的速度经历着客户端游戏曾经走过的道路,从轻度到重度,从弱交互到强交互,从简单到复杂。来到2014,我们正处于一个中间时期,由于门槛低,渠道单一,手机游戏依然是渠道为王。当然这并不是这篇随笔的重点。
同时手机游戏由于目前的环境,需要更快的速度推上市场,这便带来了快速开发的需求。而unity3d搭上了这一波,同时也将.net更深入地推入游戏开发人员的手中。这便是这篇随笔的由来。之前对.net有一定了解,但没有系统学习过。最近读了Essential C#这本书,作为新手学习的话,个人十分推荐,中文版应该叫C#本质论。后面记述的便是读后的一些笔记,若有理解上的偏差,还望指正。
1 System.Environment.NewLine.
跨平台换行符,由于平台差异,windows换行使用rn,而Unix上使用n。所以当需要保持输出的跨平台性,可以使用Console.WriteLine,或者在字符串后添加System.Environment.NewLine
2 String
和AS,以及很多脚本语言一样,C#中的String也是不可改变的。相同字符串在内存中只会有一份内存,而不同的字符串变量都引用该内存。修改String会生成一份新的内存来保存新的字符串,而不会修改其本身。如果由于构建一个字符串需要很多步骤,因此修改之前创建的字符串是不可避免的。这时可以使用System.Text.StringBuilder,它提供了如String类似的函数,区别在于System.Text.StringBuilder的函数是修改其本身的内存,而不是新生成一个。
3 @逐字解释字符串
在字符输出时,可以使用@来直接输出@后面的字符串。@后面除了"的转移字符起作用外,其他都不会有作用。简单来说就是输出即见即所得。
4 nullable修饰符
一般来说,C#中的值类型是不能赋值为null的,但在一些特殊情况下,如果输入是一个魔数,比如说数据库中某一列允许null,那么当这个数据输入到C#中,就会产生问题。所以我们在这种情况下可以使用nullable修饰符来使得值类型接受null。例如int? account = null
5 数组以及多维数组
数组声明Type[] varName。多维数组通过在[]添加“,”来声明。比如Type[,] varName是声明一个二维数组。数组的维度数等于","的数量+1。数组的Length是不会改变的。它的Clear函数并不会减少数组的Length大小,只是将数组的每个元素的值设置成默认值。
6 ??操作符号
在C#2.0中加入判断null的快捷操作符,很方便。expression1??expression2。如果1不为null,表达式的值为1,反之,为2。比如说name = filename??"default.txt"。这个用来做空的保护太方便了。
7 nowarn:<warn list>
除了使用#pragma指令来关闭某条warning外,比如说#pragma warning disable 1030。还可以在编译器命令行中使用nowarn:<warn list>中关闭warning。与前者不同的是前者只影响单个cs文件,后者影响整个编译过程。例如:csc /nowarn:1591
8 using与别名
using除了可以引用命名空间,还可以设置别名。比如using StringFormat = System.String;
9 params函数变参
C#中的函数变参通过params关键字和数组来声明。传入函数的每一个参数分别为数组的每一个元素。比如:static string combine(params string[] paths)。我们可以foreach(string s in paths)。当不传入参数时,数组大小为0.
10 对象初始化列表与集合初始化列表
在C#3.0中加入了对象初始化列表,可以在构造函数之后,加入列表,对对象的可见域和属性进行赋值。这个也是吸入像js这种动态语言的特性,方便书写代码。在IL中,它被翻译为构造函数调用后,后面跟着列表中每个属性的赋值操作,且顺序与列表中的顺序一致。比如:A a = new A(){Title = "ABC", Sa = 1}; 而集合初始化列表与其类似,不同在于它用来在初始化时给集合添值。比如说List<A> a = new List<A>(){ new A(),new A(),new A()};
11 Finalizers
它定义了对象在被垃圾回收前执行的行为。finailzers不会在对象被标记为垃圾回收时立即执行,而是在对象被标记后与程序结束之前的这段时间内执行。更详细来说,GC发现回收对象带有finailzer,就不会立即回收内存,而是将其加入到一个队列中去。然后用另外一个线程来执行队列中的对象finailzer函数,执行完毕后,再将其移除队列,告知GC可以回收该对象。这个和lua对userdata的处理有一定类似,不过lua的userdata始终是由lua_state所在的线程来处理的。感觉lua要更轻量级一点。
12 匿名变量与匿名类型
C#3.0加入了匿名类型。例如:var p1 = new{title = "avb", year = 1};IL会通过你的属性赋值在编译期自动生成该类型。这又是一项动态语言的特性,类似json与lua table。不过C#的感觉只是语法糖,在IL层面依然是强类型语言。
13 静态构造函数
C#支持静态构造函数,用来初始化类。他会在第一次访问类的时候被调用,具体来说就是比如第一次调用构造,第一次访问静态函数或者成员变量。我们在静态构造函数中对静态变量赋值,如果该变量在声明处赋值,那么最后生效的是静态构造函数里的赋值。
14 静态类
public static class SimpleMesh。静态类,他有两个作用:一,防止程序员实例化这个类;二,防止在该类中声明实例变量和函数。在IL中,静态类被加入了abstract和sealed两个标识。
15 const,readonly
const常量标识符,一个标识为常量的变量,编译器会自动为其添加static,因为没有实例对象能改变他的值。如果我们手动为其添加static标识,编译器会报错。需要注意的是如果一个assembly引用了另一个assembly的常量,这个常量值会被编译到引用方的assembly里面去。如果这时被引用的assembly里的常量值变化了,引用方不重新编译的话,引用方里的值是不会改变的。因此被标识为const的变量应该是永远不会改变的,包含初始化的值。如果在未来具有可变性,应该使用readonly代替。
readonly与const不同在于:一,readonly只能用于成员变量,const还可以用于局部变量;二,readonly可以在构造函数和声明处修改。三,不同对象之间readonly的变量可以不同,所以readonly不会自动添加static;他可以用于静态变量和实例变量。四,readonly的赋值在运行时,而const在编译时。最后当数组被标识为readonly时,只是数组的个数不能改变,而不是数组的元素不能改变。因为readonly只是使得该变量不能指向新的实例。
16 Partial类和Partial函数
Partial类允许将类定义在不同的cs文件中。一般在我们需要对工具生成的代码类中添加自己的变量和函数时需要用到它。在C#3.0中,添加了Partial函数,它提供了更细节的修改生成类的方式。我们可以在生成类中声明Partial函数,比如:partial void OnNameChanging()。然后生成类中函数可以调用这个函数,而这个函数的实现在另一个cs文件中的Partial类中,这个类是我们手动编写的类,为这个生成类提供补充。因此Partial函数只能在Partial类中出现,并且并不要求所有的函数都有实现,所以这种函数的返回值只能为void,也不能使用out。因为如果没有这个设定,那么当你调用一个没有实现的partial函数时,其返回值是不确定的。C#的设计者避免了这种情况。最后,编译器在编译时发现某个调用的partial函数没有实现,那么IL中将不会存在这个函数。
17 Struct
Struct与C++中的Struct有很大不同,与Class是引用类型相比,Struct在C#里的一种值类型。并且Struct不能拥有默认构造,由于C#中对成员变量声明初始化也是被编译器放入到默认构造函数中执行的,所以Struct也不拥有这项功能。虽然不支持默认构造,但是Struct却支持带参数的构造函数,但该构造函数必须初始化所有的成员变量。同时,Struct也没有finalizers函数,虽然它也能够使用new操作符来生成实例。new在CIL层面,Class的指令是newobj.new,而Struct是initobj。最后在继承方面,所有值类型都是sealed,并且都继承于System.ValueType,但是值类型可以执行接口。由于这些特性,在C#中尽可能将Struct作为不可变类型来使用,比如配置,或者说少用。这与C,C++有很大不同。
18 boxing和unboxing
boxing和unboxing发生在值类型与引用类型相互转换的时候。比如说当一个值类型要转化到他执行的接口或者他的基类时,由于这些类型属于引用类型,这个时候就会出现boxing的操作。boxing具体分为3步:第一,在heap上分配包含值类型的数据以及一些其他信息的内存;第二,将值类型的数据从stack拷贝到上一步分配的内存中去;第三,赋值对象或者接口引用这个heap内存。反之,将引用类型转换回值类型就是unboxing。可见频繁的boxing和unboxing会十分影响性能,需要尽力避免,而且由于存在拷贝的过程,那么在boxing后,对之前的值类型修改,不会影响到boxing之后的引用类型数据。反之unboxing也是一样。
19 gc和weak reference
.net的gc使用的mark-and-compact的算法,mark部分和很多gc算法一样,从root开始,然后一直向下mark。最后得到所有的reachable的对象。接着下来是compact的部分,不是通过reachable对象去筛选unreachable,而是将reachable的对象在内存中相邻排列,这个过程也同时覆盖了unreachable的内存。最后除开reachable的内存,剩下的就是可用内存了,从而完成了gc的过程。
由于mark和移动reachable对象需要一个一致的runtime state,所以在gc时,所有的managed thread都会挂起等待gc结束。因此我们需要避免gc发生在关键代码时间里,System.GC提供了Collect函数,他会立即进入gc流程,从而降低gc在关键代码时间里出现的可能性。
.net的gc还有一个特性,就是刚创建的对象更容易被gc,而生命周期长的对象被gc的概率相对较低。这个特性是符合一般程序开发情景的。在一个函数的执行过程中,我们通常会产生很多临时的小变量。为了实现这个特性,.net在内部将对象分为三代,每当一个对象存活了一个gc周期,就会被放入到下一代中,直到最后一代。然后gc运行的频率在0代上最高,2代上最低。
最后是weak reference:和lua类似,弱引用保持了对对象的引用关系,但不会影响对象是否被gc。一般我们可以用它来引用一些创建消耗高,维持占用高的数据,比如一个很大的数据库表。这时weak reference利用了对象被解引用,但gc还不一定到来这个机会。如果这段时间又需要访问这块数据,我们就可以首先判定弱引用是否为null,如果没有为null,说明对象还在,那么就可以再次使用。值得注意的是,这个过程需要先将弱引用赋值给强引用再判断是否为null,防止在判定与赋值之间对象被gc。
20 finalizer函数和IDisposable接口
finalizer函数与c++的类析构函数语法表达一致,他是在gc时被调用来释放与对象相关联的一些资源的接口,比如文件,数据库连接等。我们不能直接调用它,它会在对象被gc时,加入到一个叫f-reachable的队列中去,然后在加入队列到程序结束之间的某一个时间点在另一个线程中触发。因此finalizer具有不确定性因素,而IDisposable接口提供了显示调用的Dispose接口,配合using语句,可以主动调用。
21 Lambda表达式和闭包
在C#3.0中加入了Lambda表达式。如:(int a, int b) => {a < b},由于C#的强类型要求,所以编译器如果能够通过他的赋值delegate的参数类型以及表达式的返回值推导出参数类型,那么参数的类型可以省去。如(a, b) => {a < b}。他也是一层语法糖,在IL层面通过有无upvalue,表现为两种形式。无upvalue的为一个普通函数,有upvalue的为一个seal类,upvalue为他的成员变量。所以在C#中,函数依然不是第一类值,但他也具有了一些函数式语言的特性。不过由于强类型要求,使用时个人感觉还是不如动态语言方便,比如说Lambda表达式的参数个数,返回类型,还是受到声明的delegate的约束。