首页 > 代码库 > C#图解教程 第六章 深入理解类

C#图解教程 第六章 深入理解类

深入理解类
类成员
成员修饰符的顺序
实例类成员
静态字段
从类的外部访问静态成员
静态字段示例
静态成员的生存期
静态函数成员
其他静态类成员类型
成员常量
常量与静态量
属性
属性声明和访问器
属性示例
使用属性
属性和关联字段
执行其他计算
只读和只写属性
属性与公共字段
计算只读属性示例
自动实现属性
静态属性
实例构造函数
带参数的构造函数
默认构造函数
静态构造函数
对象初始化语句
析构函数
readonly修饰符
this关键字
索引器
什么是索引器
索引器和属性
声明索引器
索引器的set访问器
索引器的get访问器
关于索引器的补充
为Employee示例声明索引器
另一个索引器示例
索引器重载
访问器的访问修饰符
分部类和分部类型
分部方法

深入理解类

类成员


前两章阐述了9种类成员中的两种:字段和方法。本章将会介绍除事件(第14章)和运算符外的其他类成员,并讨论其特征。

技术分享

技术分享

成员修饰符的顺序


字段和方法的声明可以包括许多如public、private这样的修饰符。本章还会讨论许多其他修饰符。多个修饰符一起使用时,它们需要怎么排序呢?

[特性] [修饰符] 核心声明

  • 修饰符
    • 修饰符,必须放在核心声明前
    • 多个修饰符顺序任意
  • 特性
    • 特性,必须放在修饰符和核心声明前
    • 多个特性顺序任意

例如,public和static都是修饰符,可以用在一起修饰某个声明。因为它们都是修饰符,所以顺序任意。下面两行代码是语义等价的:

public static int MaxVal;
static public int MaxVal;

技术分享

技术分享

实例类成员


类的每个实例拥有自己的各个类成员的副本,这些成员称为实例成员
改变一个实例字段的值不会影响任何其他实例成员中的值。

class D
{
    public int Mem1;
}
class Progarm
{
    static void Main()
    {
        D d1=new D();
        D d2=new D();
        d1.Mem1=10;
        d2.Mem1=28;
        Console.WriteLine("d1={0},d2={1}",d1.Mem1,d2.Mem2);
    }
}

技术分享

技术分享

技术分享

技术分享

静态字段


除了实例字段,类还可以拥有静态字段

  • 静态字段被类的所有实例共享,所有实例访问同一内存位置。因此,如果该内存位置的值被一个实例改变了,这种改变对所有实例都可见。
  • 可以使用static修饰符将字段声明为静态
class D
{
    int Mem1;        //实例字段
    static int Mem2; //静态字段
}

例:静态字段演示

  • 因为Mem2是静态的,类D的两个实例共享单一的Mem2字段。如果Mem2被改变了,这个改变在两个实例中都能看到
  • 成员Mem1没有声明为static,所以每个实例都有自己的副本
class D
{
    int Mem1;
    static int Mem2;
    ...
}
static void Main()
{
    D d1=new D();
    D d2=new D();
    ...
}

技术分享

技术分享

从类的外部访问静态成员


静态成员可以使用点运算符从类的外部访问。但因为没有实例,所以必须使用类名。

类名
 ↓
D.Mem2=5;
   ↑
  成员名
静态字段示例
  • 一个方法设置两个数据成员的值
  • 另一个方法显示两个数据成员的值
class D
{
    int Mem1;
    static int Mem2;
    public void SetVars(int v1,int v2)
    {
        Mem1=v1;
        Mem2=v2;
    }
    public void Display(string str)
    {
        Console.WriteLine("{0}:Mem1={1},Mem2={2}",str,Mem1,Mem2);
    }
}
class Program
{
    static void Main()
    {
        D d1=new D(),d2=new D();
        d1.SetVars(2,4);
        d1.Display("d1");
        d2.SetVars(15,17);
        d2.Display("d2");
        d1.Display("d1");
    }
}

技术分享

技术分享

静态成员的生存期
  • 之前我们已经看到了,只有在实例创建后才产生实例成员,实例销毁后实例成员也就不在存在
  • 即使类没有实例,也存在静态成员,并可以访问
class D
{
    int Mem1;
    static int Mem2;
    ...
}
static void Main()
{
    D.Mem2=5;
    Console.WriteLine("Mem2={0}",D.Mem2);
}

