C#深入理解类 - C#入门基础

本章包含:

  • 类成员
  • 成员修饰符的顺序
  • 实例类成员静态字段
  • 从类的外部访问静态成员
  • 静态函数成员
  • 其他静态类成员类型
  • 成员常量
  • 常量和静态量
  • 属性
  • 实例构造函数
  • 静态构造函数对象初始化语句
  • 析构函数
  • readonly 修饰符
  • this关键字
  • 访问器的访问修饰符
  • 分部类和分部类型
  • 分部方法

类成员

类成员包括:字段、常量、方法、属性、构造函数、析构函数、运算符、索引、事件


修饰符顺序

格式:

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

修饰符

  • 如果有修饰符,必须放在核心声明前
  • 多个修饰符可以任意顺序排列;

特性

  • 如果有特性,必须放在修饰符和核心声明前;
  • 多个特性可以任意顺序排列;

特性以后再讲,现在先讲解修饰符,有:public private protected static async 等;

// 多个修饰符因为可以任意排序,所以下面两种写法等价:
public static int MaxVal;
static public int MaxVal;

每个实例成员都是类的一个副本,改变其中一个实例成员不会影响另一个实例成员;

class D{
    public int Mem1;
}

D d1 = new D();
D d2 = new D();
d1.Mem1 = 10; d2.Mem2 = 20;

image


静态字段

静态字段被类的所有实例共享,所有实例都访问同一内存位置。因此,如果该内存位置的值被一个实例改变了,这种改变对所有的实例都可见。

class D{
    int Mem1;    // 实例字段
    static int Mem2;    // 静态字段
}

D d1 = new D();
D d2 = new D();

通过类访问:

public class D
{
    static public int AA = 66;
}

class Program
{
    static void Main()
    {
        Console.WriteLine($"{D.AA}");
    }
}

甚至不需要使用类来点出来,而是通过using:

using System;
using System.IO;

using static ConsoleApp2.D;
using static System.Console;
using static System.Math;

namespace ConsoleApp2
{
    public class D
    {
        static public int AA = 66;
    }
    class Program
    {
        static void Main()
        {
            WriteLine($"{AA}, {Sqrt(16)}");
        }
    }
}

静态成员的生命周期

静态成员的生命期与实例成员的不同。

即使类没有实例,也存在静态成员,并且可以访问。

上图中,没有类实例的静态成员仍然可以被赋值并读取,因为静态字段与类有关,而与实例无关

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


静态函数 成员

与静态字段一样,独立于任何类的实例,即使没有类的实例,仍然可以调用静态方法;

但是:静态方法的方法体中,不能访问实例成员!但能访问其他静态成员;

class X{
    static public int A;
    static public void PrintValA(){
        // 静态函数中只能访问静态成员
        Console.WriteLine(A);
    }
}

X.A = 10;
X.PrintValA();

可以声明的静态类成员

可以的数据成员(存储数据的):字段、类型

可以的函数成员(执行代码的):方法、属性、构造函数、运算符、事件

不能的:常量、索引器


成员常量

常量是只读的;

说白了,常量就是一个其值永远不会改变的静态字段。

常量的值会在编译时自动推算,编译器会在遇到常量时,将其逐个替换为该常量的值。

与局部常量一样,只是局部常量初始化在方法体中,而成员常量初始化在类中:

class MyClass{
    const int IntVal = 100;
}

const double PI = 3.1416;   // 错误,不能在类型声明之外声明

与C/C++ 不同,C#中没有全局常量!


静态只读变量

在某一个应用程序中初始化之后,不能对其进行修改。但是在不同的应用程序中可以有不同的值。

常量 和 静态只读变量 有什么区别?

常量在所有的程序中都是同一个值,而静态只读变量在不同的程序中可以有不同的值。


常量 与 静态量

成员常量 表现得更像是 静态值(即:静态的常/变量),成员常量对类的每个实例都是“可见的”;而且即使没有类的实例化也可以使用。

与真正的静态量不同,常量没有自己的存储位置,而是在编译时被编译器替换。类似C和C++的 #fefine 值。

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

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

属性

