C#学习笔记(Part II) 教材版本:C#图解教程第五版

方法

所谓的方法实质上即为所属于某个类的函数,只是不同于C,其仅被通过该类所创建的对象所调用。

方法的构成

方法头

  • 规定方法所返回的类型(包括void)
  • 方法的名称
  • 可以传递的参数

方法体
包含了可执行的语句序列,规定了该方法所需要进行的操作,其可以使用传递的参数,并通过运算来获取返回值并返回。

方法体内部的代码执行

方法体作为一个块,其是由大括号括其的语句序列,其内部的内容包含:

  • 局部变量
  • 控制流结构
  • 方法调用
  • 内嵌的块
  • 其他方法,称之为局部函数

局部变量

局部变量一般是创建来进行临时的操作的变量,其也是用以来保存数据并进行计算的,只是它的存在仅包含被创建到方法结束这一段时间内。其他时间内,该局部变量便不存在。
对比示例变量和局部变量

角度示例变量局部变量
生存期从示例变量创建开始直到该实例不在被调用在它在块中被声明到块结束这段过程中
隐式初始化初始化成该类型的默认值没有隐式初始化,如果在声明前调用便会编译错误
存储区域因为是类的成员所以被储存在堆里,无论是值类型还是引用类型引用类型存于栈,数据存于堆
类型的推断和var关键字

在JavaScript中,var关键字是可以指代任意的类型,所有的类型的可以被var所声明,因此JavaScript是一种弱类型的语言。而C#中,我们也可以使用var关键字类快速地声明一个变量,但是C#中的var并不是一种类型,其只是一个快速声明变量的方式,其根据右翼的数值类型来决定该变量的类型而并不会改变其类型。因此var关键字并不会改变C#强类型的性质。

        class SomeType{
            
        }
        void varTest(){
            var num=6; //因为右翼的数值是整数,因此此时该var代之int类型
            var obj=new SomeType();//此时右翼的数值是SomeType对象,因此var此时代指SomeType类型
        }
嵌套快中的局部变量

方法体内可以嵌套其他的块。

  1. 可以有任意数量的块,并且它们是可以嵌套的,它们可以嵌套到任意级别
  2. 局部块中的局部变量声明周期仅限于声明的块中
int calculate(int para){
            int var1=5;
            {	//嵌套块开始
                int var2=10;
                int result=var1*var2;
                Console.WriteLine("result is {0}",result);
            }  //嵌套块结束
            return 0;
        }

上述的var1在嵌套块声明开始之前就被声明,因此其在块进入前便在栈中被声明,而var2则是在进入块声明后才被声明,因此在进入后才在栈中声明,并在块结束后便会被出栈。而var1相较于嵌套块则是全局变量只在方法结束后才会被出栈。由此我们可以看出我们如果希望合理的使用内存空间可以通过控制栈的声明来实现内存的最优调度。

局部常量

我们前面知道了var关键字在C#中的使用,其与JavaScript十分类型,现在我们可以来看下const关键字在C#中的使用。已知JavaScript中const是为了声明一个常量而存在的,在C#中,这一点同样适用。我们可以通过在变量声明前添加const关键字来声明一个常量,注意此处的const后需要添加变量的类型:

int testConst(int num){
            const int alpha=22;
            int result=alpha*num;
            return 0;
        }

控制流

一个方法包含了组成程序行为的大部分代码。其余部分在其他的函数成员中,如属性和运算符。
术语控制流值得是程序从头到尾的执行流程,默认情况下,程序执行顺序是自上而下的,而通过控制语句来改变程序执行的顺序和逻辑。

选择语句
  • if… 有条件地执行一条语句
  • if…else 有条件地执行一条或另一条语句
  • switch 有条件地执行一组语句中的某一条
循环语句
  • for循环—>在顶部测试
  • while循环—>在顶部测试
  • do循环—>在底部测试
  • foreach为一组中每个成员执行一次
跳转语句
  • break 跳出当前循环
  • continue 到当前循环的底部
  • goto 到一个命名地语句
  • return 返回到调用方法地位置继续执行

返回值

方法如果希望返回一个值,则我们需要以所需返回的类型替换void关键字:

void DisplayHour(){...}
int GetHour(){...}

局部函数

我们知道方法块内的代码可以调用另一个方法。如果另一个方法在同一个类中,可以直接使用它的名称并传入参数的方式进行调用,如果需要在别的类中调用则可以将修饰符设置为public,此时便可以通过该类的实例进行调用。
从C#7.0开始,我们可以在一个方法内声明另一个单独的方法,这样可以将嵌入的方法和其他的代码隔离开,又因为是在一个方法内声明的因此无法在该方法外进行调用。使用得到我们可以是的我们的方法更加清晰和灵活:

using System;
namespace Main2{
    class C3{
        static void Main(){
        var obj=new C3();
        int result=obj.multiply(2,4);
        Console.WriteLine("Result is {0}",result);
        }
		  int multiply(int a,int b){
            int result=0;
            int add(int c,int d){
                return c+d;
            }
            for (int i = 0; i < b; i++)
            {
                result=add(result,a);  
            }
            return result;
        }
    }
}

形参和实参

我们在定义一个方法时,我们位置规定可以插入的参数即为形参,形参是不确定的值,但是它却是有意义的值,其代表该方法被调用时所传入的值。
而实参实际上即为我们实际传入的参数,它们都是确定的值,当然它们也可以是一个会返回有效值的表达式。其在传入方法后便会替换掉原先的形参进行实际的运算。

值参数

参数有很多种,它们各自以略微不同的方式从方法传入或传出数据。到目前为止,我们看到的都是默认的值参数
当我们使用值参数时,其通过将实参的值复制到形参的方式将其传递给方法,方法被调用时,系统会进行一下操作:

  • 在栈中为形参分配空间
  • 将实参的值复制给形参
    因此值参数的实参并不一定需要是一个变量,它可以是任意形式的可以返回相应数据类型的表达式:
float fun1(float val){
	...
}

float k=3.14;
float fVal1=fun1(k);
float fVal2=fun2(2k-1);

此时fVal1和fVal2均会被赋值
一下将会展示值参数的特点:

using System;
namespace Main2{
    class MyClass{
        public int val=20;
    }

    class Program{
        static void MyMethod(MyClass f1,int f2){
            f1.val=f1.val+5;
            f2=f2+5;
        }

        static void Main(){
            MyClass a1=new MyClass();
            int a2=10;
            
            MyMethod(a1,a2);
        }
    }
}

1.此时在MyMethod被调用前,a1和a2作为形参便在栈中被分配了空间:
在这里插入图片描述

2.在方法开始时,a1因为是引用参数,因此首先其引用被f1所复制,与其指向了同一个地址而a2因为是引用类型所以直接在栈中被直接赋值给f2:
在这里插入图片描述

3.在方法结束后f1和f2均被出栈,因此此时结果将会变成:
在这里插入图片描述

引用参数

  • 当我们使用引用参数时需要使用ref关键字进行修饰
  • 实参必须时变量,如果实参是一个引用类型的变量则可以赋值为一个引用或者null
// 		定义是需要使用ref关键字
    static void RefMethod(ref int val){
        Console.WriteLine("this ref parameter is {0}",val);
    }
    static void Main(){
        int a=2;
// 		调用时也需要使用ref关键字
        RefMethod(ref a);
// 		我们必须引用一个变量
				RefMethod(1);
    }

而相较于前面的值参数,当我们使用了引用参数时,形参与实参的关系便发生了变化,形参原本的独立性质被剥离,其变成了一个实参的别名。对于原本的引用类型而言,变化并不大,由于依然是引用的同一个地址因此此时该引用地址下的值仍会被改变,而原本的基本类型此时由于形参和实参本质上时同一个值因此,此时形参对于值的变更将会同步到实参上。

输出参数

前面我们的的参数在传入时一般情况下是需要提前为之赋值的,继而使得其参与指定的运算,但是这些运算或许本身不会改变参数的值。然而在某些特殊情况下,我们或许存在一些方法是需要明确的为参数赋值而构造的,此时我们便可以将参数设置为输出参数,该种参数将需要在参数声明和调用时均需要使用out关键字。一旦我们添加了改关键字,被声明的参数在被声明的方法内便必须被明确赋值。这也就意味着即使我们传入的参数是没有被明确声明的值在经过方法调用后也一定会有相关的赋值:

//方法调用前参数被赋值
 class MyClass{
        public int val=25;
    }
    class Program{
    static void MyMethod(out MyClass obj,out int num){
        obj=new MyClass();
        num=22;
    }
    static void Main(){
        var obj=new MyClass();
        obj.val=3;
        int num=11;
        MyMethod(out obj,out num);
        Console.WriteLine("The obj val is {0}, and num is {1}",obj.val,num);
    }
    }
//方法调用前参数未被赋值
 class MyClass{
        public int val=25;
    }
    class Program{
    static void MyMethod(out MyClass obj,out int num){
        obj=new MyClass();
        num=22;
    }
    static void Main(){
        MyClass obj;
        int num;
        MyMethod(out obj,out num);
        Console.WriteLine("The obj val is {0}, and num is {1}",obj.val,num);
    }
    }

以上两者得到的结果都是一致的。

参数数组

