C#高编 - 对象和类型
1.类和结构
结构和类的区别是它们在内存中的存储方式、访问方式(类是存储在堆(heap)上的引用类型,而结构是存储在栈(stack)上的值类型)和它们的一些特征(如结构不支持继承)。
较小的数据类型使用结构可以提高性能。
对于类和结构,都使用关键字new来声明实例:这个关键字创建对象并对其进行初始化。
类
2.类中的数据和函数称为类的成员。除了这些成员外,类还可以包含嵌套的类型(如其他类)。
成员的可访问性可以是public、protected、internal protected、private和internal。
数据成员是包含类的数据—字段、常量和事件的成员。数据成员可以是静态数据。类成员总是实例成员,除非用static进行显式的声明。
函数成员提供了操作类中数据的某些功能,包括方法、属性、构造函数和终结器(finalizer)、运算符和索引器。函数成员默认为实例成员,使用static可以把方法定义为静态方法。
- 终结器类似于构造函数,但是在CLR检测到不再需要某个对象时调用它。它们的名称与类相同,但前面有一个“~”符号。不可能预测什么时候调用终结器。
- 索引器允许对象以数组或集合的方式进行索引。
3.方法
正式的C#术语区分函数和方法。在C#术语中,“函数成员”不仅包含方法,而且也包含类或结构的一个非数据成员,如索引器、运算符、构造函数和析构函数等,甚至还有属性。
参数可以通过引用或通过值传递给方法。当通过引用传递时,被调用的方法得到的就是这个变量,所以在方法内部对变量改变在方法退出后仍旧有效。而通过值传递时,被调用的方法得到的是变量的一个相同副本。对于复杂的数据类型,按引用传递效率更高,因为在按值传递时,必须复制大量的数据。
注意:字符串的行为方式有所不同,因为字符串是不可变的,所以字符串无法采用一般引用类型的行为方法。(需使用ref或out)
ref:使用ref可使值通过引用传递给方法。在调用方法时,也需要添加ref关键字。
C#仍要求对传递给方法的参数进行初始化,在传递给方法之前,无论是按值传递,还是按引用传递,任何变量都必须初始化。
out:传递给方法的变量可以不初始化。
命名参数:参数一般需要按定义的顺序传送给方法,命名参数允许按任意顺序传递。如:FullName(lastName:"Doe",firstName:"John");
如果方法有几个参数,就可以在同一个调用中混合使用位置参数和命名参数。
可选参数:参数也可以是可选的。必须为可选参数提供默认值。可选参数还必须是方法定义的最后一个参数。
void TestMethod(int notOptionalNumber,Int optionalNumber = 10) { System.console.Write(optionalNumbe+ notOptionalNumber); }
重载:
- 两个方法不能仅在返回类型上有区别。
- 两个方法不能仅根据参数是声明为ref还是out来区分
5.属性:
- 在属性定义中省略set访问器,就可以创建只读属性。同样,省略get访问器,就可以创建只写属性。但是,这是不好的编程方法。
- C#允许给属性的get和set访问器设置不同的访问修饰符,所以属性可以有公有的get访问器或受保护的set访问器。
- 如果属性的set和get访问器中没有任何逻辑,就可以使用自动实现的属性。如:public int Age{get;private set;}(必须有两个访问器,访问级别可以不同)
- JIT编译器可生成高度优化的代码,并在适当的时候随意内联。因此不必担心通过属性访问字段这些额外的函数调用带来的系统开销。
6.构造函数:
可以把构造函数定义为private或protected,这样不相关的类也不能访问它们。
当类无定义任何公有的或受保护的构造函数,则无法实例化。这在下面两种情况下是有用的:
- 类仅用作某些静态成员或属性的容器,因此永远不会实例化它。
- 希望类仅通过调用某个静态成员函数来实例化(对象实例化的类工厂方法)
静态构造函数:无参数的静态构造函数,只执行一次。而实例构造函数在每次创建实例都会执行。
编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。
需要注意的是:
- .Net运行库没有确保什么时候执行静态构造函数,所以不应把要求在某个特定时刻(如,加载程序集时)执行的代码放在其中。也不能预计不同类的静态构造函数按照什么顺序执行。但可以确保至多执行一次,即在代码引用类之前调用它。因此,静态构造函数代码不能依赖其它的静态构造函数的执行情况。
- 静态构造函数没有访问修饰符,其它代码从来不访问它。且不能带任何参数,一个类也只能有一个静态构造函数。
- 静态构造函数只能访问类的静态成员,不能访问类的实例成员。
- 无参实例构造函数与静态构造函数可以在一个类中同时定义。尽管参数列表相同,但并不矛盾,因为在加载类时执行静态构造,而在创建实例时执行实例构造。
从构造函数中调用其他构造函数
由于一个类中的不同构造函数包含一些共同的代码,如初始化了相同的字段,最好把所有代码放在一个地方。
构造函数初始化器:
class Car { private string description; private unit nWheels; public Car(string description,unit nWheels) { this.description = description; this.nWheels = nWheels; } public Car(string description) : this(description,4) { } }
这里,this关键字仅调用参数最匹配的那个构造函数。注意,构造函数初始化器在构造函数的函数体之前执行。且优先执行引用的构造函数,再执行本身。
C#构造函数初始化器还可以包含对直接基类的构造函数的调用,将base关键字代替this。初始化器不能有多个调用。
7.只读字段:与常量区别在于,需要一些变量,其值不应改变,但在运行之前其值是未知的。
规则:
- 可以在构造函数中给只读函数赋值,但不能在其他地方赋值。
- 只读字段可以是一个实例字段,而不是静态字段,每个实例可以有不同的值。
- 与const字段不同,如果要把只读字段设置为静态,就必须显式声明它。(const不需要public)
- 在构造函数中不必给只读字段赋值,如果没有赋值,它的值就是其特定数据类型的默认值,或者在生命时给它初始化的值。(适用于只读的静态和实例字段)
8.匿名类型
匿名类型只是继承自Object且没有名称的类,var与new关键字一起使用时,可以创建匿名类型。该类的定义从初始化器中推断,类似于隐式类型化的变量。
如:
var captain = new {FirstName = "James",MiddleName = "T",LastName = "Kirk"};
这会生成一个包含FirstName、MiddleName和LastName属性的对象。如果创建另一个对象,如:
var doctor= new {FirstName = "Leonard",MiddleName = "",LastName = "McCoy"};
Captain和doctor的类型就相同。
如果所设置的值来自于另一个对象,就可以简化初始化器。如:
var captain= new {person.FirstName, person.MiddleName, person.LastName};
这些新对象的类型名未知。编译器为类型“伪造”了一个名称,但只有编译器才能使用它。不能也不应使用新对象上的任何类型反射,因为结果不一致。
结构
9.结构是值类型:
存储在栈中或存储为内联(如果它们是存储在堆中的另一个对象的一部分),其生存期的限制与简单的数据类型一样。
- 结构不支持继承。
- 对于结构构造函数,编译器总是提供一个无参的默认构造函数,它是不允许替换的。
- 使用结构,可以指定字段如何在内存中的布局。
new运算符与类和其它引用类型的工作方式不同。new运算符并不分配堆中的内存,而是只调用相应的构造函数,根据传送给它的参数,初始化所有的字段。如下述完全合法代码:
Dimensions point = new Dimensions(); point.Length = 3; point.Width = 6; //可修改为如下方式 Dimensions point; point.Length = 3; point.Width = 6;
如果是类,则会产生一个编译错误,因为point包含一个未初始化的引用—不指向任何地方的一个地址,所有不能给其字段设置值。但对于结构,变量声明实际上是为整个结构在栈中分配空间,
所以就可以为它赋值了。但下面的代码会产生一个编译错误,编译器会抱怨用户使用了未初始化的变量:
Dimensions point;
Double D = point.Length;
需调用new运算符,或者给所有字段赋值,结构才完全初始化。但如果结构定义为类的成员字段,在初始化包含的对象时,该结构会自动初始化为0;
结构影响性能的两个方面
- 正面:为结构分配内存速度非常快,在结构超出了作用域被删除时速度也很快。
- 负面:把结构作为参数来传递或者把结构赋予另一个结构(如A=B),结构的所有内容就被复制,而对于类,只复制引用。这样会有性能损失,根据结构大小,性能损失也不同。(可用ref避免)
10.结构和继承:
结构不是为继承设计的。这意味着,它不能从一个结构中继承。唯一例外是对应的结构最终派生与类System.Object。因此类可以访问或重写System.Object的方法。
结构的继承链:Stuct 派生自System.ValueType 派生自 System.Object。
11.结构的构造函数:
- 不允许定义无参构造函数。
- 提供字段的初始值不能绕过默认构造函数。见如下代码
struct Dimensions { public double Length = 1; // error.Initial values not allowed public double Width = 2; // error.Initial values not allowed }
- 可以为结构提供Close()或Dispose()方法
11.部分类
partial关键字预先把类、结构或接口放在多个文件中。当多个开发人员需要访问同一个类,或者某种类型的代码生成器生成了一个类的某部分,把类放在多个文件中是有益的。
如果声明时使用了下面的关键字,这些关键字就必须应用于同一个类的所有部分:
- public
- private
- protected
- internal
- abstract
- sealed
- new
- 一般约束
12.静态类
如果类只包含静态的方法和属性,该类就是静态的。
静态类在功能上与使用私有静态构造函数创建的类相同,不能创建静态类的实例。
13.Object类
如果在定义类时没有指定基类,编译器就会自动假定这个类派生自Object。
System.Object()方法
- Tostring():获取对象字符串表示的一种快捷方式。声明为虚方法。
- GetHashTable():如果对象放在名为映射(也成为散列表或字典)的数据结构中,就可以使用这个方法确定把对象放在结构的什么地方。如果希望把类当作字典的一个键,则需要重写此方法,当有严格的限制。
- Equals()(两个方法)和ReferenceEquals()方法:如果把3个用于比较对象相等性的不同方法组合起来,就说明.NET在比较相等性方面有相当复杂的模式。与运算符“==”在使用上有微妙的区别。
- Finalize()方法:最接近C++风格的析构函数,在引用对象作为垃圾被回收以清理资源时调用它。垃圾收集器不能直接删除对未托管资源的引用,因为它只负责托管资源,因此此方法如果要使用需要重写。
- MemberwiseClone()方法:复制对象,返回对副本的一个引用(对于值类型,是一个装箱的引用)。注意,得到的副本是一个浅表复制,即它复制了类中所有的值类型,如果类包含内嵌的引用,就只复制引用,而不复制引用的对象。这个方法是受保护的,所以不能复制外部的对象。不是虚方法,所以不能重写。
14.扩展方法:
在没有类的源代码的情况下,允许改变一个类,添加函数。如果是有类的源代码,继承就是给对象添加功能的好方法。
对于扩展方法,第一个参数是要扩展的类型,它放在this关键字的后面。这告诉编译器,这个方法是类型的一部分。
using System; namespace Wrox { public class Money { private decimal amount; public decimal Amount { get { r eturn amount; } set { amount = value; } } public override string ToString() { return "$" + Amount.ToString(); } } } //扩展方法 namespace Wrox { public static class MoneyExtension { public static void AddToAmount(this Money money,decimal amountToAdd) { money.Amount += amountToAdd; } } }
规则如下:
- 扩展方法是静态方法,它是类的一部分,但实际上没有放在类的源代码中。即使是静态的,也要使用标准的实例方法语法。
-
Money cash1 = new Money(); cash1.Amount = 40M; cash1.AddToAmount(10M);
- 在扩展方法中,可以访问所扩展类型的所有公有方法和属性。
- 如果扩展方法与类中的某个方法同名,就从来不会调用扩展方法。类中已有的任何实例方法优先。