技术分享

技术分享
字段与类有关,与实例无关

技术分享
技术分享

静态成员即使没有类的实例也存在。如果静态字段有初始化语句,那么会在使用该类的任何静态成员之前初始化该字段,但没必要在程序执行的开始就初始化。

静态函数成员


  • 如同静态字段,静态函数成员独立于任何类实例。无需类实例就可以调用静态方法
  • 静态函数成员不能访问实例成员。只能访问静态成员

例:静态函数与静态字段

class X
{
    static public int A;
    static public void PrintValA()
    {
        Console.WriteLine("Value of A:{0}",A);
    }
}
class Program
{
    static void Main()
    {
        X.A=10;
        X.PrintValA();
    }
}

技术分享

技术分享

技术分享

技术分享

其他静态类成员类型


下表中为可以声明为static的类成员类型做了√标记

技术分享
技术分享

成员常量


成员常量类似本地常量,只是它被声明在类声明中而不是方法内。

class MyClass
{
    const int IntVal=100;//定义值为100的int类型常量
}
const double PI=3.1416; //错误:不能在类型声明之外

与本地常量类似,初始化成员常量的值在编译时必须是可计算的。

class MyClass
{
    const int IntVal1=100;
    const int IntVal2=2*IntVal1;//正确:因为IntVal的值在前一行已设置
}

与本地常量类似,不能在成员常量声明后给它赋值

class MyClass
{
    const int IntVal//错误:声明时必须初始化
    IntVal=100;     //错误:不允许赋值
}

与C和C++不同,在C#中没有全局变量。每个常量都必须声明在类型内。

常量与静态量


成员常量比本地常量更有趣,因为它们表现得像静态置。它们对类的每个实例都是“可见的”,而且即使没有类的实例也可以用。与真正的静态量不同,常量没有自己的存储位置,而是在编译时被编译器替换。常量类似C和C++中的#define。
正因为常量在内存没有存储位置,所以它也不能作为左值(被赋值)。
static静态量是有自己的存储位置的。

例:常量示例

class X
{
    public const double PI=3.1416;
}
class Program
{
    static void Main()
    {
        Console.WriteLine("pi={0}",X.PI);
    }
}

技术分享

技术分享

 

虽然常量成员表现得像一个静态量,但不能将常量声明为static

static const double PI=3.14;//错误:不能将常量声明为static

属性


属性代表类的实例或类中的一个数据项成员。使用属性看起来像写入后读取一个字段,它们语法相同。

与字段类似,属性有如下特征

  • 它是命名的类成员
  • 它有类型
  • 它可以被赋值和读取

然而和字段不同,属性是一个函数成员

  • 它不为数据存储分配内存
  • 它是执行代码

属性是指定的一组两个匹配的、称为访问器的方法

  • set访问器为属性赋值
  • get访问器从属性获取
属性声明和访问器
  • set访问器
    • 拥有一个单独的、隐式的值参,名臣为value,与属性的类型相同
    • 拥有一个返回类型void
  • get访问器
    • 没有参数
    • 拥有一个与属性类型相同的返回类型

技术分享

技术分享

访问器的其他重点

  • get访问器的所有执行路径必须包含一条return语句,返回一个属性类型的值
  • 访问器set、get顺序任意,除这两个访问器外在属性上不允许有其他方法
属性示例

例:名为C1的类,包含一个名为MyValue的属性

  • 属性本身没有任何存储。访问器决定如何处理发进来的数据,以及什么数据发出去。示例中,属性使用TheRealValue字段作为存储。
  • set访问器接受它的输入参数value,并把值赋给字段TheRealValue
  • get访问器只是返回字段TheRealValue的值
class C1
{
    private int TheRealValue;//字段:分配内存
    public int MyValue       //属性:不分配内存
    {
        set
        {
            TheRealValue=value;
        }
        get
        {
            return TheRealValue;
        }
    }
}

技术分享

技术分享

使用属性
  • 要写入属性,在赋值语句的左边使用属性名
  • 要读取属性,把属性名用在表达式中
  • 属性根据是写入还是读取,隐式调用访问器。(不能显示调用访问器)
int MyValue
{
    get{...}
    set{...}
}
...
MyValue=5;
z=MyValue;
属性和关联字段