当我们为一个函数指定了一个形参,那么该函数被执行时便需要严格按照指定参数的数量即类型顺序进行参数的传入才能顺利地完成调用。然而当我们使用了参数数组以后,这一切就不一样了。因为它允许同时该参数同时为0个或者n个该类型的参数,声明该种参数我们需要添加params关键字用以区分常规的数组参数:

 static int AccumulateMethod(params int[] nums){
        int result=0;
        if(nums!=null&&nums.Length>0){
            for (int i = nums.Length - 1; i >= 0 ; i--)
            {
                result+=nums[i];
            }
        }
        return  result;
    }

	 static void Main(){
        int result=AccumulateMethod(1,2,3);
        Console.WriteLine("The result is {0}",result);
    }

我们注意到相较于其他集中参数声明和调用来说,我们声明参数数组时仅需在声明时使用关键字params,而调用时则不需要使用。

ref返回

通过前文我们可以发现,当我们使用了ref后,我们可以提取该变量的引用,更近一步地说我们时可以提取它的地址。而两个变量的地址一旦相等它们便相对于彼此为别名的状态,如:

int a=3;
ref int b=ref a;
Console.WriteLine($"a={a},b={b}");
a=4;
Console.WriteLine($"a={a},b={b}");
b=5;
Console.WriteLine($"a={a},b={b}");

此时输出结果为:
a=3,b=3
a=4,b=4
a=5,b=5
然而ref更常用的还是作为返回类型的辅助关键字,用以表达返回变量的引用而不是单纯的值,这里可以理解为Java种getter和setter的进阶实现:

using System;

namespace Main5
{
    class Simple
    {
        private int Val=5;

        public ref int RefOfVal(){
            return ref Val;
        }

        public void PrintVal(){
            Console.WriteLine("Val is {0}",Val);
        }
    }
    class Program
    {
    public static void Main(){
        var obj=new Simple();
//注意此时我们的obj.RefOfVal()前还是需要添加ref关键字
        ref int ValRef=ref obj.RefOfVal();
        obj.PrintVal();
        ValRef=22;
        obj.PrintVal();
    }
    }
}

命名参数

我们一般情况是默认使用位置参数,也就是我们是按照参数的位置来规定参数的。而实际上我们可以通过使用“参数名:参数值”的形式来指定参数,此时由于是按照参数的名字指定的参数,因此称之为命名参数,此时我们也无需关注参数的位置与否。而这个参数的名字也就是我们声明时为之赋予的:

// 声明方法
int Calculate(int a,int b,int c){
	...
}
// 调用方法
Calculate(c:23,a:15,b:33)

可选参数

在C#中我们还可以声明一种可选参数,声明这种参数我们仅需在方法声明时为之赋予一个默认值即可:

// 声明方法
int SomeMethod(int a,int b=4){
   ...
}
// 调用方法
SomeMethod(24);

栈帧

通过前文我们知晓局部变量和参数均是位于栈上的。
实际上在调用方法时,内存从栈的顶部开始分配,保存和方法的关联的数据项。这块内存便称之为栈帧

栈帧包含的内存保存内容:
  1. 返回地址,也就是这个方法退出后继续执行的位置
  2. 分配内存的参数,也就是方法的值参数或者参数数组
  3. 和方法调用相关的其他的管理数据项
在方法调用时,整个栈帧都会压入栈
在方法退出时,整个栈帧都会从栈中被弹出,这种被弹出的栈有时也被称之为栈展开
  class Program{
        private static void MethodAlpha(int para,int meter){
            Console.WriteLine($"Enter MethodAlpha with parameter:{para},{meter}");
            MethodBeta();
            Console.WriteLine("Exit MethodAlpha!");
        }
        private static void MethodBeta(){
            Console.WriteLine($"Enter MethodBeta without parameter");
            Console.WriteLine("Exit MethodBeta!");
        }

        public static void Main(){
            Console.WriteLine("Enter Main!");
            MethodAlpha(22,32);
            Console.WriteLine("Exit Main~");
        }
    }

我们首先在Main方法内调用了MethodAlpha,而后我们又在MethodAlpha中调用了MethodBeta,此时我们可以得到:

#执行Main函数,Main函数此时开始展开
1.Enter Main! 
#调用了MethodAlpha,此时22,32作为参数被压入栈中,MethodAlpha函数此时开始展开
2.Enter MethodAlpha with parameter:22,32
#在MethodAlpha中调用了MethodBeta,MethodBeta函数此时开始展开
3.Enter MethodBeta without parameter
#MethodBeta结束,此时MethodBeta被弹出栈
4.Exit MethodBeta!
#MethodAlpha结束,此时MethodAlpha被弹出栈
5.Exit MethodAlpha!
#Main结束,此时Main被弹出栈
6.Exit Main~

递归

一个方法除了可以调用其他函数以外还可以调用自身,这种调用自身的调用方式,我们称之为递归调用。
这里的阶乘算法便使用了递归调用:

        static long Factorial(int value){
            if(value<=1)
                return value;
            else
                return value*Factorial(value-1);
        }

        public static void Main(){
            Console.WriteLine($"Result is {Factorial(10)}");
        }

类の深入理解

类成员

前文中有提及的成员包含了字段和方法,在本节我们还将学习新的成员。

🕃 类成员的构成
数据成员函数成员
字段方法运算符
属性索引
构造函数事件
析构函数

成员的修饰符顺序

【特性】【修饰符】核心声明

  • 修饰符
    1. 如果有修饰符则必须置于核心声明之前
    2. 如果有多个修饰符则它们可以任意排序
  • 特性
    1. 如果有特性,则必须置于修饰符和核心声明之前
    2. 如果有多个特性则可任意排序

我们使用过的修饰符如private,public和static等,因此以下两种表达含义时一样的:

private static String name;
static private String name;

实例类成员

类的成员可以关联到类的一个实例上也可以关联到该类的所有实例上,而默认情况下这些成员均是仅关联到类的一个实例上的,这样的实例我们称之为实例成员。
当我们声明的成员为实例成员时,那么该成员将会在每一个创建的实例中存在一个副本,这也就意味着一个实例改变其下的实例成员并不会影响其他实例下的该成员。每一个实例下的实例成员均是独立的个体。

using System;

namespace Main
{
    class Main7
    {
        static void Main(){
            var a=new Sample();
            var b=new Sample();
            ref int valueAlpha=ref a.REFOfVAL();
            ref int valueBeta=ref b.REFOfVAL();
            valueAlpha=22;
            valueBeta=23;
            Console.WriteLine($"Value Alpha and Beta is {a.REFOfVAL()},{b.REFOfVAL()}");
        }
    }

    class Sample
    {
        private int VAL;
        public ref int REFOfVAL(){
            return ref VAL;
        }
    }
}

此时输出结果为:

Value Alpha and Beta is 22,23

静态字段

我们之前也提到了有的成员可以被应用到该类的所有实例之上,这种成员便是所谓的静态字段。
一个类的静态字段将会被该类所有的实例共享,这也就意味着一个实例对于该类的静态变量的修改会被其他所有实例所知晓。即改变是对所有实例可见的。
注意由于静态变量严格意义上是属于类的,因此实例是无法直接调用它的:

 	class Main7
    {
        static void Main(){
            var a=new Sample();
            var b=new Sample();
            a.VAL=23;	//编译错误
        }
    }
	class Sample
    {
        public static int VAL;
 		}

如果说我们一旦使用了using static 类名的语法引入了类,那么其内部的静态成员便可以直接进行调用了:

// 引入Console类下的Console类
using static System.Console;

namespace Main
{
    class Main7
    {
        static void Main(){
            var a=new Sample();
            var b=new Sample();
            ref int valueAlpha=ref a.REFOfVAL();
            ref int valueBeta=ref b.REFOfVAL();
            valueAlpha=22;
            valueBeta=23;
            //此时我们可以直接调用Console类下的WriteLine函数
            WriteLine($"Value Alpha and Beta is {a.REFOfVAL()},{b.REFOfVAL()}");
        }
    }

    class Sample
    {
        public static int VAL;
        static int PUBLICVAL;
        public ref int REFOfVAL(){
            return ref VAL;
        }
    }
}
静态字段的实例
using static System.Console;

namespace Main
{
    class Main7
    {
        static void Main(){
            var a=new Sample();
            var b=new Sample();
            ref int valueAlpha=ref a.REFOfVAL();
            ref int valueBeta=ref b.REFOfVAL();
            valueAlpha=22;
            valueBeta=23;
            WriteLine($"Value Alpha and Beta is {a.REFOfVAL()},{b.REFOfVAL()}");
            WriteLine($"a's VAL is {a.getVAL()},b's VAL is {b.getVAL()}");
            a.SETVAL(12);
            WriteLine($"a's VAL is {a.getVAL()},b's VAL is {b.getVAL()}");
            b.SETVAL(33);
            WriteLine($"a's VAL is {a.getVAL()},b's VAL is {b.getVAL()}");
        }
    }

    class Sample
    {
        private  int VAL;
        public static  int PUBLICVAL=11;

        public void SETVAL(int Value){
            PUBLICVAL=Value;
        }

        public int getVAL(){
            return PUBLICVAL;
        }

        public ref int REFOfVAL(){
            return ref VAL;
        }
    }
}

输出结果为:

Value Alpha and Beta is 22,23
a's VAL is 11,b's VAL is 11
a's VAL is 12,b's VAL is 12
a's VAL is 33,b's VAL is 33
静态成员的生命周期
  • 我们注意到只有实例创建后才能产生实例成员,在这些实例销毁后,实例成员便不再存在了
  • 但是即使类没有实例,也存在静态成员,并可以直接访问

