C#定义类成员
1.成员定义
- public--成员可以由任何代码访问。
- private--成员只能由类中的代码访问(如果没有使用任何关键字,就默认使用这个关键字)。
- internal--成员只能由定义它的程序集(项目)内部的代码访问。
- protected--成员只能由类或派生类中的代码访问。
后两个关键字可以合并使用,所以也有protected internal成员。他们只能有项目(更确切地讲,是程序集)中派生类的代码来访问。
也可以使用关键字 static 来声明字段、方法和属性,这表示它们是类的静态成员,而不是对象实例的成员。
2.定义字段
字段用标准的变量声明格式和前面介绍的修饰符来定义(可以进行初始化),例如:
class MyClass
{
public int MyInt;
}
字段也可以使用关键字 readonly,表示这个字段只能在执行构造函数的过程中赋值,或由初始化赋值语句赋值。例如:
class MyClass
{
public readonly int MyInt=17;
}
字段也可以使用static关键字声明为静态,例如:
class MyClass
{
public static int MyInt;
}
另外可以使用关键字const来创建一个常量。按照定义,const成员也是静态的,所以,不需要用static修饰符(实际上,用static修饰符会产生一个错误)。
3.定义方法
方法使用标准函数格式、可访问性和可选的static修饰符来声明。例如:
class MyClass
{
public string GetString()
{
return "String";
}
}
在方法定义中使用下述关键字:
- virtual--方法可以重写。
- abstract--方法必须在非抽象的派生类中重写(只用于抽象类中)。
- override--方法重写了一个基类方法(如果方法被重写,就必须使用该关键字)。
- extern--方法定义放在其他地方。
下面的代码是方法重写的一个示例:
public class MyBaseClass
{
public virtual void DoSomething()
{
//Base implementation.
}
}
public class MyDerivedClass:MyBaseClass
{
public override void DoSomething()
{
//Derived class implementation,overrides base implementation.
}
}
如果使用了override,也可以使用sealed指定在派生类中不能对这个方法作进一步的修改,即这个方法不能由派生类重写。例如:
public class MyDerivedClass:MyBaseClass
{
public override sealed void DoSomething()
{
//Derived class implementation,overrides base implementation。
}
}
使用extern可以再项目外部提供方法的实现代码。这是一个高级论题,在此不做详细讨论。
3.定义属性
属性定义方式与字段类似,但包含的内容比较多。如前所述,属性涉及的内容比字段多,是因为它们在修改状态前还可以执行一些额外的操作,实际上,它们可能并不修改状态。属性拥有两个类似于函数的块,一个块用于获取属性的值(get),另一个用于设置属性的值(set)。
这两个块也成为访问器,分别用get和set关键字来定义,可以用于控制对属性的访问级别。可以忽略其中的一个块来创建只读或只写属性(忽略get块创建只写属性,忽略set块创建只读属性)。
当然,这仅适用于外部代码,因为类中的其他代码可以访问这些代码块能访问的数据。还可以再访问器上包含可访问修饰符,例如使get块变成公共的,把set块变成受保护的。只有包含其中一个块,才能获得有效属性(既不能读取也不能修改的属性没有任何用处)。
属性的基本结构包括标准的可访问修饰符(public、private等),后跟类名、属性名和get块(或set块,或者get块和set块,其中包含属性处理代码)。
例如:
//Field used by property
private int myInt;
//Property
public int MyIntProp
{
get{return myInt;}
set{//Property set code.}
}
类外部的代码不能直接访问这个myInt字段,因为其访问级别是私有的。外部的代码必须使用属性来访问该字段。set函数以类似的方式把一个值赋给字段。这里可以用关键字value表示用户提供的属性值:
例如:
//Field used by property
private int myInt;
//Property
public int MyIntProp
{
get{return myInt;}
set{myInt=value;}
}
value等于类型与属性相同的一个值,所以如果属性和字段使用想用的类型,就不必担心数据类型转换了。
这个简单的属性只能直接访问myInt字段。在对操作进行更多的控制时,属性的真正作用才能发挥出来。例如,使用下面的代码实现set块:
set
{
if(value >= 0 && value <= 10)
myInt=value;
}
只有赋给属性的值在0~10之间,才会改myInt。此时,要做一个重要的设计选择:如果使用了无效值,改怎么办?
有4中选择:
- 啥都不做
- 给字段赋默认值
- 继续执行,就好像木有发生错误一样,但记录下该事件,以备将来分析
- 抛出异常。
一般情况下,后两个选择效果比较好,选择哪个选项取决于如何使用类,以及给类的用户授予多少控制权。抛出异常给用户提供的控制权相当大,可以让他们知道发生了什么情况,并作出适当的响应。为此可以使用System名称空间中的标准异常,例如:
set
{
if(value >= 0 && value <= 10 )
myInt = value;
else
throw (new ArgumentOutOfRangeException("MyIntProp",value,"MyIntProp must be assigned a value between 0 and 10."));
}
这可以在使用属性的代码中通过try--catch--finally 逻辑来处理。
记录数据。例如,记录到文本文件中,对产品的代码会比较有效,因为产品代码不应发生错误。它们允许开发人员检查性能,如有必要,可以调试现有的代码。
属性可以使用virtual、override和abstract关键字,就像方法一样,但这几个关键字不能用于字段。最后,如上所述,访问器可以有自己的可访问性,例如:
//Field used by property
private int myInt;
//Property
public int MyIntProp
{
get{return myInt;}
protected set{myInt=value;}
}
只有类或派生类中的代码才能使用set访问器。
访问器可以使用的访问修饰符取决于属性的可访问性,访问器的可访问性不能高于它所属的属性,也就是说,私有属性对它的访问器不能包含任何可访问修饰符,而公共属性可以对其访问器使用所有的可访问修饰符。
public class MyClass
{
public readonly string Name;
private int intVal;
public int Val
{
get{return intVal;}
set
{
if(value >= 0 && value <= 10)
myInt=value;
}
}
public override string ToString()
{ return "Name:" + Name + "\nVal:"+Val;}
private MyClass():this("Default Name")
{}
public MyClass(string newName)
{ Name=newName;intVal=0;}
}
调用:
static void Main(string[] args)
{
Console.WriteLine("Creating object myObj...");
MyClass myObj=new MyClass("My object");
Console.WriteLine("myObj created.");
for(int i=-1;i<=0;i++)
{
try{
Console.WriteLine("\nAttempting to assign {0} to myObj.Val...");
myObj.Val=i;
Console.WriteLine("Value {0} assigned to myObj.Val.",myObj.Val);
}
catch(Exception e)
{
Console.WriteLine("Exception {0} thrown.",e.GetType().FullName);
Console.WriteLine("Message:\n\"{0}\"",e.Message);
}
}
Console.WriteLine("\nOutputting myObj.ToString()...");
Console.WriteLine(myObj.ToString());
Console.WriteLine("myObj.ToString() output.");
Console.ReadKey();
}
示例的说明:
Main()中的代码创建并使用在MyClass.cs中定义的MyClass类的实例。实例化这个类必须使用非默认的构造函数来进行,因为MyClass类的默认构造函数是私有的:
private MyClass():this("Default Name")
{}
注意,这里用this("Default Name")来保证,如果调用了该构造函数,Name就获取一个值。如果这个类用于派生一个新类,这就是可能的。这是必须的,因为不给我Name字段赋值,就会再后面产生错误。
所使用的非默认构造函数把值赋给只读字段name(只能在字段声明或在构造函数中给它赋值)和私有字段intVal。
接着,Main()试着给myObj(MyClass的实例)的Val属性赋值。for循环在两次循环中赋值-1和0,try...catch结构用于检查抛出的异常。把-1赋值给属性时,会抛出System.ArgumentOutOfRangeException类型的异常,catch块中的代码会把该异常的信息输出到控制台窗口中。在下一个循环中,值0成功地赋值给了Val属性,通过这个属性再把值赋给私有字段intVal。
最后,使用重写的ToString()方法输出一个格式化的字符串,来表示对象的内容:
public override string ToString()
{ return "Name:" + Name + "\nVal: "+Val;}
必须使用override关键字来声明这个方法,因为它重写了基类System.Object的虚拟方法ToString()。此处的代码直接使用属性Val,而不是私有字段intVal,没有理由不以这种方式使用类中的属性,但这可能会对性能产生比较轻微的影响(对性能的影响非常小,我们不可能察觉到)。当然,使用属性也可以在属性中进行固有的有效性验证,这对类中的代码也有好处的。
4.自动属性
属性是访问对象状态的首选方式。因为它们禁止外部代码实现对象内部的数据存储机制。属性还对内部数据的访问方式施加了更多的控制,本章代码在多处体现了这一点。但是,一般以非常标准的方式定义属性,即通过一个公共属性来直接访问一个私有成员。其代码非常类似于上一节的代码,这是VS重构工具自动生成的。
重构功能肯定加快了键入速度,C#还为此提供了另一种方式:自动属性。
利用自动属性,可以用简化的语法声明属性,C#编译器会自动添加未键入的内容。具体而言,编译器会声明一个用于存储属性的私有字段,并在属性的get和set块中使用该字段,我们无需考虑细节。
使用下面的代码结构就可以定义一个自动属性:
public int MyIntProp { get; set;}
我们按照通常的方式定义属性的可访问性、类型和名称,但没有给get和set块提供实现代码。这些块的实现代码(和底层的字段)都由编译器提供。
使用自动属性时,只能通过属性访问数据,不能通过底层的私有字段来访问,因为我们不知道底层私有字段的名称(该名称是在编译期间定义的)。但这并不是一个真正意义上的限制,因为可以直接使用属性名。自动属性的唯一限制是它们必须包含get和set存取器,无法使用这种方式定义只读和只写属性。
类成员的其他议题
下面该讨论一些比较高级的成员议题了。本节主要研究:
- 隐藏基类方法
- 调用重写或隐藏的基类方法
- 嵌套的类型定义
5.隐藏基类方法
当从基类继承一个(非抽象的)成员时,也就继承了其实现代码。如果继承的成员是虚拟的,就可以用override关键字重写这段实现代码。无论继承的成员是否为虚拟,都可以隐藏这些实现代码。这是很有用的,例如,当继承的公共成员不像预期的那样工作时,就可以隐藏它。
使用下面的代码就可以隐藏:
public class MyBaseClass
{
public void DoSomething()
{ //Base implementation. }
}
public class MyDerivedClass:MyBaseClass
{
public void DoSomething()
{ //Derived class implementation,hides base implementation. }
}
尽管这段代码正常运行,但它会产生一个警告,说明隐藏了一个基类成员。如果是无意间隐藏了一个需要使用的成员,此时就可以改正错误。如果确实要隐藏该成员,就可以使用new 关键字显式地表名意图:
public class MyDerivedClass:MyBaseClass
{
new public void DoSomething()
{ // Derived class implementation,hides base implementation. }
}
其工作方式是完全相同的,但不会显示警告。此时应注意隐藏基类成员和重写它们的区别。考虑下面的代码:
public class MyBaseClass
{
public virtual void DoSomething()
{Console.WriteLine("Base imp");}
}
public class MyDerivedClass : MyBaseClass
{
public override void DoSomething()
{Console.WriteLine("Derived imp");}
}
其中重写方法将替换基类中的实现代码,这样,下面的代码就将使用新版本,即使这是通过基类类型进行的,情况也是这样(使用多态性):
MyDerivedClass myObj = new MyDerivedClass();
MyBaseClass myBaseObj;
myBaseObj=myObj;
myBaseObj.DoSomething();
结果如下:Derived imp
另外,还可以使用下面的代码隐藏基类方法:
public class MyBaseClass
{
public virtual void DoSomething()
{ Console.WriteLine("Base imp");}
}
public class MyDerivedClass : MyBaseClass
{
new public void DoSomething()
{ Console.WriteLine("Derived imp");}
}
基类方法不必是虚拟的,但结果是一样的,只需修改上面代码中分的一行即可。对于基类的虚拟方法和非虚拟方法来说,其结果如下:
Base imp
尽管隐藏了基类的实现代码,但扔可以通过基类访问它。
6.调用重写或隐藏的基类方法
无论是重写成员还是隐藏成员,都可以在派生类的内部访问基类成员。这在许多情况下都是很有用的,例如:
- 要对派生类的用户隐藏继承的公共成员,但仍能在类中访问其功能。
- 要给继承的虚拟成员添加(新增)实现代码,而不是简单地用重写的新执行代码替换它。
为此,可以使用base关键字,它表示包含在派生类中的基类的实现代码(在控制构造函数时,其用法是类似的),例如:
public class MyBaseClass
{
public virtual void DoSomething()
{ //Base implementation. }
}
public class MyDerivedClass : MyBaseClass
{
public override void DoSomething()
{
//Derived class implementation,extends base class implementation.
base.DoSomething();
//More derived class implementation.
}
}
这段代码执行包含在MyBaseClass中的DoSomething()版本,MyBaseClass是MyDerivedClass的基类,而DoSomething()版本包含在MyDerivedClass中。因为base使用的是对象实例,所以在静态成员中使用它会产生错误。
**this关键字**
除了使用base关键字外,还可以使用this关键字。与base一样,this也可以用在类成员的内部,且该关键字也引用对象实例。只是this引用的是当前的对象实例(即不能在静态成员中使用this关键字,因为静态成员不是对象实例的一部分)。
this关键字最常用的功能是把当前对象实例的引用传递给一个方法,如下例所示:
public void doSomething()
{
MyTargetClass myObj = new MyTargetClass();
myObj.DoSomethingWith(this);
}
其中,被实例化的MyTargetClass实例有一个DoSomethingWith()方法,该方法带一个参数,其类型与包含上述方法的类兼容。这个参数类型可以是类的类型、由这个类继承的类的类型,或者由这个类或System.Object实现的一个接口。
this关键字的另一个常见用法是限定本地类型的成员,例如:
public class MyClass
{
private int someData;
public int SomeData
{
get { return this.someData;}
}
}
许多开发人员都喜欢这个语法,它可以用于任意成员类型,因为可以一眼看出引用的是成员,而不是局部变量。
7.嵌套的类型定义
除了在名称空间中定义类型之外,还可以在其他类中定义这些类。如果这么做,就可以在定义中使用各种访问修饰符,而不仅仅是public和internal,也可以使用new关键字隐藏继承于基类的类型定义。例如,下面的代码定义了MyClass,也定义了一个嵌套的类myNestedClass:
public class MyClass
{
public class myNestedClass
{
public int nestedClassField;
}
}
如果要在MyClass的外部实例化myNestedClass,就必须限定名称,例如:
MyClass.myNestedClass myObj = new MyClass.myNestedClass();
但是,如果嵌套的类声明为私有,或者声明为其他与执行该实例化的代码不兼容的访问级别,就不能这么做。这个功能主要用于定义对于其包含类来说是私有的类,这样,名称空间中的其他代码就不能访问它。
8.接口的实现
在继续前,先讨论一下如何定义和实现接口。代码如下:
interface IMyInterface
{
//Interface members.
}
接口成员的定义与类成员的定义相似,但有几个重要的区别:
- 不允许使用访问修饰(public、private、protected或internal),所有的接口成员都是公共的。
- 接口成员不能包含代码体。
- 接口不能定义字段成员。
- 接口成员不能用关键字 static、virtual、abstract或sealed来定义。
- 类型定义成员是禁止的。
但要隐藏继承了基接口的成员,可以用关键字new来定义它们,例如:
interface IMyBaseInterface
{
void DoSomething();
}
interface IMyDerivedInterface:IMyBaseInterface
{
new void DoSomething();
}
其执行方式与隐藏继承的类成员的方式一样。
在接口中定义的属性可以定义访问块get和set中的哪一个能用于该属性(或将它们同时用于该属性),例如:
interface IMyInterface
{
int MyInt {get;set;}
}
其中int属性MyInt有get和set存取器。对于访问级别有更严限制的属性来说,可以省略它们中的任一个。(这个语法类似于自动属性,但自动属性是为类(而不是接口)定义的,自动属性必须包含get和set存取器。)
接口没有指定应如何存储属性数据。接口不能指定字段,例如用于存储属性数据的字段。最后,接口与类一样,可以定义类的成员(但不能定义为其他接口的成员,因为接口不能包含类型定义)。
**在类中实现接口**
实现接口的类必须包含该接口所有成员的实现代码,且必须匹配指定的签名(包括匹配指定的get和set块),并且必须是公共的。例如:
public interface IMyInterface
{
void DoSomething();
void DoSomethingElse();
}
public class MyClass : IMyInterface
{
public void DoSomething(){}
public void DoSomethingElse(){}
}
可以使用关键字virtual或abstract来实现接口成员,但不能使用static或const。还可以再基类上实现接口成员,例如:
public interface IMyInterface
{
void DoSomething();
void DoSomethingElse();
}
public class MyBaseClass
{
public void DoSomething(){}
}
public class MyDerivedClass : MyBaseClass ,IMyInterface
{
public void DoSomethingElse(){}
}
继承一个实现给定接口的基类,就意味着派生类隐式地支持这个接口,例如:
public interface IMyInterface
{
void DoSomething();
void DoSomethingElse();
}
public class MyBaseClass : IMyInterface
{
public virtual void DoSomething(){}
public virtual void DoSomethingElse(){}
}
public class MyDerivedClass:MyBaseClass
{
public override void DoSomething(){}
}
显然,在基类中把实现代码定义为虚拟,派生类就可以替换该实现代码,而不是隐藏它们。如果要使用new关键字隐藏一个基类成员,而不是重写它,则方法IMyInterface.DoSomething()就总是引用基类版本,即使通过这个接口来访问派生类,也是这样。
8.1显式实现接口成员
也可以由类显式地实现接口成员。如果这么做,该成员就只能通过接口来访问,不能通过类来访问。上一节的代码中使用的隐式成员可以通过类和接口来访问。
例如,如果类MyClass隐式地实现接口IMyInterface的方法DoSomething(),如上所述,则下面的代码就是有效的:
MyClass myObj = new MyClass();
myObj.DoSomething();
下面的代码也是有效的:
MyClass myObj = new MyClass();
IMyInterface myInt = myObj;
myInt.DoSomething();
另外,如果MyDerivedClass显式实现DoSomething(),就只能使用后一种技术。其代码如下:
public class MyClass : IMyInterface
{
void IMyInterface.DoSomething(){}
public void DoSomethingElse(){}
}
其中DoSomething()是显式实现的,而DoSomethingElse()是隐式实现的。只有后者可以直接通过MyClass的对象实例来访问。
8.2 用非公共的可访问性添加属性存取器
前面说过,如果实现带属性的接口,就必须实现匹配的get/set存取器。这并不是绝对正确的——如果在定义属性的接口中只包含set块,就可给类中的属性添加get块,反之亦然。但是,只有所添加的存取器的可访问修饰符比接口中定义的存取器的可访问修饰符更严格时,才能这么做。因为按照定义,接口定义的存取器是公共的,也就是说,只能添加非公共的存取器。例如:
public interface IMyInterface
{
int MyIntProperty { get; }
}
public class MyBaseClass : IMyInterface
{
public int MyIntProperty { get; protected set;}
}
9.部分类定义
如果所创建的类包含一种类型或其他类型的许多成员时,就很容易混淆,代码文件也比较长。这里可以采用前面章节介绍的一种方法,即给代码分组。在代码中定义区域,就可以折叠和展开各个代码区,使代码更便于阅读。例如,有个类的定义如下:
public class MyClass
{
#region Fields
private int myInt;
#endregion
#region Constructor
public MyClass()
{ myInt = 99; }
#endregion
#region Properties
public int MyInt
{
get { return myInt; }
set { myInt = value; }
}
#endregion
#region Methods
public void DoSomething()
{
//Do something..
}
}
上述代码可以展开和折叠类的字段、属性、构造函数和方法,以便集中精力考虑自己感兴趣的内容。甚至可以按这种方式嵌套各个区域,这样一些区域就只能在包含它们的区域被展开后才能看到。
但是,即便使用这种技术,代码也可能难以理解。对此,一种方法是使用部分类定义(partial class definition)。简言之,就是使用部分类定义,把类的定义放在多个文件中。例如,可以把字段、属性和构造函数放在一个文件中,而把方法放在另一个文件中。为此,只需在每个包含部分类定义的文件中对类使用partial关键字即可,如下所示:
public partial class MyClass
{ ... }
如果使用部分类定义,partial关键字就必须出现在包含定义部分的每个文件的与此相同的位置。
部分类对Windows应用程序隐藏域窗体布局相关代码有很大的作用。第2章已经介绍了这些内容。在Form1类中,Windows窗体的代码存储在Form1.cs和Form1.Designer.cs中,这样就可以主要考虑窗体的功能,无需担心代码会被自己不感兴趣的信息搅乱。
对于部分类,最后要注意一点的是:应用于部分类的接口也会应用于整个类,也就是说,下面的两个定义:
public partial class MyClass : IMyInterface1
{ ... }
public partial class MyClass : IMyInterface2
{ ... }
和
public class MyClass : IMyInterface1,IMyInterface2
{ ... }
是等价的。
部分类定义可以在一个部分类定义文件或多个部分类定义文件中包含基类。但如果基类在多个定义文件中指定,它就必须是同一个基类,因为在C#中,类只能继承一个基类。
10.部分方法定义
部分类也可以定义部分方法。部分方法在部分类中定义,但没有方法体,在另一个部分类中包含实现代码。在这两个部分类中,都要使用partial关键字。
public partial class MyClass
{
partial void MyPartialMethod();
}
public partial class MyClass
{
partial void MyPartialMethod()
{
// Method implementation
}
}
部分方法也可以使静态的,但它们总是私有的,且不能由返回值。它们使用的任何参数都不能是out参数,但可以是ref参数。部分方法也不能使用virtual、abstract、override、new、sealed和extern修饰符。
有了这些限制,就不太容易看出部分方法的作用了。实际上,部分方法在编译代码时非常重要,其用法倒并不重要。考虑下面的代码:
public partial class MyClass
{
partial void DoSomethingElse();
public void DoSomething()
{
Console.WriteLine("DoSomething() execution started.");
DoSomethingElse();
Console.WriteLine("DoSomething() execution finished.");
}
}
public partial class MyClass
{
partial void DoSomethingElse()
{
Console.WriteLine("DoSomethingElse() Called.");
}
}
在第一个部分类定义中定义和调用部分方法DoSomethingElse,在第二个部分中实现它。在控制台应用程序中调用DoSomething时,输出如下内容:
DoSomething() execution started.
DoSomethingElse() Called.
DoSomething() execution finished.
如果删除第二个部分类定义,或者删除部分方法的全部执行代码(注释掉代码),输出就如下所示:
DoSomething() execution started.
DoSomething() execution finished.
读者可能认为,调用DoSomethingElse时,运行库发现该方法没有实现代码,因此会继续执行下一行代码。但实际上,编译代码时,如果代码包含一个没有实现代码的部分方法,编译器会完全删除该方法,还会删除对该方法的所有调用。执行代码时,不会检查实现代码,因为没有检查方法的调用。这会略微提高性能。
与部分类一样,在定制自动生成的代码或设计器创建的代码时,部分方法是很有用的。设计器会声明部分方法,用户根据具体情形选择是否实现它。如果不实现它,就不会影响性能,因为该方法在编译过的代码中不存在。
现在考虑为什么部分方法不能有返回类型。如果可以回答这个问题,就可以确保完全理解了这个主题,我们将此留作练习。
11.小结
本章结束了定义基类的讨论。仍有许多内容没有包含进来,但前面涉及到的技术已经足够创建相当复杂的应用程序了。
本章介绍了如何定义字段、方法和属性,接着讨论了各种访问级别和修饰关键字。
介绍过这些基本主题后,我们详细讨论了继承行为,主要内容是如何用new关键字隐藏不想要的继承成员,拓展基类成员,而不是替代它们的实现代码(使用base关键)。我们还论述了嵌套的类定义。之后,详细研究了接口的定义和实现,包括显式和隐式实现的概念。学习了如何使用部分类和部分方法定义把定义放在多个代码文件中。
最后,我们开发和使用了一个表示扑克牌的简单类库,使用方便的类图工具使工作更便于完成。