正如上文C1示例中的。一种常见的方式是在类中将字段声明为private以封装字段,并声明一个public属性来控制从类的外部对该字段的访问。
和属性关联的字段常被称为后备字段、后背存储。

例:使用public的MyValue来控制对private的TheRealValue的访问

class C1
{
    private int TheRealValue=http://www.mamicode.com/10;//字段:分配内存
    public int MyValue          //属性:不分配内存
    {
        set{TheRealValue=http://www.mamicode.com/value;}
        get{return TheRealValue;}
    }
}
class Program
{
    static void Main()
    {
        C1 c=new C1();
        Console.WriteLine("MyValue:{0}",c.MyValue);
        c.MyValue=20;
        Console.WriteLine("MyValue:{0}",c.MyValue);
    }
}

属性和它后备字段的命名有两种约定。
约定一:属性使用Pascal大小写,字段使用Camel大小写。虽然这违反了“仅使用大小写区分不同标识符是坏习惯”。但胜在简单,有意义。
约定二:属性使用Pascal大小写,字段使用Camel大小写并在开头加"_"号。

private int firstField;
public int FirstField
{
    get{return firstField;}
    set{firstField=value;}
}
private int _secondField;
public int SecondField
{
    get{return _secondField;}
    set{_secondField=value;}
}
执行其他计算

属性访问器不仅对后备字段传进传出数据。也可以执行任何计算。
例:通过set属性访问器限制Hour的最大值为24

int Hour=12;
int MyValue
{
    set
    {
        Hour=value>24?24:value;
    }
    get
    {
        return Hour;
    }
}

上面示例中,演示的条件运算符,将在第8章详细阐述。
条件运算符是一种三元运算符,计算问号前的表达式,如果表达式结果为true,则返回问号后第一个表达式,否则,返回冒号后的表达式。

只读和只写属性
  • 只有get访问器的属性称为只读属性。只读属性是一种安全的,把一项数据从类或类的实例中传出,而不允许太多访问方法
  • 只有set访问器的属性称为只写属性,只写属性是一种安全的,把一项数据从类的外部传入类,而不允许太多访问方法
  • 两个访问器中至少要定义一个

技术分享

技术分享

属性与公共字段

按照推荐的编码实践,属性比公共字段更好

  • 属性是函数型成员而不是数据成员,允许你处理输入和输出,而公共字段不行
  • 属性可以只读或只写,字段不行
  • 编译后的变量和编译后的属性语义不同
计算只读属性示例

例:类RightTriangle(直角三角形)的只读属性Hypotenuse(斜边)

  • 它有两个公有字段,表示直角三角形的两个直角边长度。这些字段可以被写入、读取
  • 第三边由属性Hypotenuse表示,是只读属性,其返回值基于另外两边长度
class RightTriangle
{
    public double A=3;
    public double B=4;
    public double Hypotenuse
    {
        get{return Math.Sqrt((A*A)+(B*B));}
    }
}
class Program
{
    static void Main()
    {
        var c=new RightTriangle();
        Console.WriteLine("Hypotenuse:{0}",c.Hypotenuse);
    }
}

技术分享

技术分享

自动实现属性

因为属性经常关联到后备字段,C#提供了自动实现属性(automatically implemented property),允许只声明属性而不声明后备字段。编译器为你创建隐藏的后备字段,并且字段挂接到get和set访问器上。
自动属性的要点如下

  • 不声明后备字段-编译器根据属性类型分配存储
  • 不能提供访问器的方法体-它们必须被简单地声明为分好。get担当于简单的内存读,set担当简单的写
  • 除非通过访问器,否则不能访问后备字段。因为不能用其他方法访问,所以实现只读和只写属性没有意义,因此使用自动属性必须同时提供读写访问器。

例:自动属性

class C1
{
    public int MyValue          //属性:分配内存
    {
        set;get;
    }
}
class Program
{
    static void Main()
    {
        C1 c=new C1();
        Console.WriteLine("MyValue:{0}",c.MyValue);
        c.MyValue=20;
        Console.WriteLine("MyValue:{0}",c.MyValue);
    }
}

技术分享

技术分享

除方便以外,自动属性使你在倾向于使用公有字段的地方很容易用属性将其替代。

静态属性

属性也可以声明为static。静态属性的访问器和静态成员一样,具有以下特点