注:即使不存在类实例,静态成员依旧存在,如果静态字段有初始化语句,那么在使用该类的任何静态成员之前初始化该字段,但这种初始化并不一定发生在程序刚执行时。
正因为静态变量的这一特性,我们可以发现实际上静态变量是属于类而非实例的,这也是它与实例变量的本质区别。

静态函数成员

我们之前有提到Console类下的WriteLine函数,当我们使用static关键字引入了静态成员时,便可以直接调用WriteLine函数,这正是因为WriteLine即为Console类下的静态函数成员。
可以被声明为静态成员的类型

数据成员(存储数据)函数成员(执行代码)
✅ 字段✅ 方法
✅ 类型✅ 属性
❎ 常量✅ 构造函数
✅ 运算符
❎ 索引器
✅ 事件

成员常量

成员常量实际上就是声明在类中的常量而已,其与声明在方法内的局部常量声明一致。除此之外,一个常量它首先是在编译时可以被计算的(声明时必须被赋值),因此其本事一般为一个简单类型或者由它们组成的表达式。
注:与C或者C++不同的是在C中不存在全局常量,这也就意味这常量必须被声明在类中

常量与静态量

静态量是可以被类直接访问但是对于实例而言它们又是不可见的,常量这一点与之是相同的,但是与静态量不同的是它是没有自己的存储空间的,而是在编译时被编译器替换的。其等价与C中的#define值

using System;
using static System.Console;
{
    
}
{
    
}
namespace Main
{
    class X
    {
        public const double PI=3.14d;
    }
    class Program
    {
        public void Main(){
            WriteLine($"Invoked by Class:{X.PI}");
        }
    }
}

属性的使用

属性代表类的实例或者类的数据项的成员。使用属性就像写入或者读取一个字段,如这里的MyClass中定义了一个公有字段和一个公有属性:

MyClass mc=new MyClass();
mc.MyField=5;
mc.MyProperty=20;
属性的特征

与字段类似的特征

  • 它是命名的类成员
  • 它有类型
  • 它可以被赋值或者读取
    不同点
  • 首先属性属于是函数成员
  • 它不一定为数据存储分配内存
  • 它会执行代码
  • 属性是一组(两个)匹配的,命名的,称之为访问器的方法:
    1.set是访问器为属性赋值
    2.get是访问器从属性中获取值
以下为属性的基本定义:
int MyPropery
{
	set
	{
		SetAccessorCode
	}
	get
	{
		GetAccessorCode
	}
}

实际使用

 class Alpha
    {
        private int realField;	//字段
        public int property	//属性
        {
            set{
                realField=value;
            }
            get{
                return realField;
            }
        }
    }
属性的声明和访问器

set和get访问器有预定义的语法和语义。可以把set访问器视为一个方法,带有一个单一的参数value,使用它我们可以为该属性赋值。而get则是一个返回属性值的无参数函数,使用它我们可以访问一个属性的值。

  • set访问器总是
    1.拥有一单独的隐性的值参,名称为value,并且类型与属性类型相同
    2.返回类型为void
  • get访问其总是:
    1.没有参数
    2.拥有一个与属性相同类型的返回类型
    注意事项:
  1. get访问器的所有执行路径都必须包含一条return语句,并返回一个与属性同类型的值
  2. 访问器函数set和get可以任意排序,但是除了它们两个访问器外不允许有其他方法
实际使用
using static System.Console;
using System;
namespace Main
{
    class Alpha
    {
        private  int realField=5; //后备字段,分配内存
        public int property	//属性,不分配内存
        {
            set{
                realField=value;
            }
            get{
                return realField;
            }
        }
    }
    class Program
    {
         public static void Main(){
            WriteLine("Hello");
            var obj=new Alpha();
            WriteLine("The property is {0} (before)",obj.property);
            obj.property=10;
            WriteLine("The property is {0} (after)",obj.property);
        }
    }
}

输出结果为:

Hello
The property is 5 (before)
The property is 10 (after)

因此我们发现实际上在C#中,属性的作用实际上就是为字段提供一个访问的接口,也就相当于Java中的getter和setter方法的所以。
但是有所不同的是在Java中我们的getter和setter是一个真正的函数成员,我们需要通过显性调用该getter或者setter方法来访问和设置字段。而在C#中属性相当于一个普通的成员变量,我们在左侧调用便是相当于隐式调用了setter方法,而右侧便是getter方法:

 class Alpha
    {
        private  int realField=5;
        public int property
        {
            set{
                realField=value;
            }
            get{
                return realField;
            }
        }
    }
    class Program
    {
         public static void Main(){
            var obj=new Alpha();
            obj.property=10;	//隐式调用了setter方法
            WriteLine("The property is {0}",obj.property);	//隐式调用了getter方法
        }
    }

因为C#中我们相当于将getter和setter方法置于了“属性类”之中,因此get和set方法实际上是可添加权限修饰符的:

public int property
        {
// 		此时该属性为只读模式,我们只能访问但是无法修改
            private set{
                realField=value;
            }
            get{
                return realField;
            }
        }
		
		
public int property
        {
// 		此时该属性为仅修改模式,我们只能能修改但是无法访问
            private set{
                realField=value;
            }
            get{
                return realField;
            }
        }

属性和关联字段

前文中我们注意到了,属性是和一个关联字段绑定的通常。而我们可以将被关联的字段称之为后备字段或者后备储存,后备字段是负责实际存储的因此是被分配内存的,而属性是不会被分配内存的。
一般情况下我们的字段遵循的为Camel大小写风格,而属性则是Pascal风格的。所谓的Camel风格即除了一个单词外,其他每个单词的首字母大写,其余字母全部为小写:

class Sample
    {
        private int firstField; //Camel风格
        public int FirstField{  //Pascal风格
            get{return firstField;}
            set{firstField=value;}
        }
        private int _secondField;   //下划线加Camel风格
        public int SecondField
        {
            get{return _secondField;}
            set{_secondField=value;}
        }
    }

执行额外操作

我们使用属性访问其的一大优势在于其可以帮助我们实现除了后备字段的简单的传入和传出外还可以对于这些输入输出作额外的运算:

private int theValue;
public int TheValue
{
	set{theValue=value>100?100:value;}	//此时我们输入的theValue只可能小于等于100,这便是set可以实现的额外运算
	get{return theValue;}
}

而C#7.0为getter和setter引入了一种称之为表达函数体(或称之为lambda表达式)

只读和只写属性

我们将只有get的属性称之为只读属性,只有set的属性称之为只写属性。而只写属性很少出现,因为其可以被方法所代替

计算只读属性的示例

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()
	{
		RightTriangle c=new RightTriangle();
		Console.WriteLine($"Hypotenuse: {c.Hypotenuse}");
	}
}

输出结果为

Hypotenuse=5

自动实现属性

因为属性经常与后备字段进行绑定,所以C#提供了自动实现属性(automatically implemented property),其允许只声明属性而无需声明后备字段,编译器将会为我们创建一个隐藏的后备字段,而且自动挂接到get和set上。

使用自动实现属性的要点
  • 不声明后备字段-编译器将会根据属性类型自动分配存储
  • 不提供访问器的方法体-它们必须被声明为简易的分号,get负责简单的读,set充当简单的写
示例:
		class Sample
    {
// 			没有声明后备字段
        public int MyValue{ //分配内存
            get;	//访问器
            set;	//访问器
        }
    }
    class Main1
    {
        public static void Main(){
            var sample=new Sample();
            Console.WriteLine("MyValue is {0}",sample.MyValue);
            sample.MyValue=20;
            Console.WriteLine("MyValue is {0}",sample.MyValue);
        }
    }

输出结果

MyValue is 0
MyValue is 20

静态属性

我们的属性和一般字段一样也可以通过添加static关键字来使之变为类的静态成员,此后其便可在类没有创建示例时被直接访问,我们也可通过“using static …”的句式直接访问该静态属性

class Sample
    { 
        public static int MeValue{
            get;
            set;
        }
        public void PrintMeValue(){
            Console.WriteLine("MeValue is {0}",MeValue);
        }
    }
    class Main1
    {
        public static void Main(){
            var sample=new Sample();
            sample.PrintMeValue();
            Sample.MeValue=20;
            Console.WriteLine("MeValue is {0}",Sample.MeValue);
        }
    }
}

实例的构造函数

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

  1. 构造函数用于初始化实例的状态
  2. 如果希望能从类的外部创建类的实例则需要将构造函数声明为public
  3. 构造函数名称与类名一致
  4. 构造函数不能有返回值
class MyClass
{
	DateTime TimeOfInstantiation;	//字段
	public MyClass()	//构造函数
	{
		TimeOfInstantiation=DateTime.Now;	//初始化字段
	}
}

注:此处的Now是属于DateTime中的一个静态属性,其返回一个与DateTime新的DateTime对象实例的引用

class ConstructorMain
    {
        public DateTime DateOfInstantiation;
        public ConstructorMain(){
            DateOfInstantiation=DateTime.Now;
        }
    }
class Main1
    {
        public static void Main(){
            var obj=new ConstructorMain();
            var year=obj.DateOfInstantiation.Year;
            var month=obj.DateOfInstantiation.Month;
            var date=obj.DateOfInstantiation.Day;
            Console.WriteLine($"Instantiation Date is {year}/{date}/{month}");
        }
    }

