Unity3D学习之路 - C#学习笔记(二)
51.栈和堆是存放变量与常量的地方,它们有截然不同的生命期语义。
52.栈是用来存放局部变量和参数的内存块,当一个函数被调用和退出时,栈就会逻辑增长和减小,考虑下面的函数:
1 static int Factorial( int n )
2 {
3 if ( 0 == n )
4 {
5 return 1;
6 }
7
8 return ( n * Factorial(n-1) );
9 }
这是一个递归函数,也就说它会自己调用自己。每次函数被调用时,就会有一个新的int参数被分配在栈上,而当函数退出时,int参数被释放。
53.堆是用来存放对象(也就是引用类型的实例)的地方。无论何时当一个对象被创建时,它被分配在堆上,并且一个对该对象的引用将被返回。运行时有一个垃圾收集器,会定期的释放在堆上的对象,所以你的计算机不会耗尽内存。只要一个对象没有对它自身的任何引用,那么该对象就可以被垃圾收集器释放。堆也可以存放静态字段和常量,与在堆上分配的对象不同的是,他们不会被垃圾收集器释放,而是直到应用程序结束才会销毁。
54.注意:不能像C++那样显示的释放对象,一个未被引用的对象最终会被垃圾收集器释放。
55.C#执行明确的分配政策。在实践中,这意味着除了一个不安全的上下文以外,不可能访问到未初始化的内存。明确的分配政策有下面3个含义:
1.局部变量在读取前必须指定一个值。
2.当一个函数被调用时,必须提供函数需要的参数。(除非为默认参数)
3.所有的其他变量(如数组元素)在运行时会自动初始化。
56.所有类型的实例都有一个默认值,预定义类型的默认值就是内存中每一位都是0,引用类型的默认值是null,数值类型和枚举类型的默认值是0,char类型的默认值是'\0',bool类型的默认值是false。可以使用default关键字来获取任意类型的默认值。
57.可以使用ref和out修饰符来控制如何传递参数,如下:
58.默认情况下,C#中参数的传递方式是值传递。也就是说将值类型的实例传递给函数时,将创建一份值的副本。将引用类型的实例传递给函数时,将创建一份引用的副本,而不是所引用的对象的副本。举例如下:
1 static void Foo( int n )
2 {
3 ++n;
4 Console.WriteLine( n );
5 }
6
7 static void Main(string[] args)
8 {
9 int x = 10;
10 Foo( x ); // 创建一份x的副本
11 Console.WriteLine( x ); // x仍旧是10,因为Foo函数改变的是副本
12 }
13 ////////////////////////////////////////////////////////////////////////////
14
15 static void Foo( StringBuilder strBuilder )
16 {
17 // strBuilder与sb引用同一个对象
18 strBuilder.Append( "Reference Copy" );
19
20 // strBuilder是引用的副本,所以改变它不会影响实参
21 strBuilder = null;
22 }
23
24 static void Main(string[] args)
25 {
26 StringBuilder sb = new StringBuilder();
27 Foo( sb );
28 Console.WriteLine( sb.ToString() ); // Reference Copy
29 }
59.C#提供了ref修饰符来进行引用传递,例如:
1 static void Foo( ref int n )
2 {
3 ++n;
4 Console.WriteLine( n );
5 }
6
7 static void Main(string[] args)
8 {
9 int x = 10;
10 Foo( ref x ); // 引用传递,也就是说形参n与实参x现在引用同一块内存
11 Console.WriteLine( x ); // x现在是11
12 }
60.out修饰符与ref修饰符相似,除了:
1.传递到函数之前无需指定值。
2.函数返回之前必须被指定值。
out修饰符通常用于从函数获取多返回值的情形。
61.params修饰符可以用于一个函数的最后一个参数上,以表明该函数接受任意数量的特定类型的参数,但必须声明为数组形式,如:
1 static int Sum (params int[] ints)
2 {
3 int sum = 0;
4 for (int i = 0; i < ints.Length; i++) sum += ints[i];
5 return sum;
6 }
7
8 // 可以这样调用Sum函数
9 Console.WriteLine ( Sum (1, 2, 3, 4) ); // 10
62.从C#4.0开始,提供了默认参数,当一个参数在声明时被指定了一个默认值时,它就是默认参数,如void Foo (int x = 23){ ... }。当调用函数时,默认参数可以被省略,如Foo();此时声明时指定的默认值将被传递给函数。
63.除了使用位置指定参数,还可以使用名称指定参数,如void Foo (int x, int y){ ... }; Foo(x:1, y:2); 以名称指定参数可以使用任意顺序,如Foo(y:2, x:1)和Foo(x:1, y:2)是等价的。也可以混合使用位置和名称指定参数,但是以名称指定参数必须出现在最后,如Foo(1, y:2); 以名称指定参数与默认参数一起使用时显得特别有用,如void Bar (int a=0, int b=0, int c=0, int d=0) { ... } 就可以使用以下方式只提供一个值给d:Bar( d:3 );
64.通常有在声明变量的同时初始化的情况,此时,如果编译器能够从初始化表达式中推断出类型的话,那么就可以使用var来代替类型声明,如var x = 5;等价于int x = 5; 这称为隐式类型变量,它是静态类型,如下面的语句将产生编译错误:var x = 5; x = "string"; // 编译错误,x是int类型
65.一个赋值表达式使用=运算符将一个表达式的结果赋值给一个变量,如int x = x * 5; 赋值表达式能够和其他表达式组合,如y = 5 * (x = 2); 初始化多个变量时,这种方式特别有用:a = b = c = d = 0; 复合赋值运算符是组合赋值运算符与另一个运算符的语法简写,如x *= 2;等价于x = x * 2;
66.当一个表达式包含多个运算符时,优先级和结合律决定了计算顺序。高优先级的运算符将在低优先级的运算符之前计算。当运算符的优先级相同时,运算符的结合律决定了运算顺序。如1+2*3的计算顺序为1+(2*3),因为*运算符的优先级高于+运算符。结合律分为左结合与右结合,左结合就是计算时从左至右,如8/4/2的计算顺序为(8/4)/2,因为/运算符是左结合。右结合就是计算时从右至左,如x=y=3,先将3赋值给y,然后将表达式y=3的结果(即3)赋值给x。
67.下表以优先级从高至低的顺序列出了C#中的运算符,相同副标题下的运算符具有相同的优先级:
68.一条声明语句声明了一个新的变量,在声明变量时可以选择是否使用表达式初始化该变量,声明语句以分号结尾。可以在一条声明语句中声明多个同类型的变量,当中以逗号分隔,如int a = 5, b = 6; 一个常量的声明与变量的声明类似,除了常量在声明后不能被改变,以及必须在声明时进行初始化。
69.一个局部变量的生命期为当前语句块,不能在当前语句块或任何嵌套的语句块中声明另一个同名局部变量。
70.表达式语句意味着表达式"做了"某些事情,如下:
赋值或修改一个变量
实例化一个对象
调用一个函数
一个没有做上面任何事情的表达式是非法的语句,如string s = "foo"; s.Length; // 非法的语句。 当调用一个构造函数或者一个有返回值的函数的时候,并不是必须使用该结果的,如new StringBuilder();是合法的语句。
71.在switch语句中只能使用能够被静态求值的表达式,类型被限制在内建的整型,string类型和枚举类型。在每一个case从句的最后,必须使用一些跳转语句类型明确说明接下去该如何执行,可选的跳转语句如下:
break - 跳转到switch语句的结尾
goto case x - 跳转到另一个case从句
goto default - 跳转到default从句
任何其他的跳转语句 - 也就是return,throw,continue或goto标签
当多个值需要执行相同的代码时,可以依次列出:
1 switch (cardNumber)
2 {
3 case 13:
4 case 12:
5 case 11:
6 Console.WriteLine ("Face card");
7 break;
8 default:
9 Console.WriteLine ("Plain card");
10 break;
11 }
72.foreach语句迭代一个可枚举对象中的每一个元素,C#中大多数表示元素集合或元素列表的类型都是可枚举的,比如数组和字符串,如:
1 foreach ( char c in "foreach" )
2 {
3 Console.Write( c );
4 }
73.跳转语句包括break, continue, goto, return以及throw。break语句结束一个迭代语句或switch语句的执行。continue语句放弃循环中剩余的语句,立即开始下一次迭代。goto语句跳转到一个标签(使用冒号后缀表示)执行。return语句结束函数的执行,如果函数具有返回类型,那么必须返回一个该类型的表达式。
74.命名空间是一个域,在该域中的类型名称必须是唯一的。命名空间不受成员访问权限(private, internal, public等等)的影响。没有定义在任何命名空间的类型就说处于全局命名空间。
75.可以使用完整合格名称引用一个类型,也就是包括该类型定义所在的所有的命名空间。如:System.Security.Cryptography.RSA rsa = System.Security.Cryptography.RSA.Create(); 使用using指令导入命名空间后,就可以不需要使用完整合格名称来引用一个类型,如:using System.Security.Cryptography; RSA rsa = RSA.Create();
76.在外层命名空间声明的名称可以在内层命名空间中直接使用,而不需要使用完整合格名称,如
1 namespace Outer
2 {
3 namespace Middle
4 {
5 class Class1 {}
6
7 namespace Inner
8 {
9 class Class2 : Class1 {}
10 }
11 }
12 }
如果想要使用同一命名空间层级下不同分支里的一个类型,只需要使用完整合格名称的一部分即可:
1 namespace MyTradingCompany
2 {
3 namespace Common
4 {
5 class ReportBase {}
6 }
7
8 namespace ManagementReporting
9 {
10 class SalesReport : Common.ReportBase {}
11 }
12 }
77.如果相同的类型名称同时出现在内层和外层的命名空间中,那么内层的命名空间中的类型将替代外层的命名空间中的类型。如果想要使用外层的命名空间中的类型,需要使用包括命名空间的合格名称。
78.导入命名空间可能会导致类型名称冲突。相比导入整个命名空间,可以只导入需要的类型,并给类型一个别名,如
1 using PropertyInfo2 = System.Reflection.PropertyInfo;
2 class Program { PropertyInfo2 p; }
命名空间也可以给定别名,如
1 using R = System.Reflection;
2 class Program { R.PropertyInfo p; }
79.一个字段是一个类或结构体中的成员变量。一个字段可以使用readonly修饰符来防止它在构造后被修改,一个只读字段只能在声明时或构造函数中赋值。字段的初始化是可选的,未初始化的字段有默认值(0, \0, null, false),字段的初始化在构造函数之前,以他们声明出现的顺序执行。
80.一个类型可以重载方法(有多个相同名称的方法),只要参数类型不同即可,如
1 void Foo (int x);
2 void Foo (double x);
3 void Foo (int x, float y);
4 void Foo (float x, int y);
81.一个类或结构体可以重载构造函数,通过使用this关键字,一个重载的构造函数可以调用另外一个重载的构造函数,如
1 public class Wine
2 {
3 public Wine (decimal price) {...}
4 public Wine (decimal price, int year)
5 : this (price) {...}
6 }
当一个构造函数调用另外一个构造函数时,被调用的构造函数先执行。
82.对于类来说,当且仅当没有定义任何构造函数时,编译器会自动生成一个无参的构造函数。对于结构体来说,无参的构造函数是结构体的内部结构,因此不能自己定义。结构体的无参的构造函数负责使用默认值初始化结构体中的每一个字段。
83.为了简化对象的初始化,对象的可访问的字段或属性可以在构造后的单条语句中初始化,如
1 public class Bunny
2 {
3 public string Name;
4 public bool LikesCarrots, LikesHumans;
5 public Bunny () {}
6 public Bunny (string n) { Name = n; }
7 }
8
9 // 可以像下面这样初始化
10 Bunny b1 = new Bunny {
11 Name="Bo",
12 LikesCarrots = true,
13 LikesHumans = false
14 };
15
16 Bunny b2 = new Bunny ("Bo") {
17 LikesCarrots = true,
18 LikesHumans = false
19 };
84.属性从外部看像字段,但在内部他们包含逻辑,像函数一样。一个属性像字段那样声明,但是附加了get/set块,如
1 public class Stock
2 {
3 decimal currentPrice;
4 public decimal CurrentPrice // 属性
5 {
6 get { return currentPrice; }
7 set { currentPrice = value; }
8 }
9 }
get和set表示属性的访问器,当属性被读取的时候,get访问器就会运行,它必须返回与属性类型相同的一个值。当属性被赋值的时候,set访问器就会被运行,它包含一个名称为value的与属性类型相同的隐式参数。一个属性如果只定义了get访问器,那么它就是只读的。如果只定义了set访问器,那么它就是只写的。
85.大多数属性的实现仅仅只是读与写一个与属性同类型的私有字段,一个自动属性声明可以告诉编译器提供这种实现,自动属性声明如下:
1 public class Stock
2 {
3 public decimal CurrentPrice { get; set; }
4 }
此时编译器就会自动生成一个私有字段供属性读和写,该字段的名称是编译器生成的,所以不能引用。
86.索引器提供了一种自然的语法来访问一个类或结构体中的元素。写一个索引器需要定义一个名称为this的属性,然后在方括号中指定参数:
1 class Sentence
2 {
3 string[] words = "The quick brown fox".Split();
4 public string this [int wordNum] // 索引器
5 {
6 get { return words [wordNum]; }
7 set { words [wordNum] = value; }
8 }
9 }
一个索引器可以使用多个参数。每个类型可以定义多个索引器,只要它们的参数类型不同即可。如果省略set访问器,那么索引器就是只读的。
87.一个常量是一个无法改变其值的字段。一个常量在编译时期被静态求值,无论何时使用它,编译器将用字面值替换它。一个常量使用关键字const进行声明,声明时必须被初始化:
1 public class Test
2 {
3 public const string Message = "Hello World";
4 }
88.静态构造函数在每个类型上执行一次,而不像普通构造函数那样在每个实例上执行一次。一个类型只能定义一个静态构造函数,并且必须无参。当一个类型被使用之前,运行时会自动调用静态构造函数,有2件事会触发这种行为:实例化一个类型的对象以及访问类型中的静态成员。有个需要注意的地方:如果静态构造函数引发一个未处理的异常,那么在应用程序的生命期内,该类型都无法使用。
89.一个类可以被标记为静态的,表明它必须完全由静态成员组成。
90.终结器(Finalizers)是只有类拥有的函数,该函数在垃圾收集器回收一个未被引用的对象的内存时被调用。终结器的语法是类名加上前缀~:
1 class Class1
2 {
3 ~Class1() { ... }
4 }
91.一个类只能有一个基类,但它自身可以作为许多类的基类。
92.引用是多态的,也就是说一个类型的变量可以引用它的子类对象。多态的实现基于这样一个事实:子类拥有其基类的所有功能。
93.一个对象的引用能够:
隐式的向上转型到基类的引用
显式的向下转型到子类的引用
在兼容的引用类型之间向上转型或向下转型将执行引用转换,引用转换将创建一个指向同对象的引用。向上转型总是成功的,而向下转型是否成功取决于对象的类型是否合适。
94.一个向上转型操作从子类引用创建一个基类引用。一个向下转型操作从基类引用创建一个子类引用,一个向下转型需要显式指定因为可能会失败,如果失败,将抛出InvalidCastException异常:
House h = new House();
Asset a = h; // 向上转型总是成功
Stock s = (Stock)a; // 向下转型失败: a不是Stock类型
95.as运算符执行向下转型,如果转型失败,会返回null,而不是抛出异常。as运算符不能用于数值转换。
96.is运算符用于测试是否一个引用转换将会成功,换句话说,也就是是否一个对象派生自一个特定类(或实现了一个接口)。
97.函数,属性,索引器以及事件都可以被声明为virtual,被标记为virtual就能够被想要提供特殊实现的子类覆盖(overridden),子类通过使用override修饰符来实现覆盖:
1 public class Asset
2 {
3 public string Name;
4 public virtual decimal Liability { get { return 0; } }
5 }
6
7 public class House : Asset
8 {
9 public decimal Mortgage;
10 public override decimal Liability
11 { get { return Mortgage; } } // 覆盖基类的属性,提供特殊实现。使用override修饰符
12 }
虚方法和覆盖方法的签名,返回类型以及访问权限必须一致。如果一个覆盖方法想要调用基类的实现,那么可以使用base关键字。
98.一个被声明为抽象(abstract)的类不能被实例化。抽象类可以定义抽象成员,抽象成员类似于虚成员,但他们不需要提供一个默认实现。子类必须提供抽象成员的实现,除非子类也被声明为抽象类。
99.一个基类和一个子类可以定义相同的成员,例如:
1 public class A { public int Counter = 1; }
2 public class B : A { public int Counter = 2; }
此时可以说B类中的Counter字段隐藏了A类中的Counter字段。编译器会生成警告并且按如下的方法解决二义性:A类的引用对象绑定A.Counter,B类的引用对象绑定B.Counter。但是有时候,可能是故意想要隐藏成员,在这种情况下,可以使用new修饰符:
public class A { public int Counter = 1; }
public class B : A { public new int Counter = 2; }
new修饰符其实只是使得编译器不再会生成警告,并且可以告诉其他程序员,这是故意的。
100.一个覆盖的方法可以使用sealed关键字来密封它的实现,从而防止被其子类覆盖。也可以密封类,从而隐式的密封所有的虚函数。