  • 不能访问类的实例成员–它们能被实例成员访问
  • 不管类是否有实例,它们都存在
  • 当从类的外部访问时,必需使用类名引用

例:静态属性

class Trivial
{
    public static int MyValue{get;set;}
    public void PrintValue()
    {
        Console.WriteLine("Value from inside:{0}",MyValue);
    }
}
class Program
{
    static void Main()
    {
        Console.WriteLine("Init Value:{0}",Trival.MyValue);
        Trival.MyValue=10;
        Console.WriteLine("New Value:{0}",Trival.MyValue);
        var tr=new Trivial();
        tr.PrintValue();
    }
}

技术分享

技术分享

实例构造函数


实例构造函数是一个特殊的方法,它在创建类的每个新实例时执行。

  • 构造函数用于初始化类实例的状态
  • 如果希望从类的外部创建类的实例,需要将构造函数声明为public
class MyClass
{         和类名相同
             ↓
    public MyClass()
    {     ↑
       没有返回类型
        ...
    }
}
  • 构造函数的名称与类相同
  • 构造函数不能有返回值

例:使用构造函数初始化TimeOfInstantiation字段为当前时间

class MyClass
{
    DateTime TimeOfInstantiation;
    ...
    public MyClass()
    {
        TimeOfInstantiation=DateTime.Now;
    }
    ...
}

在学完静态属性后,我们可以仔细看看初始化TimeOfInstantiation那一行。DateTime类(实际上它是一个结构,但由于还没介绍结构,你可以先把它当成类)是从BCL中引入的,Now是类DateTIme的静态属性。Now属性创建一个新的DateTIme类实例,将其初始化为系统时钟中的当前日期和时间,并返回新DateTime实例的引用。

带参数的构造函数
  • 构造函数可以带参数。参数语法和其他方法完全相同
  • 构造函数可以被重载

例:有3个构造函数的Class

class Class1
{
    int Id;
    string Name;
    public Class1(){Id=28;Name="Nemo";}
    public Class1(int val){Id=val;Name="Nemo";}
    public Class1(String name){Name=name;}
    public void SoundOff()
    {
        Console.WriteLine("Name{0},Id{1}",Name,Id);
    }
}
class Program
{
    static void Main()
    {
        CLass1 a=new Class1(),
               b=new Class1(7),
               c=new Class1("Bill");
        a.SoundOff();
        b.SoundOff();
        c.SoundOff();
    }
}

技术分享

技术分享

默认构造函数

如果在类的声明中没有显式的提供实例构造函数,那么编译器会提供一个隐式的默认构造函数,它有以下特征。

  • 没有参数
  • 方法体为空

只要你声明了构造函数,编译器就不再提供默认构造函数。

例:显式声明了两个构造函数的Class2

class Class2
{
    public Class2(int Value){...}
    public Class2(string Value){...}
}
class Program
{
    static void Main()
    {
        Class2 a=new Class2();//错误!没有无参数的构造函数
        ...
    }
}
  • 因为已经声明了构造函数,所以编译器不提供无参数的默认构造函数
  • 在Main中试图使用无参数的构造函数创建实例,编译器产生一条错误信息

静态构造函数


实例构造函数初始化类的每个新实例,static构造函数初始化类级别的项。通常,静态构造函数初始化类的静态字段。

  • 初始化类级别的项
    • 在引用变任何静态成员之前
    • 在创建类的任何实例之前
  • 静态构造函数在以下方面与实例构造函数类似
    • 静态构造函数的名称和类名相同
    • 构造函数不能返回值
  • 静态构造函数在以下方面和实例构造函数不同
    • 静态构造函数声明中使用static
    • 类只能有一个静态构造函数,而且不能带参数
    • 静态构造函数不能有访问修饰符
class Class1
{
    static Class1
    {
        ...
    }
}

关于静态构造函数还有其他要点