带参数的构造函数

  • 构造函数可以带参数,参数的语法与其他方法完全相同
  • 构造函数可以被重载
    在使用创建对象表达式创建类的新实例是,要使用new运算符。后面跟着类的某个构造函数,如:
 class SingleClass
    {
        public int id;
        public string name;

        public SingleClass(){
            id=1;
            name="Nemo";
        }
        public SingleClass(int val){
            id=val;
            name="Nemo";
        }
        public SingleClass(string name){
            id=3;            
            this.name=name;
        }
    }
    class Program{
    static void Main(){
        var obj1=new SingleClass();
        var obj2=new SingleClass(2);
        var obj3=new SingleClass("Nemo");
        Console.WriteLine($"obj1->[id:{obj1.id},name:{obj1.name}]");
        Console.WriteLine($"obj1->[id:{obj2.id},name:{obj2.name}]");
        Console.WriteLine($"obj1->[id:{obj3.id},name:{obj3.name}]");
    }
    }

输出结果

obj1->[id:1,name:Nemo]
obj2->[id:2,name:Nemo]
obj2->[id:3,name:Nemo]

默认构造函数

如果我们没有显性的创建任何一个构造函数,则默认会存在一个没有参数的构造函数。但是一旦我们显性的创建了任何一个构造函数,那么这个默认的构造函数便不再可以使用。

静态构造函数

构造函数也可以被声明为static的。实例构造函数初始化类的每个新实例,而static构造函数初始化类级别的项。通常静态构造函数是用以初始化静态字段的。
以下为静态构造函数的几个注意事项:

  • 初始化类级别的项
    1. 在引用任何静态成员之前
    2. 在创建类的任何实例之前
  • 静态构造函数与实例构造函数类似
    1. 静态构造函数的方法名与实例构造函数一致
    2. 构造函数不能返回返回值
  • 静态构造函数在以下方面与实例构造函数不同
    1. 静态构造函数声明中使用static关键字
    2. 一个类只能有一个静态构造函数,且无参数
    3. 静态构造函数不能有访问修饰符

静态构造函数的使用

class RandomNumberClass
    {
// 	Random类可以用以创建一个随机数对象
        private static Random RandomKey;

        static RandomNumberClass(){
            RandomKey=new Random();
        }
        public int WriteRandom(){
// 			使用静态构造函数创建的对象来实际获取随机数
            return RandomKey.Next();
        }
    }
class Program{
    static void Main(){
        var random1=new RandomNumberClass();
        var random2=new RandomNumberClass();
        Console.WriteLine("Random1 is {0}",random1.WriteRandom());
        Console.WriteLine("Random1 is {0}",random2.WriteRandom());
    }
    }

输出结果:

Random1 is 282683994
Random2 is 305230632

对象初始化语句

如果一个类的字段为public的,那么该字段将可以使用对象初始化语句进行初始化,也就是类似Javascript的对象创建方法,其包含了两种,一种是带参数的,另一种是不带参数的:

  • new TypeName{FieldOrProp=InitExpr,FieldOrProp=InitExpr…}
  • new TypeName(ArgList){FieldOrProp=InitExpr,FieldOrProp=InitExpr…}
    实际上该中创建方法是对于构造函数的一种补充,其将会在对应的构造函数执行后被执行:
class Point
    {
        public int X=1;
        public int Y=2;
    }
 class Program{
    static void Main(){
        Point pt1=new Point();
        Point pt2=new Point{X=3,Y=4};
        Console.WriteLine($"pt1:[X={pt1.X},Y={pt1.Y}]");
        Console.WriteLine($"pt2:[X={pt2.X},Y={pt2.Y}]");
    }
    }

输出结果:

pt1:[X=1,Y=2]
pt2:[X=3,Y=4]

析构函数

析构函数(destructor) 执行在类的实例被销毁前需要的清理或释放非托管资源的行为。所谓的非托管资源是指通过Win32 API获得的文件句柄,或非托管内存块。使用.NET资源是无法得到它们的,但是如果坚持使用.NET类,则不需要为类编写析构函数。我们将在后文中再提起该函数

readonly关键字

字段可以使用readonly修饰符声明,其作用类似于将字段声明为const,一旦将字段是指为readonly则被设定的值便无法被改变。
readonly可以被声明的场合:

  • 字段的声明语句时被声明
  • 类的任意构造函数中,如果字段为static的则需要声明在静态构造函数中
    相较于const的优点:
    const是在声明时必须被定义,因此其是在编译时便已被确定了,但是readonly则可以在构造方法中被赋值,也就意味着其在运行中被定义,因此其声明更具有灵活性。
    *const的两个特点:
  • 它可以是实例字段也可以是静态字段,而const相对而言则表现为静态的偏向
  • 它在内存中将被分配内存
class Shape
    {
        readonly double PI=3.1415926d;
        readonly int NumberOfSides;

        public Shape(double side1,double side2)
        {
            // Shape表示一个矩形
            NumberOfSides=4;
            // ...
        }

        public Shape(double side1,double side2,double side3)
        {
            // Shape表示一个三角形📐
            NumberOfSides=3;
            //...
        }
    }
}

this关键字

this关键字是使用在类中,是对当前实例的引用。但它只允许被用于类成员的代码块中:

  • 实例构造函数中
  • 实例方法中
  • 属性和索引器的实例访问器(索引器将会在下文中提到)
    但是它并不适用于静态函数或者静态成员相关的代码中,因为,它是属于实例的代指,而静态变量则是属于类的范畴。更确切地说this只是为了区分类的成员和(局部变量和参数的)。其使用与java是一致的,这里不再赘述

索引器

假设我们定义一个类Employee,它有三个string类型的字段,那么可以使用字段名称访问它们:

		class Employee
    {
        public string FirstName="Genesis";
        public string LastName="Yang";
        public string CityName="Shanghai";
    }
    class Program
    {
        static void Main(){
            Employee e=new Employee();
            e.FirstName="Doe";
            e.LastName="Jane";
            e.CityName="Dallas";
            Console.WriteLine("{0}",e.FirstName);
            Console.WriteLine("{0}",e.LastName);
            Console.WriteLine("{0}",e.CityName);
        }
    }

但是我们可以使用索引来访问它们将会变得更加简单。此时我们的索引器便将整个类视作了一个数组,这便是索引器所作的事情。当我们使用了索引进行访问时我们可以使用 [index] 来代替传统的".(dot)"。此时它被称之为索引运算符

索引器的构造

索引器是一组get和set的访问器所构成的,这也与属性是类似的:

string this[int index]
{
set
{
SetAccessorCode
}
get
{
GetAccessorCode
}
}
索引器与属性

索引器与属性的相似点:

  • 和属性一样,索引器不用分配内存来进行存储
  • 它们均是用来访问其他的数据成员的,它们与这些成员关联。并为之提供获取和设置的访问

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

索引器的注意事项:

  • 和属性一样索引器可以和属性一样有一个或者多个访问器
  • 索引器总是实例成员,因此无法被声明为static类型的
  • 和属性一样,set和get可以干些什么也可以不干任何事情,只是get必须保证返回某个类型的值即可
声明索引器

索引器声明的注意事项:

  • 索引器没有名称,在名称的位置是关键字this
  • 参数列表在方括号中间
  • 参数列表中至少声明一个参数
ReturnType this[Type param1,...]
{
get
{
...
}
set
{
...
}
}

索引器的get访问器

我们可以通过该一个或多个索引参数来调用get访问器,索引参数决定了使用哪一个值:

string s=e[0]; //此时的0即为索引参数

get访问器方法内的代码必须检查索引参数,确定它表示的是哪个字段,并返回该字段的值。以下两者为等价的关系:
get访问器的定义

Type this[ParamList]
{
	get
	{
	AccessorBody
	return ValueOfType;
	}
	set
	{
	...
	}
}

get访问器的含义

Type get(ParamList)
{
	AccessorBody
	return ValueOfType;
}

索引器实例

class Employee
    {
        public string FirstName="Genesis";
        public string LastName="Yang";
        public string CityName="Shanghai";
        public string this [int index]{
            get
            {
                switch(index){
                    case 0:
                    return FirstName;
                    case 1:
                    return LastName;
                    case 2:
                    return CityName;
                    default:
                    throw new IndexOutOfRangeException("index");
                }
            }
            set
            {
                switch(index){
                    case 0:
                    FirstName=value;
                    break;
                    case 1:
                    LastName=value;
                    break;
                    case 2:
                    CityName=value;
                    break;
                    default:
                    throw new IndexOutOfRangeException("index");
                } 
            }
        }
    }

class Program
    {
        static void Main(){
            Employee e=new Employee();
            e[0]="Doe";
            e[1]="Jane";
            e[2]="Dallas";
            Console.WriteLine("{0}",e[0]);
            Console.WriteLine("{0}",e[1]);
            Console.WriteLine("{0}",e[2]);
        }
    }

索引器的重载

索引器和一般方法一样可以被重载。但索引器的重载类型的不同是不可的,因为索引器的名称都是一致的(即没有实际名称,而是统一为this)

