- #1. 类和结构
-
#2. 类
- 2.1 数据成员
- 2.2 函数成员
- 2.3 只读字段
- #3. 匿名类型
-
#4. 结构
- 4.1 结构是值类型
- 4.2 结构和继承
- 4.3 结构的构造函数
- #5. 部分类
- #6. 静态类
-
#7. Object类
- 7.1 System.Object()方法
- 7.2 ToString()方法
#1. 类和结构
类和结构实际上都是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法。类定义了类的每个对象(称为实例)可以包含什么数据和功能。
class PhoneCustomer {
public const string DayOfSendingBill = "Monday";
public int CustomerID;
public string FirstName;
public string LastName;
}
结构与类的区别是:它们在内存中的存储方式、访问方式(类是存储在堆(heap)上的引用类型),而结构是存储在栈(stack)上的值类型和它们的一些特征(如结构不支持继承)。较小的数据类型使用结构可提高性能。但在语法上,结构和类非常相似,主要区别是使用关键字struct代替class来声明结构。例如,如果希望所有的PhoneCustomer实例都分布在栈上,而不是在托管堆上,就可以编写下面的语句:
struct PhoneCustomerStruct {
public const string DayOfSendingBill = "Monday";
public int CustomerID;
public string FirstName;
public string LastName;
}
对于类和结构,都使用关键字new来声明实例:这个关键字创建对象并对其进行初始化。在下面的例子中,类和结构的字段值都默认为0:
PhoneCustomer myCustomer = new PhoneCustomer(); //works for a class
PhoneCustomerStruct myCustomer2 = new PhoneCustomerStruct(); //works for a struct
#2. 类
类中的数据和函数称为类的成员。Microsoft的正式术语对数据成员和函数成员进行了区分。除了这些成员外,类还可以包含嵌套的类型(如其他类)。成员的可访问性可以是public、protected、internal protected、private或internal。
2.1 数据成员
数据成员是包含类的数据——字段、常量和事件的成员。数据成员可以是静态数据。类成员总是实例成员,除非用static进行显示的声明。
字段是与类相关的变量。一旦实例化PhoneCustomer对象,就可以使用语法Object.FieldName来访问
这些字段,如下例所示:
PhoneCustomer customer1 = new PhoneCustomer();
customer1.FirstName = "Simon";
常量与类的关联方式同变量与类的关联方式。使用const关键字来声明常量。如果把它声明为public,就可以在类的外部访问它。
2.2 函数成员
函数成员提供了操作类中数据的某些功能,包括方法、属性、构造函数和终结器(finalizer)、运算符以及索引器。
- 方法是与某个类相关的函数,与数据成员一样,函数成员默认为实例成员,使用static修饰符可以把方法定义为静态方法(类成员)。
- 属性是可以从客户端访问的函数组,其访问方式与访问类的公共字段类似。C#为读写类中的属性提供了专用的语法。在客户端代码中,虚拟的对象被当做实际的东西。
- 构造函数是在实例化对象时自动调用的特殊函数。它们必须与所属的类同名,且不能有返回类型。构造函数用于初始化字段的值。
- 终结器类似于构造函数,但是在CLR检测到不再需要某个对象的时候调用它。它们的名称与类相同,但前面有个“~”符号。不可能预测什么时候调用终结器。
- 运算符执行的最简单的操作就是加法和减法。在两个整数相加时,严格来说,就是对整数使用“+”运算符。C#还允许指定把已有的运算符应用于自己的类(运算符重载)。
- 索引器允许对象以数组或集合的方式进行索引。
1. 方法
注意,正式的C#术语区分函数和方法。在C#术语中,“函数成员”不仅包含方法,而且也包含类或结构的一些非数据成员,如索引器、运算符、构造函数和析构函数等,甚至还有属性。这些都不是数据成员,字段、常量和事件才是数据成员。
- (1) 方法的声明
在C#中,方法的定义包括任意方法修饰符(如方法的可访问性)、返回值类型,然后依次是方法名和输入参数的列表(用圆括号括起来)和方法体(用花括号括起来)。
[modifiers] return_type MethodName([parameters])
{
//Method body
}
每个参数都包括参数的类型名和在方法体中的引用名称。但如果方法有返回值,return语句就必须与返回值一起使用,以指定出口点。例如:
public bool IsSquare(Rectangle rect)
{
return (rect.Height == rect.Width);
}
- (2) 给方法传递参数
参数可以通过引用或者通过值传递给方法。在变量通过引用传递给方法时,被调用的方法得到的就是这个变量,所以在方法内部对变量进行的任何改变在方法退出后仍旧有效。而如果变量通过值传递给方法,被调用的方法得到的是变量的一个相同副本。也就是说,在方法退出后,对变量进行的修改会丢失。对于复杂的数据类型,按引用传递的效率更高,因为在按值传递时,必须复制大量的数据。
在C#中,除非特别说明,所有的参数都通过值来传递。
static void SomeFunction(int[] ints, int i)
{
ints[0] = 100;
i = 100;
}
static void Main(string[] args)
{
int i = 0;
int[] ints = { 0, 1, 2, 4, 8 };
//Display the orginal values
Console.WriteLine("i = " + i);
Console.WriteLine("ints[0] = " + ints[0]);
Console.WriteLine("Calling SomeFunction");
//After this method returns,ints will be changed,
//but i will not.
SomeFunction(ints, i);
Console.WriteLine("i = " + i);
Console.WriteLine("ints[0] = " + ints[0]);
}
- (3) ref参数
如前所述,通过值传送变量是默认的,也可以迫使值参数通过引用传送给方法。为此,要使用ref关键字。如果把一个参数传递给方法,且这个方法的输入参数前带有ref关键字,则该方法对变量所做的任何改变都会影响原始对象的值:
static void SomeFucntion(int[] ints, ref int i) {
ints[0] = 100;
i = 100;//The change to i will persist after SomeFunction() exist.
}
在调用方法时,还需要使用ref关键字:
SomeFucntion(ints, ref i);
最后,C#仍要求对传递给方法的参数进行初始化,理解这一点也非常重要。在传递给方法之前,无论是按值传递,还是按引用传递,任何变量都必须初始化。
- (4) out参数
在C风格的语言中,函数常常能从一个例程中输出多个值,这使用输出参数实现。只要把输出的值赋予通过引用传递给方法的变量即可。通常,变量通过引用传递的初值并不重要,这些值会被函数重写,函数甚至从来没有使用过它们。
编译器使用out关键字来初始化。在方法的输入参数前面加上out前缀时,传递给该方法的变量可以不初始化。该变量通过引用传递,所以在从被调用的方法中返回时,对应方法对该变量进行的任何改变都会保留下来。在调用该方法时,还需要使用out关键字,与在定义该方法时一样。
static void SomeFunction(out int i)
{
i = 100;
}
static void Main(string[] args)
{
int i; //note how i is declared but not initialized.
SomeFunction(out i);
Console.WriteLine(i);
}
- (5) 命名参数
参数一般需要按定义的顺序传送给方法。命名参数允许任意顺序传递。所以下面的方法:
static string FullName(string firstName,string lastName) {
return firstName + " " + lastName;
}
下面的方法调用会返回相同的全名:
Console.WriteLine(FullName("henry","hu"));
Console.WriteLine(FullName(lastName:"hu",firstName:"henry"));
如果方法有几个参数,就可以在同一个调用中混合使用位置参数和命名参数。
- (6) 可选参数
参数也可以是可选的。必须为可选参数提供默认值。可选参数还必须是方法定义的最后一个参数。所以下面的方法声明是不正确的:
void TestMethod(int optionalNumber = 10,int notOptionalNumber)
{
Console.Write(optionalNumber + notOptionalNumber);
}
要使这个方法正常工作,就必须在最后定义optionalNumber参数。
- (7) 方法的重载
C#支持方法的重载——方法的几个版本有不同的签名(即,方法名相同,但参数的个数和/或类型不同)。为了重载方法,只需声明同名但参数个数或类型不同的方法即可。
class ResultDisplayer
{
void DisplayResult(string result)
{
//impletation
}
void DisplayResult(int result)
{
//impletation
}
}
在任何语言中,对于方法重载,如果调用了错误的重载方法,就有可能出现运行错误。现在,知道C#在重载方法的参数方面有一些小限制即可:
- 两个方法不能仅在返回类型上有区别。
- 两个方法不能仅根据参数是声明为ref还是out来区分。
2. 属性
属性(property)的概念是:它是一个方法或一对方法,从客户端代码看来,它们是一个字段。例如Windows窗体的Height属性。假定有下面的代码:
// mainForm is of type System.Windows.Forms
mainForm.Height = 400;
在语法上,上面的代码类似于设置一个字段,但实际上是调用了属性访问器,它包含的代码重新设置了窗体的大小。
在C#中定义属性,可以使用下面的语法:
public string SomeProperty
{
get
{
return "This is the property value."
}
set
{
//do whatever needs to be done to set the property.
}
}
get访问器不带任何参数,且必须返回属性声明的类型。也不应该为set访问器指定任何显示参数,但编译器假定它带一个参数,其类型也与属性相同,并表示为value。
- (1) 只读和只写属性
在属性定义中省略set访问器,就可以创建只读属性。因此,如下代码把Name变成只读属性:
private string name;
public string Name
{
get
{
return name;
}
}
同样,在属性定义中省略get访问器,就可以创建只写属性。
- (2) 属性的访问修饰符
C#允许给属性的get和set访问器设置不同的访问修饰符,所以属性可以有公有的get访问器和私有或收保护的set访问器。这有助于控制属性的设置方式或时间。
public string Name
{
get
{
return _name;
}
private set
{
_name = value;
}
}
上述代码示例中,set访问器有一个私有访问修饰符,而get访问器没有任何访问修饰符。这表示get访问器具有属性的访问级别。在get和set访问器中,必须有一个具备属性的访问级别。如果get访问器的访问级别是protected,就会产生一个编译错误,因为这会使两个访问器的访问级别都不是属性。
- (3) 自动实现的属性
如果属性的set和get访问器中没有任何逻辑,就可以使用自动实现的属性。这种属性会自动实现后备成员变量。
public int Age {get; set;}
不需要声明private int age。编译器会自动创建它。使用自动实现的属性,就不能在属性设置中验证属性的有效性。所以在上面的例子中,不能检查是否设置了无效的年龄。但必须有两个访问器。尝试把该属性设置为只读属性,就会出错:
public int Age {get;}
但是,每个访问器的访问权限可以不同。因此,下面的代码是合法的:
public int Age {get; private set;}
- (4) 内联
C#代码会编译为IL,然后在运行时JIT编译为本地可执行代码。JIT编译器可生成高度优化的代码,并在适当的时候随意地内联代码(即,用内联代码来代替函数调用)。如果实现某个方法或属性仅是调用另一个方法,或返回一个字段,则该方法或属性肯定是内联的。但要注意,在何处内联代码完全由CLR决定。我们无法使用像C++中inline这样的关键字来控制哪些方法是内联的。
3. 构造函数
声明基本构造函数的语法就是声明一个与包含的类同名的方法,但该方法没有返回类型:
public class MyClass
{
public MyClass()
{
}
//rest of class definition
}
一般情况下,如果没有提供任何构造函数,编译器会在后台创建一个默认的构造函数。这是一个非常基本的构造函数。它只能把所有的成员字段初始化为标准的默认值。
- (1) 静态构造函数
C#的一个新特征是也可以给类编写无参数的静态构造函数。这种构造函数只执行一次,而前面的构造函数是实例构造函数,只要创建类的对象,就会执行它。
class MyClass
{
static MyClass()
{
//initialization code
}
//rest of class definition
}
编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。
.Net运行库没有确保什么时候执行静态构造函数,所以不应把要求某个特定时刻(例如,加载程序集时)执行的代码放在静态构造函数中。也不能预计不同类的静态构造函数按照什么顺序执行。但是,可以确保静态构造函数至多运行一次,即在代码引用类之前调用它。在C#中,通常在第一次调用类的任何成员之前执行静态构造函数。
注意,静态构造函数没有访问修饰符,其他C#代码从来不调用它,但在加载类时,总是由.NET运行库调用它,所以访问修饰符没有任何意思。处于同样原因,静态构造函数不能带任何参数,一个类也只能有一个静态构造函数。很显然,静态构造函数只能访问类的静态成员,不能访问类的实例成员。
public class UserPreference
{
public static readonly Color BackColor;
//Static constructor
static UserPreference()
{
DataTime now = DateTime.Now;
if(now.DayOfWeek == DayOfWeek.Saturday
|| now.DayOfWeek == DayOfWeek.Sunday)
{
BackColor = Color.Green;
}
else
{
BackColor = Color.Red;
}
}
//Instance constructor
private UserPreference()
{
}
}
- (2) 从构造函数中调用其他构造函数
有时,在一个类中有几个构造函数,以容纳某些可选参数,这些构造函数包含一些共同的代码。例如,下面的情况:
class Car
{
private string description;
private uint nWheels;
public Car(string description,uint nWheels)
{
this.description = description;
this.nWheels = nWheels;
}
public Car(string description)
{
this.description = description;
this.nWheels = 4;
}
}
这两个构造函数初始化了相同的字段,显然,最好把所有的代码放在一个地方。C#有一个特殊的语法,称为构造函数初始化器,可以实现此目的:
class Car
{
private string description;
private uint nWheels;
public Car(string description,uint nWheels)
{
this.description = description;
this.nWheels = nWheels;
}
public Car(string description) : this(description,4)
{
}
//etc.
}
这里,this关键字仅调用参数最匹配的那个构造函数。注意,构造函数初始化器在构造函数的函数体之前执行。现在假定运行下面的代码:
Car myCar = new Car("Proton Persona");
在本例中,在带一个参数的构造函数的函数体执行之前,先执行带两个参数的构造函数(但在本例中,因为在带一个参数的构造函数的函数体中没有代码,所以没有区别)。
C#构造函数初始化器可以包含对同一个类的另一个构造函数的调用,也可以包含对直接基类构造函数的调用(使用相同的语法,但应使用base关键字代替this)。初始化器中不能有多个调用。
2.3 只读字段
常量的概念就是一个包含不能修改的值的变量,常量是C#与大多数编程语言共有的。但是,常量不必满足所有的要求。
readonly关键字比const关键字灵活得多,允许把一个字段设置为常量,但还需要执行一些计算,以确定初始值。其规则是可以在构造函数中给只读字段赋值,但不能在其他地方赋值。 只读字段还可以是一个实例字段,而不是静态字段,类的每个实例可以有不同的值。与const字段不同,如果要把只读字段设置为静态,就必须显示声明它。
#3. 匿名类型
var关键字可用于表示隐式类型化的变量。var和new关键字一起使用时,可以创建匿名类型。匿名类型只是一个继承Object且没有名称的类。该类的定义从初始化器中推断,类似于隐式类型化的变量。
如果需要一个对象包含某个人的姓氏、中间名和名字,则声明如下:
var captain = new {FirstName = "James", MiddleName = "T", LastName = "Kirk"};
这会生成一个包含FirstName、MiddleName和LastName属性的对象。
#4. 结构
有时仅需要一个小的数据结构。此时,类提供的功能多于我们需要的功能,由于性能原因,最好使用结构。看看下面的例子:
class Dimensions
{
public double Length;
public double Width;
}
上面的代码定义了类Dimensions,它只存储了某一项的长度和宽度。有时我们需要存储轻量级的数据,并不需要class所带来的复杂功能。
为此,只需要修改代码,用关键字stuct代替class,定义一个结构而不是类,如本章前面所述:
struct Dimensions
{
public double Length;
public double Width;
}
为结构定义函数与为类定义函数完全相同。下面代码说明了结构的构造函数和属性:
struct Dimensions
{
public double Length;
public double Width;
public Dimensions(double length,double width)
{
Length = length;
Width = width;
}
public double Diagonal
{
get
{
return Math.Sqart(Length*Length + Width*Width);
}
}
}
结构是值类型,不是引用类型。它们存储在栈中或存储为内联(inline)(如果它们是存储在堆中的另一个对象的一部分),其生存期的限制与简单的数据类型一样。
- 结构不支持继承。
- 对于结构构造函数的工作方式有一些区别。尤其是编译器总是提供一个无参数的默认构造函数,它是不允许替换的。
- 使用结构,可以指定字段如何在内存中的布局。
因为结构实际上是把数据项组合在一起,有时大多数或者全部字段都声明为public。
4.1 结构是值类型
虽然结构是值类型,但在语法上常常可以把它们当作类来处理。例如,在上面的Dimensions类的定义中,可以编写下面的代码:
Dimensions point = new Dimensions();
point.Length = 3;
point.Width = 6;
注意,因为结构是值类型,所以new运算符与类和其他引用类型的工作方式不同。new运算符并不分配堆中的内存,而是只调用相应的构造函数,根据传送给它的参数,初始化所有的字段。对于结构,可以编写下述完全合法的代码:
Dimensions point;
point.Length = 3;
point.Width = 6;
如果Dimensions是一个类,就会产生一个编译错误,因为point包含一个未初始化的引用——不指向任何地方的一个地址,所以不能给其字段设置值。但对于结构,变量声明实际上是为整个结构在栈中分配空间。所以就可以为它赋值了。但要注意下面的代码会产生一个编译错误,编译器会抱怨用户使用了未初始化的变量:
Dimensions point;
Double D = point.Length;
结构遵循其他数据类型都遵循的规则:在使用前所有的元素都必须进行初始化。在结构上调用new运算符,或者给所有的字段分别赋值,结构就完全初始化了。当然,如果结构定义为类的成员字段,在初始化包含的对象时,该结构会自动初始化为0。
结构是会影响性能的值类型,但根据使用结构的方式,这种影响可能是正面的,也可能是负面的。
- 正面的影响是为结构分配内存和销毁时,速度非常快。因为它们将内联或者保存在栈中。
- 负面的影响是,只要把结果作为参数来传递或者把一个结构赋予另一个结构(如A=B,其中A和B是结构),结构的所有内容就被复制。
4.2 结构和继承
结构不是为继承设计的。这意味着:它不能从一个结构中继承。唯一的例外是对应的结构(和C#中的其他类型一样)最终派生于类System.Object。结构的继承链是:每个结构派生自System.ValueType类,System.ValueType类又派生自System.Object。
4.3 结构的构造函数
为结构定义构造函数的方式与为类定义构造函数的方式相同,但不允许定义无参数的构造函数。这看起来似乎没有意义,其原因隐藏在.NET运行库的实现方式中。下述情况非常少见:.NET运行库不能调用用户提供的自定义无参数构造函数。因此Microsoft采用一种非常简单的方式,禁止在C#结构内使用无参数的构造函数。
前面说过,默认构造函数把数值字段都初始化为0,把引用类型初始化为null,且总是隐式地给出,即使提供了其他参数的构造函数,也是如此。提供字段的初始值也不能绕过默认构造函数。下面的代码会产生编译错误:
struct Dimensions
{
public double Length = 1; //error. Initial Values not allowed
public double Width = 2; //error. Initial Values not allowed
}
#5. 部分类
partial关键字允许把类、结构和结构放在多个文件中。一股青况下,=个类全部驻留在单个文件中。但有时,多个开发人员需要访问同一个类,或者某种类型的代码生成器生成了一个类的某部分,所以把类放在多个文件中是有益的。
partial关键字的用法是:把partial放class、struct或interface关键字前面。在下面的例子中,
TheBigClass类驻留在两个不同的源文件BigClassPart1.cs和BigClassPart2.cs中:
//BigClassPart1.cs
partial class TheBigClass
{
public void MethodOne()
{
}
}
//BigClassPart2.cs
partial class TheBigClass
{
public void MethodTwo()
{
}
}
编译包含这两个源文件的项目时,会创建一个TheBigClass类,它有两个方法MethodOne()和MethodTwo()。
#6. 静态类
如果类只包含静态的方法和属性,该类就是静态的。静态类在功能上与使用私有静态构造函数创建的类相同。不能创建静态类的实例。使用static关键字,编译器可以检查用户是否不经意间给该类添加了实例成员。如果是,就生成一个编译错误。这可以确保不创建静态类的实例。
static class StaticUtilities
{
public static void HelperMethod()
{
}
}
调用HelperMethod不需要StaticUtilities类型的对象。使用类型名即可以进行该调用:
StaticUtilities.HelperMethod();
#7. Object类
所有的.NET类都派生自System.Object。实例上,如果在定义类时没有指定基类,编译器就会自动假定这个类派生自Object。
其实际意义在于,除了自己定义的方法和属性等外,还可以访问为0刂ect定义的许多公有的和受保护的成员方法。这些方法可用于自己定义的所有其他类中。
7.1 System.Object()方法
下面将简要总结每个方法的作用:
- ToString():是获取对象的字符串表示的一种便捷方式。当只需要快速获取对象的内容,以进行调试时,就可以使用这个方法。
- GetHashCode():如果对象放在名为映射(也称为散列表或字典)的数据结构中,就可以使用这个方法。
- Equals()和ReferenceEquals():如果把3个用于比较对象相等性的不同方法组合起来,就说明 .NET Framework在比较相等性方面有相当复杂的模式。
- Finalize():它最接近C++风格的析构函数,在引用对象作为垃圾被回收以清理资源时调用它。Finalize()的Object实现方式实际上什么也没有做,因而被垃圾收集器忽略。如果对象拥有未托管资源的引用,则在该对象被删除时,就需要删除这些引用,此时一般要重写Finalize()。
- GetType():这个方法从System.Type派生的类的一个实例。这个对象可以提供对象成员所属类的更多信息,包括基本类型、方法、属性等。System.Type还提供了 .NET的反射技术的入口点。
- MemberwiseClone():这是System.Object中唯一没有在本书的其他地方详细论述的方法。因为它在概念上相当简单,它只复制对象,并返回对副本的一个引用(对于值类型,就是一个装箱的引用)。
7.2 ToString()方法
ToString()方法是快速获取对象的字符串表示的最快捷的方式。
int i = 50;
string str = i.ToString(); //returns "50"