  • 类既可以有静态构造函数也可以有实例构造函数
  • 如同静态方法,静态构造函数不能访问类的实例成员,因次也不能是哟个this访问器
  • 不能从程序中显式调用静态构造函数,系统会自动调用它们,在:
    • 类的任何实例被创建前
    • 类的任何静态成员被引用前

静态构造函数示例

class RandomNumberClass
{
    private static Random RandomKey;
    static RandomNumberClass()
    {
        RandomKey=new Random();
    }
    public int GetRandomNumber()
    {
        return RandomKey.Next();
    }
}
class Program
{
    static void Main()
    {
        var a=new RandomNumberClass();
        var b=new RandomNumberClass();
        Console.WriteLine("Next Random #:{0}",a.GetRandomNumber());
        Console.WriteLine("Next Random #:{0}",b.GetRandomNumber());
    }
}

技术分享

技术分享

对象初始化语句


对象初始化语句扩展了创建语法,允许你在创建新的对象实例时,设置字段和属性的值。

技术分享
技术分享

例:

new Point {X=5,Y=6};
  • 创建对象的代码必须能够访问初始化的字段和属性。如上例中,X和Y必须是public
  • 初始化发生在构造方法执行之后,因为构造方法中设置的值可能会在对象初始化中重置为不同的值
public class Point
{
    public int X=1;
    public int Y=2;
}
class Program
{
    static void Main()
    {
        var pt1=new Point();
        var pt2=new Point(X=5,Y=6);
        Console.WriteLine("pt1:{0},{1}",pt1.X,pt1.Y);
        Console.WriteLine("pt2:{0},{1}",pt2.X,pt2.Y);
    }
}

技术分享

技术分享

析构函数


析构函数(destructor)执行在类的实例被销毁前需要的清理或释放非托管资源行为。非托管资源通过Win32 API获得文件句柄,或非托管内存块。使用.NET资源无法得到它们,因此如果坚持使用.NET类,就无需为类编写析构函数。
因此,我们等到第25章再描述析构函数。

readonly修饰符


字段可用readonly修饰。其作用类似于将字段声明为const,一旦值被设定就不能改变。

  • const字段只能在字段声明语句中初始化,而readonly字段可以在下列任意位置设置它的值
    • 字段声明语句,类似const
    • 类的任何构造函数。如果是static字段,初始化必须在静态构造函数中完成
  • const字段的值必须在编译时决定,而readonly字段值可以在运行时决定。这种增加的自由性允许你在不同环境或构造函数中设置不同的值
  • 和const不同,const的行为总是静态的,而readonly字段有以下两点
    • 它可以是实例字段,也可以是静态字段
    • 它在内存中有存储位置

例:Shape类,两个readonly字段

  • 字段PI在它的声明中初始化
  • 字段NumberOfSides根据调用的构造函数被设置为3或4
class Shape
{
    readonly double PI=3.1416;
    readonly int NumberOfSides;
    public Shape(double side1,double side2)
    {
        // 矩形
        NumberOfSides=4;
        ...
    }
    public Shape(double side1,double side2,double side3)
    {
        // 三角形
        NumberOfSides=3;
        ...
    }
}

this关键字


this关键字在类中使用,表示对当前实例的引用。它只能被用在下列类成员的代码块中。

  • 实例构造函数
  • 实例方法
  • 属性和索引器的实例访问器

静态成员不是实例的一部分,所以不能在静态函数成员中使用this。换句话说,this用于下列目的:

  • 用于区分类的成员和本地变量或参数
  • 作为调用方法的实参

例:MyClass类,在方法内使用this关键字区分两个Var1

class MyClass
{
    int Var1=10;
    public int ReturnMaxSum(int Var1)
    {          参数  字段
                 ↓    ↓
        return Var1>this.Var1?Var1:this.Var1;
    }
}
class Program
{
    static void Main()
    {
        var mc=new MyClass();
        Console.WriteLine("Max:{0}",mc.ReturnMaxSum(30));
        Console.WriteLine("Max:{0}",mc.ReturnMaxSum(5));
    }
}

技术分享

技术分享

索引器


加入我们定义一个Employee类,它带有3个string型字段,如果不用索引器,我们用字段名访问它们。

class Employee
{
    public string LastName;
    public string FirstName;
    public string CityOfBirth;
}
class Program
{
    static void Main()
    {
        var emp1=new Employee();
        emp1.LaseName="Doe";
        emp1.FirstName="Jane";
        emp1.CityOfBirth="Dallas";
    }
}

技术分享

技术分享

如果能使用索引访问它们将会很方便,好像该实例是字段的数组一样。