class Sample
    {
        private string name;
        private int age;

        public string this [int idx]
        {
            set{
                switch(idx){
                    case 0:
                    name=value;
                    break;
                    default:
                    throw new IndexOutOfRangeException("idx");
                }
            }
            get{
                switch(idx){
                    case 0:
                    return name;
                    default:
                    throw new IndexOutOfRangeException("idx");
                }
            }
        }

        public string this [int idx,int index]
        {
            set{
                switch(idx){
                    case 0:
                    name=value;
                    break;
                    default:
                    throw new IndexOutOfRangeException("idx");
                }
            }
            get{
                switch(idx){
                    case 0:
                    return name;
                    default:
                    throw new IndexOutOfRangeException("idx");
                }
            }
        }

        public int this [float idx] //此处的参数不能再为int了,因为已有单个参数的索引器拥有了该参数列表,即使它并不是int类型的索引器
        {
            set{
                switch(idx){
                    case 0:
                    age=value;
                    break;
                    default:
                    throw new IndexOutOfRangeException("idx");
                }
            }
            get{
                switch(idx){
                    case 0:
                    return age;
                    default:
                    throw new IndexOutOfRangeException("idx");
                }
            }
        }
    }

说明: 请记住,每个索引器的参数列表都不可以相同

访问器的访问修饰符

我们前文中提到的属性和索引器均是有get和set访问器所构成的.事实上这两个访问器均可以独立的设置访问权限,只是需要遵循以下的规定:

  • 仅当成员(属性或者索引器)既有get访问器也有set访问器是,其访问器才能有访问修饰符
  • 虽然要求必须两个访问器同时声明,但是仅有一个访问器可以被赋予权限修饰符,即只读和只写只能选择一个
  • 访问器的权限必须比成员本身的访问权限更加严格

   class Person
   {
       public string Name{
           get;
           private set; //声明为只读属性
       }
       public Person(string name){
           Name=name;
       }
   }
   class Program
   {
       static void Main(){
           var p=new Person("Rongxin Yang");
           Console.WriteLine("Name is {0}",p.Name);
       }
   }

权限的分级:
public–>–>protected internal–>protected(internal)–>private

分部类和分部类型

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

  • 每个分部类的声明都含有一些类成员的声明
  • 类的分部类声明可以在同一个文件中也可以在不同的文件中
    每个分部类声明必须被标注为partial class,而不是class.分布类声明几乎一致只是多了partial修饰符.
partial class SampleAlpha
    {
        public int memberOne;
        public int memberTwo;
    }
    partial class SampleAlpha
    {
        public int memberThree;
        public int memberFour;

        public SampleAlpha(int a,int b,int c,int d)
        {
            memberOne=a;
            memberTwo=b;
            memberThree=c;
            memberFour=d;
        }
    }

class Program
    {
        static void Main(){
            var Sample=new SampleAlpha(1,2,3,4);
            Console.WriteLine($"Sample:[{Sample.memberOne},{Sample.memberTwo},{Sample.memberThree},{Sample.memberFour}]");
        }
    }

实际上分布类即将一个完整的类拆分为多个部分,它们可以合成一个完整的类.这也增加了类的拓展性和灵活性.
说明:类型修饰符partial不是关键字,所以在其他上下文中,可以在程序中把它用作是标识符.但直接使用在关键字class,struct或者interface上其表示分部类型

分部方法

我们可以像分部类那样声明分部方法.分部方法有两个部分组成:
声明分部方法:

  1. 给出签名和返回类型
  2. 声明部分的实现部分只是一个分号
    实现分部方法
  3. 给出签名和返回类型
  4. 以普通的语句块形式实现
    我们定义和实现声明的签名和返回类型必须匹配.签名和返回类型有如下特征:
  • 返回类型必须是void
  • 签名不能包括访问修饰符,这使分部方法是隐式私有
  • 参数列表不能包括out参数
  • 在定义声明和实现声明都必须包含上下文关键字partial,并且直接放在关键字void之前
partial class MyClass
    {
        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("The sum between {0} and {1} is {2}",x,y,x+y);
        }
    }
class Program
    {
        static void Main(){
            var MyClass=new MyClass();
            MyClass.Add(2,3);
        }
    }

输出结果:

The sum between 2 and 3 is 5

类和继承

类继承

通过继承我们可以定义一个新类,新类纳入一个已经声明的类并对其进行扩展:

  • 可以使用已存在的类作为新类的基础。已存在的类称之为基类(base class)。新类则成为派生类(derived class)。派生类成员的组成如下:
    1. 本身声明中的成员
    2. 基类的成员
  • 要声明一个派生类需要在类名后加入基类规格说明。基类规格说明由冒号和用作基类的类名称组成。派生类直接继承自列出的基类
  • 派生类扩展它的基类,因为它包含了基类的成员,还有它自身的新增功能
  • 派生类不能删除它所继承的任何成员
    这里OtherClass它将继承自名为SomeClass的类:
class OtherClass : SomeClass
{
	...
}

访问继承的成员

继承的成员可以被访问,就像它们是派生类自己声明的一样(继承的构造函数有些不同)

class SomeClass
    {
        public string Field1="base class field";
        public void Method1(string value){
            Console.WriteLine($"Base class -- Mehod1: {value}");
        }
    }

    class OtherClass:SomeClass{
        public string Field2="derived class dield";
        public void Method2(string value){
            Console.WriteLine($"Base class -- Mehod2: {value}");            
        }
    }
    class Program{
        static void Main(){
            OtherClass oc=new OtherClass();

            oc.Method1(oc.Field1);
            oc.Method1(oc.Field2);
            oc.Method2(oc.Field1);
            oc.Method2(oc.Field2);
        }
    }

所有类都派生自object类

除了特殊的类object外,所有的类都是派生类。即使它们并未规定规格说明。类object是唯一的非派生类,因为它是继承层次结构的基础
没有规定基类规格说明的类都是隐性地直接派生自object类。
关于继承的其他注意事项:

  • 一个类声明的基类规格说明只能有一个单独的类。这称之为单继承
  • 虽然类只能直接继承一个基类,但是派生的层次是没有限制的,也就是说基类也可以继承另一个基类…
    基类和派生类是相对的术语,所有的类都是派生类,要么派生自object,要么派生自一个基类,但是无论如何派生,最后总有一个类是只派生自object。

屏蔽基类的成员

我们前文提到了,我们无法在一个派生类中删除其基类的任何成员。但是我们可以使用和基类名称相同的成员来屏蔽基类的成员。这也是继承的主要功能之一。
派生类中屏蔽基类成员的一些要点:

  1. 要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员并需要重名
  2. 通过在派生类中声明新的带有相同签名的函数成员,可以屏蔽继承的函数成员。请注意:签名是由函数名和参数列表所组成的,其中并不包含函数的返回类型
  3. 要让编译器指导我们在故意屏蔽继承的类成员,此时我们可以使用new修饰符。否则程序虽然可以正常编译,但是它也会警告我们隐藏了一个继承的成员
  4. 我们可以屏蔽一个静态成员
		class Root
    {
        public string Field="Root Field";
        public void Method(string value){
            Console.WriteLine("Root method value: {0}",value);
        }
    }
    class SubClass:Root{
// 		使用new关键字屏蔽基类成员
        new public string Field="SubClass Field";
// 		使用new关键字屏蔽基类的函数
        new public void Method(string value){
            Console.WriteLine("SubClass method value: {0}",value);
        }
    }
    class Program{
        static void Main(){
            SubClass sub=new SubClass();
            sub.Method(sub.Field);
        }
    }
}

实际上熟悉Java的朋友都知道,我们在Java中也可以在字类中定义与父类相同的函数。此时Java将其称之为方法的重写(Override)。我们可以在方法上添加注解@Override来向编译器表名我们重写的意图,只是C#中我们换成了new关键字。事实上在Java中我们如果定义了与父类相同的字段,其亦视作为对于父类字段的隐藏,但是这种做法在Java中并不提倡。有些实际项目中甚至不允许这种操作。

访问基类

如果我们希望访问一个被屏蔽的基类的继承成员,则我们可以使用base access表达式。该表达式由关键字base.成员名的格式进行访问:

Console.WriteLine("{0}",base.Field);

此时我们调用了父类中被隐藏的Field成员

class SomeClass{
        public string Field="Field--In the base class";
    }    
    class OtherClass : SomeClass{
        new public string Field="Field --- In the derived class";
        public void printField()
        {
            Console.WriteLine(Field);
            Console.WriteLine(base.Field);
        }
    }
    class Program{
        static void Main(){
            OtherClass other=new OtherClass();
            other.printField();
        }
    }
}

虽然我们可以使用base来引用父类中被隐藏的属性,但是这并不推荐,因为我们或许存在更加合理的类的设计。

使用基类的引用

派生类的实例由基类的实例和派生类的新增成员组成。派生类的引用指向整个类对象,其中包含了基类的部分。
我们可以将一个派生类的引用强制转换为一个基类的引用,因为派生类是属于基类的拓展,因此,一旦转换完成后,转换后的引用将不再可以调用派生成员的成员了:

class RootClass
    {
        public string Field="Root Field";
        public void print(){
            Console.WriteLine(Field);
        }
    }
class DerivedClass : RootClass
    {
        new public string Field="Derived Field";
        public string NewField="NewField";
        new public void print(){
            Console.WriteLine(Field);
        }
    }
