开发工具与关键技术:对象和类型
作者:邓崇富
撰写时间:2019 年6 月 30 日
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
类和结构:
类和结构实际上都是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法。类定义了类的每个对象(称为实例)可以包含什么数据和功能。例如,如果一个类表示一个顾客,就可以定义字段CustomerID、Name和Address,以包含顾客的信息。还可以定义处理在这些字段中存储的数据的功能。接着,就可以实例化类的一个对象,来表示某个顾客,为这个实例设置相关字段的值,并使用其功能。
public class PhoneCustomer
{
public const string DayOfSendBill = "Monday";
public int CustomerID;
public string Name;
}
解构不同于类,因为它们不需要在堆上分配空间(类是引用类型,总是存储在堆(heap)上),而结构是值类型,通常存储在栈(stack)上,另外,结构不支持继承。
较小的数据类型使用结构可提高性能。但在语法上,结构与类非常相似,主要的区别是使用关键字struct代替class来声明的结构。例如,如果希望所有的PhoneCustomer实例都分布在栈上,而不是分布在托管堆上,就可以编写成下面的语句:
public struct PhoneCustomerStruct
{
public const string DayOfSendBill = "Monday";
public int CustomerID;
public string Name;
}
对于类和结构,都使用关键字new来声明实例:这个关键字创建对象并对其进行初始化。在下面的例子中,类和结构的字段值都默认为0:
//对象实例化
var myCustomer = new PhoneCustomer();
//结构实例化
var myCustomer2 = new PhoneCustomerStruct();
在大多数情况下,类要比结构常用得多。因此先指出类和结构得区别,以及选择使用机构而不使用类得特殊原因,但除非特别说明,否则就可以假定用于类的代码也适用于结构。
类:
类包含成员,成员可以是静态或实例成员。静态成员属于类;实例成员属于对象。静态字段的值对每个对象都是相同的,而每个对象的实例字段都可以有不同的值。静态成员关联了static修饰符。下面详细说说类的成员:
字段:
字段是与类相关的变量。前面的例子已经使用了PhoneCustomer类中的字段。一旦实例化PhoneCustomer对象,就可以使用语法Object.FieldName来访问这些字段,如下例所示:
var myCustomer = new PhoneCustomer();
myCustomer.CustomerID = 1234;
myCustomer.Name = "小红";
常量与类的关联方式和变量与类的关联方式相同。使用const关键字来声明常量。如果把它声明为public,就可以在类中的外部访问它
public class PhoneCustomer
{
public const string DayOfSendBill = "Monday";
public int CustomerID;
public string Name;
}
最好不把字段声明为public。如果修改类的公共成员,使用这个公共成员的每个调用程序也需要更改。例如。如果希望在下一个版本中检查最大的字符串长度,公共字段就需要更改为一个属性。使用公共字段的现有代码,必须重新编译,才能使用这个属性(尽管在1调用程序看来,语法与属性相同)。如果只在现有的属性中改变检查,那么调用程序不需要重新编译就能使用新版本。最好把字段声明为private,使用属性来访问字段。
属性:
属性(property)的概念是:它是一个方法或一对方法,在客户端代码看来,是一个字段。下面把前面示例中变量名为_firstName的名字字段改为私有。FirstName属性包含get和set访问器,来检索和设置支持字段的值:
public class PhoneCustomer
{
private string _Name;
public string Name
{
get { return _Name; }
set { Name = value; }
}
}
Get访问器不带任何参数,且必须返回属性声明的类型。也不应为set访问器指定任何显式参数,但编译器假定它带一个参数,其类型也与属性相同,并表示为value。
下面的示例使用另一个命名约定。下面的代码包含一个属性Age,它设置了一个字段age。在这个例子中,age表示Age的后备变量。
private int age;
public int Age
{
get { return age; }
set { age = value; }
}
自动实现的属性:
如果属性的set和get访问器中没有任何逻辑,就可以使用自动实现的属性。这种属性会自动实现后备成员变量。前面Age示例的代码如下:
public int Age { get; set; }
不需要声明私有字段。编译器会自动创建它。使用自动实现的属性,就不能直接访问字段,因为不知道编译器生成的名称。
使用自动实现的属性,就不能在属性设置中验证属性的有效性,所以在上面的例子中,不能检查是否设置了无效的年龄。
自动实现的属性可以使用属性初始化器来初始化:
public int Age { get; set; } = 43;
属性的访问修饰符:
C#允许给属性的get和set访问器设置不同的访问修饰符,所以属性可以有公有的get访问器和私有的或受保护的set访问器。这有助于控制属性的设置方式或时间。在下面的代码示例中,注意set访问器有一个私有访问修饰符,而get访问器没有任何访问修饰符。这表示get访问器具有属性的访问及别。在get和set访问器中,必须有一个具备属性的访问级别。如果get访问器的访问级别是protected,就会产生一个编译错误,因为这会使两个访问器的访问级别都不是属性。
public class Class2
{
private string _name;
public string Name
{
get
{
return _name;
}
private set
{
_name = value;
}
}
}
方法:
注意,正式的C#术语区分函数和方法。在C#术语中,“函数成员”不仅包含方法,也包含类或结构的一些非数据成员,如索引器、运算符、构造函数和析构函数等,甚至还有属性。这些都不是数据成员,字段、常量和事件才是数据成员。
方法的声明,在C#中,方法的定义包括任意方法修饰符(如方法的可访问性)、返回值的类型,然后依次是方法名、输入参数的列表(用圆括号括起来)和方法体(用花括号括起来)。
每个参数都包括参数的类型名和在方法体中的引用名称。但如果方法有返回值,则return语句就必须与返回值一起使用,以指定出口点,例如:
public bool IsSquare(Rectangle rect)
{
return (rect.Height == rect.width);
}
如果没有返回值,就把返回类型指定为void,因为不能省略返回类型。如果方法不带参数,仍需要在方法名的后面包含一对空的圆括号()。此时return语句就是可选的,当到达右花括号时,方法会自动返回。
调用方法:
在下面的例子中,说明了类的定义和实例化、方法的定义和调用的语法。类Math定义了静态成员和实例化成员:
public class Math
{
public int Value { get; set;}
public int GetSquare() => Value * Value;
public static int GetSquareOf(int x) => x * x;
public static double GetPi() => 3.14159;
}
Program类利用Math类,调用静态方法,实例化一个对象,来调用实例成员:
using static System.Console;
namespace MathSample
{
class Program
{
static void Main()
{
//Try calling some static functions.
WriteLine($"Pi is {Math.GetPi()}");
int x = Math.GetSquareOf(5);
WriteLine($"Square of 5 is {x}");
var math = new Math();
math.Value = 30;
WriteLine($"Value field of math variable contains {math.Value}");
WriteLine($"Square of 30 is {math.GetSquare()}");
}
}
}
运行MathSample示例,结果如下
Pi is 3.14159
Square of 5 is 25
Value field of math variable contains 30
Square of 30 is 900