    static void Main()
    {
        var emp1=new Employee();
        emp1[0]="Doe";
        emp1[1]="Jane";
        emp1[2]="Dallas";
    }

技术分享

技术分享

什么是索引器

索引器是一组get和set访问器,与属性类似。

索引器和属性

索引器和属性在很多方法类似

  • 和属性一样,索引器不用分配内存来存储
  • 索引器通常表示多个数据成员

可以认为索引器是为类的多个数据成员提供get、set属性。通过索引器,可以在许多可能的数据成员中进行选择。索引器本身可以是任何类型。

关于索引器的注意事项

  • 和属性一样,索引器可以只有一个访问器,也可以两个都有
  • 索引器总是实例成员,因此不能声明为static
  • 和属性一样,实现get、set访问器的代码不必一定关联到某字段或属性。这段代码可以什么都不做,只要get访问器返回某个指定类型值即可
声明索引器
  • 索引器没有名称。在名称的位置,关键词是this
  • 参数列表在方括号中
  • 参数列表中至少声明一个参数
Return Type this [Type param1,...]
{
    get{...}
    set{...}
}

声明索引器类似于声明属性。

技术分享
技术分享

索引器的set访问器

当索引器被用于赋值时,set访问器被调用,并接受两项数据

  • 一个隐式参数,名为value,value持有要保存的数据
  • 一个或多个索引参数,表示数据应该保存在哪里

下图例表明set访问器有如下语义

  • 它的返回类型为void
  • 它使用的参数列表和索引器声明中的相同
  • 它有一个名为value的隐式参数,值参类型和索引类型相同

技术分享

技术分享

索引器的get访问器

get访问器方法体内的代码必须检查索引参数,确定它表示哪个字段,并返回字段值。
get访问器有如下语义

  • 它的参数列表和索引器声明中的相同
  • 它返回与索引器相同类型的值

技术分享

技术分享

关于索引器的补充

和属性一样,不能显示调用get、set访问器。取而代之,当索引器用在表达式中取值时,将自动调用get访问器。索引器被赋值时,自动调用set访问器。
在“调用”索引器时,要在方括号中提供参数。

   索引   值
    ↓    ↓
emp[0]="Doe";           //调用set访问器
string NewName=emp[0];  //调用get访问器
为Employee示例声明索引器

下面代码为示例中的类Employee声明了一个索引器

  • 索引器需要去写string类型的值,所以string必须声明为索引器的类型。它必须声明为public,以便从类外部访问
  • 3个字段被强心索引为整数0-2,所以本例中方括号中间名为index的形参必须为int型
  • 在set访问器方法体内,代码确定索引指的是哪个字段,并把隐式变量value赋给它。在get访问器方法体内,代码确定索引指的哪个字段,并返回该字段的值
class Employee
{
    public string LastName;
    public string FirstName;
    public string CityOfBirth;
    public string this[int index]
    {
        set
        {
            switch(index)
            {
                case 0:LaseName=value;
                    break;
                case 1:FirstName=value;
                    break;
                case 2:CityOfBirth=value;
                    break;
                default:
                    throw new ArgumentOutOfRangeException("index");
            }
        }
        get
        {
            switch(index)
            {
                case 0:return LaseName;
                case 1:return FirstName;
                case 2:return CityOfBirth;
                default:throw new ArgumentOutOfRangeException("index");
            }
        }
    }
}
另一个索引器示例

例:为类Class1的两个int字段设置索引

class Class1
{
    int Temp0;
    int Temp1;
    public int this[int index]
    {
        get
        {
            return(index==0?Temp0:Temp1;)
        }
        set
        {
            if(index==0){Temp0=value;}
            else{Temp1=value;}
        }
    }
}
class Example
{
    static void Main()
    {
        var a=new Class1();
        Console.WriteLine("Values -- T0:{0},T1:{1}",a[0],a[1]);
        a[0]=15;
        a[1]=20;
        Console.WriteLine("Values--T0:{0},T1:{1}",a[0],a[1]);
    }
}

技术分享

技术分享

索引器重载

类可以有任意多个参数列表不同的索引器。(返回类型不同,不是重载)

例:下面示例有3个索引器

class Myclass
{
    public string this[int index]
    {
        get{...}
        set{...}
    }
    public string this[int index1,int index2]
    {
        get{...}
        set{...}
    }
    public int this[float index1]
    {
        get{...}
        set{...}
    }
}

访问器的访问修饰符


本章中,你已看到了两种带get、set访问器的函数成员:属性和索引器。默认情况下,成员的两个访问器的访问级别和成员自身相同。也就是说,如果一个属性有public访问级别,那么它的两个访问器也是public的。

不过,你可以为两个访问器分配不同访问级别。例如,下面代码演示了一个常见且重要的例子–set访问器声明为private,get访问器声明为public。(get之所以是public,是因为属性的访问级别就是public)

注意:在这段代码中,尽管可以从类的外部读取该属性,但却只能在类的内部设置它。这是非常重要的封装工具。

class Person
{
    public string Name{get;private set;}
    public Person(string name)
    {
        Name=name;
    }
}
class Program
{
    static public void Main()
    {
        var p=new Person("Capt,Ernest Evans");
        Console.WriteLine("Person‘s name is {0}",p.Name);
    }
}

访问器的访问修饰符有几个限制。最重要的限制如下。