class Program{
        static void Main(){
// 					 声明一个派生类
            DerivedClass derived=new DerivedClass();
						 derived.print();
            Console.WriteLine(derived.NewField);
//          将派生类的引用转为基类引用
		  				RootClass root=(RootClass)derived;
            root.print();
//          Console.WriteLine(root.NewField); 由于基类没有访问派生类的NewField成员的权限因此编译不通过
        }
    }

虚方法和覆写方法

前文中我们得知当我们使用基类引用是无法访问派生类的成员的。但是C#为我们提供了一种虚方法。该方法可以使基类的引用访问“升至”派生类中。
我们可以使用基类引用来调用派生类的方法需要满足以下的条件:

  • 派生类的方法和基类的方法有相同的签名和返回类型
  • 基类的方法使用了virtual标注
  • 派生类的方法使用了override进行标注
  class BaseClass
    {
        virtual public void print(){
            Console.WriteLine("Root Print");
        }
    }
    class Derived:BaseClass
    {
        public override void print()
        {
            Console.WriteLine("Derived class Print");
        }
    }
	  class Program{
        static void Main(){
            var derived=new Derived();
            derived.print();
            BaseClass baseObj=(BaseClass)derived;
            baseObj.print();
        }
    }

此时输出的结果为

Derived class Print
Derived class Print

由此我们可以得出结论当我们在基类中定义了一个virtual的方法,并且该方法被派生类(Override)覆写了,此时我们如果将派生类的引用强制转为了基类的引用则即使我们使用了强制转换后的基类的引用也可以调出被派生类覆写后的的函数

注意事项
  1. 覆写和被覆写的方法必须有相同的可访问性
  2. 不能覆写static方法和非虚方法
  3. 方法,属性,索引器乃至后文中将要学习的事件成员全部都支持被声明为virtual和override

覆写标记为override的方法

覆写方法可以在继承的任何层次出现:

  • 当使用的对象基类部分的引用调用了一个被覆写的方法时,方法的调用沿派生层次向上追溯执行,一直到override的方法的**最高派生(most-derived)**版本
  • 如果在更高的派生级别有该方法的其他声明但是没有被标记为override,则它们便不会被调用:
    class MyBaseClass
    {
        virtual public void print(){
            Console.WriteLine("This is the base class");
        }        
    }
    class MyDerivedClass:MyBaseClass
    {
        override public void print(){
            Console.WriteLine("This is the derived class");
        }
    }
    class SecondDerivedClass:MyDerivedClass
    {
        public override void print()
        {
            Console.WriteLine("This is the second derived class");
        }
    }
    class Program
    {
        static void Main()
        {
            SecondDerivedClass derivedClass=new SecondDerivedClass();
            MyBaseClass baseClass=(MyBaseClass)derivedClass;
            derivedClass.print();
            baseClass.print();
        }
    }

此时输出:

This is the second derived class
This is the second derived class

但是如果我们使用了new关键字而不是override时:

    class MyBaseClass
    {
        virtual public void print(){
            Console.WriteLine("This is the base class");
        }        
    }
    class MyDerivedClass:MyBaseClass
    {
        new public void print(){
            Console.WriteLine("This is the derived class");
        }
    }
    class SecondDerivedClass:MyDerivedClass
    {
        public override void print()
        {
            Console.WriteLine("This is the second derived class");
        }
    }
    class Program
    {
        static void Main()
        {
            SecondDerivedClass derivedClass=new SecondDerivedClass();
            MyBaseClass baseClass=(MyBaseClass)derivedClass;
            derivedClass.print();
            baseClass.print();
        }
    }

此时输出的结果为:

This is the second derived class
This is the derived class

由此我们我们可以得出结论:
无论print是通过派生类对象还是通过基类进行调用的,都会调出最下级的派生类的那个方法(使用了override修饰)。但是如果我们使用了new关键字修饰则派生类对象将会调用new的方法,但是如果通过基类来进行调用则它会向上传递一级直到遇到最高一级的override的覆写方法,如果没有,则会最终调用基类自己的方法:

 class MyBaseClass
    {
        virtual public void print(){
            Console.WriteLine("This is the base class");
        }        
    }
    class MyDerivedClass:MyBaseClass
    {
    }
    class SecondDerivedClass:MyDerivedClass
    {
        public new void print()
        {
            Console.WriteLine("This is the second derived class");
        }
    }
    class Program
    {
        static void Main()
        {
            SecondDerivedClass derivedClass=new SecondDerivedClass();
            MyBaseClass baseClass=(MyBaseClass)derivedClass;
            derivedClass.print();
            baseClass.print();
        }
    }

输出结果为:

This is the second derived class
This is the base class

其他成员的覆盖

事实上其他类型成员的覆盖与前面的方法的覆写也是如出一辙,因此这里我们不加赘述了

构造函数的执行

前文中我们已经看到了构造函数执行代码来准备一个即将被使用的类。这包括了初始化类型的静态成员和实例成员。在这一节我们将看到一个派生类对象中有一部分是基类对象:

  • 要创建一个基类部分,需要隐性地调用基类的某个构造函数
  • 继承层次链中的每个类在执行它自己的构造函数体之前会首先调用其基类的构造函数

如:

class MyDerivedClass:BaseClass
{
	int MyField1=5;			//1.首先进行成员的初始化
	int myField2;
	public MyDerivedClass()  //3.调用自己的构造函数
	{
	...
	}
}
class BaseClass
{
	public BaseClass()  //2.调用基类的构造函数
	{
	...
	}
}

注意:C#强烈反对在构造函数中调用虚方法。在执行基类的构造方法是,基类的虚方法会调用派生类的覆写方法。因此,该调用会在派生类完全初始化之前被传递到派生类中

构造函数初始化语句

我们在创建对象时默认将会调用一个无参数的构造函数,而构造函数是支持重载的。所以一个类存在多个构造函数时,如果希望一个派生类使用一个制定的基类函数来构造函数而不是使用无参的构造函数,那么我们便需要在构造函数初始化语句中指定它:

class BaseClass
    {
        public string fieldAlpha;
        public int fieldBeta;
        public BaseClass()
        {
            fieldAlpha="alpha";
            fieldBeta=22;
        }
        public BaseClass(string alpha,int beta)
        {
            fieldAlpha=alpha;
            fieldBeta=beta;
        }
    }
    class DerivedClass:BaseClass
    {
        public string Alpha;
        public bool Beta;
        public DerivedClass(string alpha,bool beta):base(alpha,33)//指定实现基类的双参数构造函数,否则默认实现无参构造函数
        {
            Alpha=alpha;
            Beta=beta;
        }
    }
    class MainClass
    {
        public static void Main(){
            DerivedClass derivedClass=new DerivedClass("derivedAlpha",true);
            Console.WriteLine(derivedClass.fieldAlpha);
            Console.WriteLine(derivedClass.fieldBeta);
            Console.WriteLine(derivedClass.Alpha);
            Console.WriteLine(derivedClass.Beta);
        }
    }

此时我们将得到结果为:

derivedAlpha
33
derivedAlpha
True

如果我们没有指定构造函数初始化语句,即 “:base(alpha,33)”,则我们将得到:

alpha
22
derivedAlpha
True

此时如果我们继续将基类的无参数构造函数去掉,则编译会异常,因为唯一的BaseClass的双参数构造函数也未传入预期的参数。总之如果基类希望使用某一个特殊的构造函数创建对象,则必须实现至少一个基类的构造函数,如,下面的代码也将导致编译的异常:

class BaseClass
    {
        public string fieldAlpha;
        public int fieldBeta;
        // public BaseClass()
        // {
        //     fieldAlpha="alpha";
        //     fieldBeta=22;
        // }
        public BaseClass(string alpha,int beta)
        {
            fieldAlpha=alpha;
            fieldBeta=beta;
        }
         public BaseClass(string alpha)
        {
            fieldAlpha=alpha;
            fieldBeta=11;
        }
    }
    class DerivedClass:BaseClass
    {
        public string Alpha;
        public bool Beta;
        public DerivedClass(string alpha,bool beta)
        {
            Alpha=alpha;
            Beta=beta;
        }
    }
    class MainClass
    {
        public static void Main(){
            DerivedClass derivedClass=new DerivedClass("derivedAlpha",true);
            Console.WriteLine(derivedClass.fieldAlpha);
            Console.WriteLine(derivedClass.fieldBeta);
            Console.WriteLine(derivedClass.Alpha);
            Console.WriteLine(derivedClass.Beta);
        }
    }

如果我们希望避免错误,我们则必须声明至少一个构造函数初始化语句来实现一个基类已有的构造函数(比如下面的BaseClass的无参构造函数由于已被注释因此不可以实现):

class BaseClass
    {
        public string fieldAlpha;
        public int fieldBeta;
        // public BaseClass()
        // {
        //     fieldAlpha="alpha";
        //     fieldBeta=22;
        // }
        public BaseClass(string alpha,int beta)
        {
            fieldAlpha=alpha;
            fieldBeta=beta;
        }
         public BaseClass(string alpha)
        {
            fieldAlpha=alpha;
            fieldBeta=11;
        }
    }
    class DerivedClass:BaseClass
    {
        public string Alpha;
        public bool Beta;
        public DerivedClass(string alpha,bool beta):base(alpha)//此时我们实现了一个基类已有的单参数构造方法
        {
            Alpha=alpha;
            Beta=beta;
        }
    }
    class MainClass
    {
        public static void Main(){
            DerivedClass derivedClass=new DerivedClass("derivedAlpha",true);
            Console.WriteLine(derivedClass.fieldAlpha);
            Console.WriteLine(derivedClass.fieldBeta);
            Console.WriteLine(derivedClass.Alpha);
            Console.WriteLine(derivedClass.Beta);
        }
    }