属性 和 字段 很像,用法上他们都几乎没区别;

但是,与字段不同,属性:

  • 是一个函数成员
  • 不一定为数据分配内存
  • 它执行代码

属性本身没有任何存储,取而代之,访问器决定如何处理发送进来的数据,以及应该将什么数据发送出去。在这种情况下,属性使用一个名为 TheRealValue 的字作为存储。

class C1 {
    private int theRealValue;      // 字段:分配内存
    
    public int MyValue{        // 属性:未分配内存
        set { theRealValue = value; }
        get { return theRealValue; }
    }
}

属性的几种命名规则:

private int firstField;  // Camel驼峰大小写
public int FirstField{    // Pascal 大小写
    get { return firstField;}
    set { firstField = value; }
}

private int _firstField;        // 下划线 和 Camel驼峰大小写
public int FirstField{
    get { return _firstField;}
    set { _firstField = value; }
}

C#7.0 中为属性的 getter 和 setter 引入了另一种语法,这语法使用表达式函数体(Lambda):

int MyValue{
    set => value >100 ? 100 : value;
    get => theRealValue;
}

可以省略 set 或 get 中其中一个,如果只有get,则属性为只读,反之为只写;但是不能两个都省略;


属性和公有字段

属性比公有字段更好,理由:

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

自动属性

public int MyValue { get; set;}

public int MyValue2 {get;}

静态属性

属性也能声明为 static,与其他静态成员一样:

  • 不能访问类的实例成员,但能被实例成员访问;
  • 不管类是否有实例,它们都是存在的;
  • 在类内部,可以仅使用名称来引用静态属性;
  • 在类外部,可通过类名或者使用 using static 结构来引用静态属性;

构造函数


静态构造函数

调用时间:

  • 在引用任何静态成员之前
  • 在创建类的任何实例之前

与实例构造函数不同:

  • 静态构造函数使用 static 关键字
  • 类只能有一个静态构造函数,且不能带参数
  • 静态构造函数不能有访问修饰符
class Class1{
    static Class1(){
        // ...
    }
}
  • 类既可以有静态构造函数,也可以有实例构造函数
  • 静态构造函数中,不能访问所在类的实例成员,因此也不能使用 this 访问器;
  • 不能从程序中显式地调用静态构造函数,系统会自动调用它们:
    • 在类的任何实例被创建之前自动调用;
    • 在类的任何静态成员被引用之前自动调用;
class A{
    private static Random RandomKey;
    static A(){
        RandomKey = new Random();
        Console.WriteLine("静态构造函数");
    }
    
    public A(){
        Console.WriteLine("实例构造函数");
    }

    public int GetRandomNumber(){
        return RandomKey.Next();
    }
}

A a = new A();
A b = new A();

a.GetRandomNumber();
b.GetRandomNumber();


// 输出:
静态构造函数
实例构造函数
324130517
实例构造函数
1771231000

对象初始化语句

public class Point{
    public int X = 1;
    public int Y = 2;
}

Point pt1 = new Point();
Point pt12 = new Point { X = 5, Y = 10 };

析构函数

析构函数(destuctor)执行在类的实例被销毁之前需要的清理或释放非托管资源的行为。

非托管资源是指通过 Win32 API 获得的文件句柄,或非托管内存块。

使用 .NET 资源是无法得到它们的,因此如果坚持使用 .NET类,就不需要为类编写析构函数,

因此,后面再介绍析构函数;


readonly 修饰符

字段可用 readonly 修饰,一旦值被设定就不能改变。(和 const 非常像吧?)

const字段只能在字段的声明语句中初始化,而 readony字段可以在下列任意位置设置它的值。

  • 字段声明语何,类似于 const
  • 类的任何构造的数。如果是static字段,初始化必须在静态构造函数的完成

const字段的值必须可在编译时决定,而readonly字段的值可以在运行时决定,这种自由性允许你在不同的环境或不同的构造函数中设置不同的值!

ccnst的行为总是静态的,而对于readonly字段以下两点是正值的:

  • 它可以是实例字段,也可以是静态字段
  • 它在内存中有存储值