  • 仅当成员(属性或索引器)既有get访问器也有set访问器时,其访问器才能有访问修饰符
  • 虽然两个访问器都必须出现,但它们中只能有一个有访问修饰符
  • 访问器的访问修饰符必须比成员的访问级别有更严格的限制性,即访问器的访问级别必须比成员的访问级别低,详见下图

技术分享

技术分享

例如,如果一个属性的访问级别是public,在图里较低的4个级别中,它的访问器可以使用任意一个。但如果属性的访问级别是protected,则其访问器唯一能使用的访问修饰符是private。

分部类和分部类型


类的声明可以分割成几个分部类的声明

  • 每个分部类的声明都含有一些类成员的声明
  • 类的分部类声明可以在同一文件中也可以在不同文件中

每个局部声明必须标为partial class,而不是class。分部类声明看起来和普通类声明相同。

类型修饰符partial不是关键字,所以在其他上下文中,可以把它用作标识符。但直接用在关键字class、struct或interface前时,它表示分部类型。

例:分部类

技术分享
技术分享

Visual Studio为标准的Windows程序模板使用了这个特性。当你从标准模板创建ASP.NET项目、Windows Forms项目或Windows Persentation Foudation(WPF)项目时,模板为每个Web页面、表单、WPF窗体创建两个类文件。

  • 一个文件的分部类包含由VS生成的代码,声明了页面上的组件。你不应该修改这个文件中的分部类,因为如果修改页面组件,VS会重新生成
  • 另一个文件包含的分部类可用于实现页面或表单组件的外观和行为
  • 除了分部类,还有另外两种分部类型
    • 局部结构(第10章)
    • 局部接口(第15章)

分部方法


分部方法是声明在分部类中不同部分的方法。
分部方法的两个部分如下

  • 定义分部方法声明
    • 给出签名和返回类型
    • 声明的实现部分只是一个分号
  • 实现分部方法声明
    • 给出签名和返回类型
    • 是以正常形式的语句块实现

关于分部方法需要了解的重要内容如下

  • 定义声明和实现声明的签名和返回类型必须匹配。签名和返回类型有如下特征
    • 返回类型必须是void
    • 签名不能包括访问修饰符,这使分部方法是隐式私有的
    • 参数列表不能包含out参数
    • 在定义声明和实现声明中都必须包含上下文关键字partial,直接放在关键字void前
  • 可以有定义部分而没有实现部分。这种情况下,编译器把方法的声明以及方法内部任何对方法的调用都移除。不能只有实现部分而没有定义部分。

下面是一个名为PrintSum的分部方法的示例

  • 因为分部方法是隐式私有的,PrintSum不能从类的外部调用。方法Add是调用PrintSum的公有方法
partial class MyClass
{
         必须是void
             ↓
    partial void PrintSum(int x,int y);//定义分部方法
    public void Add(int x,int y)
    {
        PrintSum(x,y);
    }
}
partial class MyClass
{
    partial void PrintSum(int x,int y)//实现分部方法
    {
        Console.WriteLine("Sum i {0}",x+y);
    }
}
class Program
{
    static void Main()
    {
        var mc=new MyClass();
        mc.Add(5,6);
    }
}

技术分享

技术分享

C#图解教程 第六章 深入理解类