此时输出结果为:

derivedAlpha
11
derivedAlpha
True

C#中的访问级别

C#中类的可访问级别分为了internal和public两种。其中标记为public的类可以被系统内的任何程序集中的代码所访问。而被标记为internal的类则只能被它自己所在的程序集的类所看到

程序集间的继承

目前我们一直在基类声明的同一个程序集内声明派生类。但是c#支持我们在一个不同的程序集中定义基类的派生类,但需要具备以下的条件:

  • 基类必须被声明为public,这样才能从它所在的程序集外进行访问
  • 必须在Virtual Studio的工程中的References节点中添加对包含该基类的程序集的引用。我们可以在Solution Explorer中找到
    注意:增加对于其他程序集的引用和增加using指令是两回事。我们增加程序的引用是告诉编译器所需要的类型在哪里定义。而增加using指令则允许我们引用其他的类而不必使用它们的完全限定名

成员访问修饰符

C#中总共有五种级别的成员访问修饰符:

  • public
  • private
  • protected
  • internal
  • protected internal

我们可以从四个维度来区分这五种界别,分别是同一程序集且继承,同一程序集但是不继承。不同程序集但继承和不同程序集且不继承:

修饰符同一程序集且继承同一程序集但是不继承不同程序集但继承不同程序集且不继承
public
protected internal
internal
protected
private

抽象成员

抽象成员是指设计为被覆写的函数成员。抽象成员将需要具备以下的特征。

  • 必须是一个函数成员,这也就意味着字段和常量将无法作为一个抽象成员
  • 必须使用abstract修饰符进行标记
  • 不能有实现代码块。抽象成员的代码需要使用分号表示(即没有实现语句)
    抽象成员只能在抽象类中声明,一共有四种类型的成员可以被声明为抽象的:
  1. 方法
  2. 属性
  3. 事件
  4. 索引器

注意事项:

  • 尽管抽象成员必须在派生类中使用相应的成员覆写,但不能把virtual修饰符附加到acstract修饰符
  • 类似于虚成员,派生类中抽象成员的实现必须指定override修饰符
虚成员与抽象成员
虚成员抽象成员
关键字virtualabstract
实现体有实现体无实现体
在派生类中被覆写能被覆写,使用override必须被覆写,使用override
成员的类型方法
属性
事件
索引器
方法
属性
事件
索引器

抽象类

抽象类是指被设计来被继承的类。抽象类只能被用作是其他类的基类

  • 不能用来创建实例对象
  • 抽象类使用abstract作为修饰符声明。
abstract class AbClass{
	...
}
  • 抽象类是可以继承自另一个抽象类:
abstract class AbClass{
	...
}
abstract class MyAbClass:AbClass{
	...
}
  • 任何派生自抽象类的类都必须使用override关键字实现该类的所有抽象成员,除非它自己也是抽象类。
抽象类实例
using System;
namespace MAINSPACE
{

 abstract class BaseClass{
    public void IdentifyBase(){
        Console.WriteLine("I am BaseClass");
    }
    abstract public void IdentifyDerived();
 }   

class DerivedClass : BaseClass{
        public override void IdentifyDerived()
        {
            Console.WriteLine("I am DerivedClass");
        }
    }

class Program{
     static void Main(){
        DerivedClass derived=new DerivedClass();
        derived.IdentifyBase();
        derived.IdentifyDerived();
    }
}

}
另一个实例

声明一个包含非抽象成员的抽象类(注意数据成员,字段或者常量无法被声明为抽象的):

abstract class MyBase{
    public int SideLength =10; //定义非抽象的字段成员
    const int TriangleSideCount=3; //定义非抽象的常量成员
    abstract public void PrintStuff(string s); //定义一个抽象的方法成员
    abstract public int MyInt{get;set;} //定义一个抽象的MyInt属性成员
    public int PerimetierLength(){ //一个非抽象的函数
        return TriangleSideCount*SideLength;
    }
    
}
class MyClass:MyBase{
        public override void PrintStuff(string s)
        {
            Console.WriteLine(s);
        }
        private int _myInt;
        public override int MyInt { 
            get{return _myInt;} 
            set{_myInt=value;}
        }
    }
class Program{
    static void Main(){
        MyClass obj=new MyClass();
        obj.PrintStuff("This is a string!");
        obj.MyInt=26;
        Console.WriteLine(obj.MyInt);
        Console.WriteLine($"Perimeter Length:{obj.PerimetierLength()}");
    }
}
密封类

我们提到抽象类必须作为基类使用,它不能像独立的类对象那样被实例化,而密封类则相反。

  • 密封类只能用作独立的类,它不能被用作基类
  • 密封类使用sealed修饰符进行标注
sealed class MyClass{
...
}
静态类

静态类的所有成员都是静态的。静态类用于存放不受实例数据影响的数据和函数。它的常用用途是创建一个包含一组数学方法和值的数学库。
静态类的注意事项:

  • 类本身必须被标注为static
  • 类的成员必须是静态的
  • 类可以有一个静态构造函数,但不能有实例构造函数, 因为不能创建该类的实例
  • 静态类是隐式密封的,因此我们是无法继承的
static public class MyMath{
    public static float PI=3.1415926f;
    public static bool IsOdd(int x){
        return x%2==1;
    }
    public static int Times2(int x){
        return 2*x;
    }
}

class Program{
    static void Main(){
        int val=3;
        Console.WriteLine("{0} is odd is {1}",val,MyMath.IsOdd(val));
        Console.WriteLine($"{val}*2={MyMath.Times2(val)}");
    }
}

扩展方法

迄今为止,我们发现每个方法都是和表明它的类相关联。这是面向对象语言的一个特征,Java也是这样的。而C#为我们提供了一个扩展方法这个特性,它使得我们得以编写的方法和声明它的类之外的类相关:

class MyData
{
    private double D1;
    private double D2;
    private double D3;
    public MyData(double d1,double d2,double d3)
    {
        D1=d1;D2=d2;D3=d3;
    }
    public double sum()
    {
        return D1+D2+D3;
    }
}

上面我们创建了一个简单的MyData类,其中主要包含三个字段,并有一个方法用以返回三个字段的和。很显然这个类的功能很单一,为了对该类的功能进行扩展,我们可以采用下面几种方式:

  • 如果我们有源代码,并且该源代码是运行被修改的,那么我们进行直接添加即可
  • 然而,如果该类无法被修改(作为第三方库进行导入的)。但是只要该类不是密封的,那么我们便可以用它作为基类对其进行功能的扩展
    我们注意到当类是密封的或者无法被修改的那么我们便手足无措了。为此我们便需要借助该类的共有成员在另一个使用该类共有成员的类中来编写扩展方法了:
sealed class MyData
{
    private double D1;
    private double D2;
    private double D3;
    public MyData(double d1,double d2,double d3)
    {
        D1=d1;D2=d2;D3=d3;
    }
    public double Sum()
    {
        return D1+D2+D3;
    }
}
static class ExtendMyData //这里必须是一个静态类,否则我们便需要创建该类的对象了
{
    //被扩展的方法因为是需要被调用因此需要是公共的,又因为在静态类中因此还需要是静态的
    public static double Average(this MyData md)//注意参数除了传入对象外还需要添加一个this关键字
    {
        return md.Sum()/3;
    }
}
class Program
{
    static void Main()
    {
        MyData md=new MyData(3,4,5);
        Console.WriteLine("Average:{0}",ExtendMyData.Average(md));
    }
}

表达式和运算符

表达式

运算符是一个符号,它返回某个结果的操作。操作数则是作为运算符输入的数据元素。因此运算符将会进行一下操作:

  1. 将操作数作为输入
  2. 执行某个操作
  3. 基于该操作返回一个值
    表达式则是运算符和操作数组成的字符串。C#的运算符有一个,两个或者三个操作数。可被作为操作数的结构有:
  • 字面量
  • 常量
  • 变量
  • 方法调用
  • 元素访问器,如数组访问器或者索引器
  • 其他的表达式
    表达式求值将每个运算符以适合的顺序应用到它的操作数以产生一个值的过程
  1. 值被返回到表达式求值的位置。在那里,它可能是一个封闭的表达式的操作数
  2. 除了返回值以外,一些表达式还有副作用,比如在内存中设置一个值
字面量