this 关键字

this 一般在类中使用,是对当前实例的引用;只能被用在类成员的代码块中:

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

因为静态成员不是实例的一部分,所以不能在任何静态函数成员的代码中使用 this 关键字;

class A{
    int Var1 = 10;
    public int FuncSum(int Var1){
        return Var1 > this.Var1 ? Var1 : this.Var1;
    }
}

理解它很重要的,但它实际上很少在代码中使用。


索引器

没有索引的简单类:

有时候,能用索引访问它们将会很方便,好像该实例是字段的数组一样,这正是索引器能做的事儿;

为 Employee写一个索引器,看起来像这样:


什么是索引器?

是一组 get 和 set 访问器,与属性类似。下图展示了一个类的索引器的表现形式:

索引器和属性在很多方面是相似的:

  • 和属性一样,索引器不用分配内存来存储。
  • 索引器和属性都主要被用来访向 其他 数据成员,它们与这些成员关联,并为它们提供获取和设置访问。
    • 属性通常表示 单个 数据成员
    • 索引器通常表示 多个 数据成员

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

关于索引器,还有一些注意事项:

  • 和属性一样,索引器可以只有一个访问器,也可以两个都有!
  • 索引器总是实例成员,因此不能被声明为static
  • 和属性一样,实现get和set访问器的代码不一定要关联到某个字段或属性。这段代码可以做任何事情也可以什么都不做,只要get访问器返回某个指定类型的值即可。

声明索引器

  • 索引器没有名称,在名称的位置是关键字 this
  • 参数列表在方括号中间
  • 参数列表中必须至少声明一个参数。

声明索引器很类似于声明属性:


索引器的 Set 访问器

索引器被赋值时,Set调用;

Set 接收两个数据:

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

emp[0] = "Doe";


索引器的 Get 访问器

string s =emp[0];


为 Employee 示例声明索引器

public string this[int index]
{
    set
    {
        switch (index)
        {
            case 0: LastNmae = value; break;
            case 1: FirstName = value; break;
            case 2: CityBirth = value; break;
            default: throw new ArgumentOutOfRangeException("index");
        }
    }

    get
    {
        switch (index)
        {
            case 0: return LastNmae;
            case 1: return FirstName;
            case 2: return CityBirth;
            default: throw new ArgumentOutOfRangeException("index");
        }
    }
}

另一个索引器示例:

class B
{
    int Temp0;
    int Temp1;
    public int this [int index]
    {
        get
        {
            return (0 == index) ? Temp0 : Temp1;
        }
        set
        {
            if( 0 == index)
            {
                Temp0 = value;
            }
            else
            {
                Temp1 = value;
            }
        }
    }
}

B b = new B();
b[0] = 15;
b[1] = 20;

索引器重载

访问器的访问修饰符

目前,属性 和 索引器 成员 都带有 get 和 set 访问器;

默认情况下,访问器的访问级别和 成员自身相同;

你可以为不同访问器设置不同的访问级别;

// get 为public,因为成员自身就是public修饰,而 set设置了private
public string Name { get; private set;}

只有成员同时有 get 和 set 时,才能有访问修饰符:

public string Name {private set;}  // 错误

虽然两个访问器要同时出现,但是他们只有一个有访问修饰符:

public string Name {private get; protected set;}      // 错误

访问器的访问修饰符的限制必须比成员的访问级别更加严格,即:访问器的访问级别必须比成员的访问级别低;

public string Name { get; public set; }      // 错误


分部类 和 分部类型

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

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

分部类必须被标注为 partial class ,而不是单独的 class 关键字;

分部类和普通类很一样,只是分部类用 partial 修饰了:

partial class A1{
    public int t1 {get; set;}
    public int t2 {get; set;}
}

partial class A1{
    public int t3 {get; set;}
    public int t4 {get; set;}
}

注意:partial 不是关键字,所以在其他上下文中,可以在程序中把它用作标识符。但是用在 class、struct 或 interface 之前时,它表示分部类型;

posted @ 2022-03-01 18:59  醉马踏千秋  阅读(169)  评论(0编辑  收藏  举报