就是实际的值,它们就是明确类型的确定值。如1,“Genesis”,true…这样的值(注意C#的布尔值字面量是小写的正如其他的关键字一样,python的布尔值首字母才是大写的)
整数字面量
整数字面量是最常用的字面量,它们被书写为十进制数字的序列,并且:

  • 没有小数点
  • 带有可选的后缀,指明了整数的类型
	236			//整数
	235L	 //长整数
	235U   //无符号整数
	235UL  //无符号长整数

说明:仅有整数类型字面量可以使用十六进制或者二进制格式进行表示。二进制表示需要添加前缀0x或者0X(数字0和字母X)
实数字面量
C#有三种实数类型:float,double和decimal。它们的精度分别是32位,64位和128位。它们三者都是浮点类型,这意味着它们内部是由两部分构成的,其中一部分是实际的数字,另一部分则表示的是小数点的指数。一般double使用较为频繁。
实数字面量的组成如下:

  • 十进制数字
  • 一个可选的小数点
  • 一个可选的指数部分
  • 一个可选的后缀
    以下为实数类型自变量的不同格式
float f1=236f;
double d1=236.714;
double d2=.32157
double d3=5.235e3(表示5.235*10^3)

后缀:

后缀实数类型
double
F,ffloat
D,ddouble
M,mdecimal

字符字面量
字符字由两个单引号内的字符组成。其表示单个字符可用于打印换行符或者转义。字符字字面量可以是:单个字符,一个简单的转义序列,一个十六进制的转义序列或一个Unicode转义序列:

class Program
{
    static void Main()
    {
        char c1='d'; //单个字符
        char c2='\n'; //换行符
        char c3='\x0061'; //十六进制转义字符a
        char c4='\u005a'; //Unicode转义字符Z
        Console.WriteLine(c1);
        Console.WriteLine(c2);
        Console.WriteLine(c3);
        Console.WriteLine(c4);
    }
}

几个常见的特殊字符

名称转义序列十六进制编码
空字符\00x0000
警告\a0x0007
退格符\b0x0008
水平制表符\t0x0009
换行符\n0x000A
垂直制表符\v0x000B
换页符\f0x000C
回车符\r0x000D
双引号"0x0022
单引号0x0027
反斜杠\0x005C
字符串字面量

字符串自变量使用双引号进行标记。这里存在两种字符串字面量:

  • 常规字符串字面量
  • 逐字字符串字面量

常规字符串字面量由双引号内的字符序列组成:

  1. 字符
  2. 简单转义序列
  3. 十六进制和Unicode转义序列

常规字符串

string str1="Hi there!";
string str2="Val1\t5,Val2\t10";
string str3="Add\x000ASome\u0007Interest";
Console.WriteLine(str1);
Console.WriteLine(str2);
Console.WriteLine(str3);

逐字字符串
逐字字符与前者的不同是逐字字符将不会对字符串内容进行转义,不过转义字符中存在一个例外:逐字字符串将会将两个相邻的双引号才能被转换为一个双引号。一个逐字字符串由一个 @+普通字符串所组成

 string rst1="Hi there!";
        string vst1=@"Hi there!";

        string rst2="It started,\"Four score and seven...\"";
        string vst2=@"It started,""Four score and seven...""";

        string rst3="Value 1 \t 5,Val2 \t10";
        string vst3=@"Value 1 \t 5,Val2 \t10";

        string rst4="C:\\Program Files\\Microsoft";
        string vst4=@"C:\Program Files\Microsoft";

        string rst5="Print \x000A Mutiple \u000A Lines";
        string vst5=@"Print
000A Mutiple 
Lines";

        Console.WriteLine(rst1);
        Console.WriteLine(vst1);
        Console.WriteLine();

        Console.WriteLine(rst2);
        Console.WriteLine(vst2);
        Console.WriteLine();

        Console.WriteLine(rst3);
        Console.WriteLine(vst3);
        Console.WriteLine();

        Console.WriteLine(rst4);
        Console.WriteLine(vst4);
        Console.WriteLine();

        Console.WriteLine(rst5);
        Console.WriteLine(vst5);
        Console.WriteLine();

        Console.WriteLine(rst1);
        Console.WriteLine(vst1);
        Console.WriteLine();            
Typeof 和 Nameof

typeof
使用Typeof将会返回作为其参数的任何类型的System.Type。通过这个Type类型的对象我们可以了解该类型的特征,比如其内部的字段或者方法的信息等,也就是所谓的反射机制:

using System.Reflection;
class SomeClass
{
    public int Field1;
    public int Field2;
    public void Method1(){}
    public int Method2(){return 0;}
}
class Program
{
    static void Main(){
        Type t=typeof(SomeClass);
        FieldInfo[] fi=t.GetFields();
        MethodInfo[] mi=t.GetMethods();

        foreach (FieldInfo info in fi)
        {
            Console.WriteLine($"Field:{info.Name}");
        }

        foreach (MethodInfo info in mi)
        {
            Console.WriteLine($"Method:{info.Name}");
        }
    }
}

nameof
用以返回表示变量,类型或者成员的字符串

    string var1="Local Variable";
    Console.WriteLine(nameof(var1));
    Console.WriteLine(nameof(SomeClass));
    Console.WriteLine(nameof(SomeClass.Method1));
    Console.WriteLine(nameof(SomeClass.Field1));

此时返回的结果为:

var1
SomeClass
Method1
Field1

注:需要注意的是即使我们位nameof的类为类的全限定名,它返回的结果也将是非全限定名

语句

语句就是某个类型或使程序执行某个动作的源代码指令,指令主要分为三种类型:

  1. 声明语句: 声明类型或者变量
  2. 嵌入语句: 执行动作或者管理控制流
  3. 标签语句: 控制跳转

块和简单语句:
简单语句是由一个表达式和后面跟着的分号组成,而块是由大括号包括的语句序列,其中被包括的内容可以是:

  • 声明语句
  • 嵌入语句
  • 标签语句
  • 嵌套块
        int x=10; //简单语句
        int z; //简单语句
        {  //块
            int y=20; //简单语句
            z=x+y; //嵌入语句
        top:y=10; //标签语句
        {   //嵌套块
            
        }
        }

控制流语句

  • 条件执行语句
    1. if
    2. if…else
    3. switch
  • 循环语句
    1. while
    2. do
    3. for
    4. foreach
  • 跳转语句
    1. break
    2. continue
    3. return
    4. goto
    5. throw

标签的使用

标签语句有一个标识符后面跟着一个冒号加上语句构成:

Identifier:Statement

特点

  • 该语句允许其他部分的代码转移到该语句
  • 标签语句的作用范围是代码块范围内
  • 标签语句的标签甚至可以与变量名重名,但是不能是关键字
  • 同一个重叠范围内不能存在两个相同的标签名
            int idx=0;
            idx:Console.WriteLine(idx);
            while (idx<2)
            {
                idx++;
                goto idx;
            }

输出结果为:

0
1
2

如果稍微调整一下语句的位置

            int idx=0;
            while (idx<2)
            {
                idx++;
                goto idx;
            }
						 idx:Console.WriteLine(idx);

此时的结果为:

1
goto在switch中的用法

goto语句也可以使用在switch中:

			int i=Convert.ToInt16(Console.ReadLine());
            switch (i)
            {
                case 1:
                    Console.WriteLine("Print 1");
                    break;
                case 2:
                    Console.WriteLine("Print 2");
                    break;
                case 0:
                    goto case 1;
                case -1:
                    goto default;
                default: 
                    Console.WriteLine("Default branch");
                    break;
            }

using语句

using既可以用于指令表示引入某个包(如,使用System.Math);也可以用在这里作为语句。using语句的作用是用在某些类型的非托管对象上,因为这些对象有数量限制或者十分消耗系统资源。我们需要在使用后尽快的释放该资源。使用using语句便有助于简化该过程并确保这些资源被适当的处置。
资源指的是实现了System.IDisposable接口的类或者结构。该接口含有一个单独的名为Disposable的方法。
使用资源有三个阶段:

  • 分配资源
  • 使用资源
  • 处置资源
    使用资源一般由两种方式:

第一种形式

  • 圆括号内的代码分配资源
  • Statement是使用使用资源的代码
  • using语句隐性产生处置该资源的语句
using (ResourceType Identifer = Expression) Statement
使用的实例
using System.IO;
    class Program
    {
        static void Main()
        {
			//使用using语句
            using (TextWriter tw=File.CreateText("Lincoln.txt"))
            {
                tw.WriteLine("Four scores and seven years ago,...");
            }
			//使用using语句
            using (TextReader tr=File.OpenText("Lincoln.txt"))
            {
                string InputString;
                while (null!=(InputString=tr.ReadLine()))
                {
                 Console.WriteLine(InputString);   
                }
            }
        }
    }

我们注意到我们使用这种方式调用资源并不需要像Java那样手动地关闭资源
using语句也可以用于相同类型的多个资源,资源声明使用逗号隔开:

using (ResourseType Id1=Expr1,Id2=Expr2...)EmbbeddedStatement
使用实例
            using (TextWriter tw1 = File.CreateText("Lincoln.txt"),
                   tw2 = File.CreateText("Franklin.txt"))
            {
                tw1.WriteLine("Four scores and seven years ago...");
                tw2.WriteLine("Early to bed;Early to rise...");
            }

            using (TextReader tr1=File.OpenText("Lincoln.txt"),
                   tr2=File.OpenText("Franklin.txt"))
            {
                string InputString;
                while (null!=(InputString=tr1.ReadLine()))
                {
                    Console.WriteLine(InputString);
                }

                while (null!=(InputString=tr2.ReadLine()))
                {
                    Console.WriteLine(InputString);
                }
            }

第二种形式

using(Expression)EmbeddedStatement

使用该种方式进行调用我们首先需要在表达式外首先声明该资源:

            TextWriter tw = File.CreateText("Lincoln.txt");
            // tw.Dispose(); 我们可以在使用该资源前就将其Dispose
            using (tw)
            {
                tw.WriteLine("Four scores and seven years ago...");
            }

使用该种方式我们可以保证该资源可以被Dispose掉,但我们无法保证在使用该资源时会不会资源就已经被Dispose了

  • 11
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值