C#学习笔记

C#

基础程序结构

using System;// using 关键字用于在程序中包含 System 命名空间。 一个程序一般有多个 using 语句。
namespace HelloWorldApplication //一个 namespace 里包含了一系列的类。HelloWorldApplication 命名空间包含了类 HelloWorld。
{
   /*class 声明。类 HelloWorld 包含了程序使用的数据和方法声明。类一般包含多个方法。方法定义了类的行为。在这里,HelloWorld 类只有一个 Main 方法。*/
   class HelloWorld
   {
      static void Main(string[] args)
      {
         /* 我的第一个 C# 程序*/
         Console.WriteLine("Hello World");
         Console.ReadKey();//Console.ReadKey(); 是针对 VS.NET 用户的。这使得程序会等待一个按键的动作,防止程序从 Visual Studio .NET 启动时屏幕会快速运行并关闭。
      }
   }
}

基本语法

using System;
namespace RectangleApplication
{
    class Rectangle
    {
        // 成员变量
        double length;//声明一个double类型的变量length 
        double width;//声明一个double类型的变量width
        public void Acceptdetails() //一个用来赋值的方法
        {
            length = 4.5;    
            width = 3.5;
        }
        public double GetArea() //一个用来计算的方法
        {
            return length * width;
        }
        public void Display()  //一个用来打印的方法
        {
            Console.WriteLine("Length: {0}", length);
            Console.WriteLine("Width: {0}", width);
            Console.WriteLine("Area: {0}", GetArea()); //打印GetArea方法的计算结果
        }
    }
    
    class ExecuteRectangle
    {
        static void Main(string[] args) //程序入口方法,创建实例,调用相应的方法
        {
            Rectangle r = new Rectangle();
            r.Acceptdetails();
            r.Display();
            Console.ReadLine();
        }
    }
}

在这段代码中,逻辑是这样的:

  • 首先进入 Main 方法,创建一个名称为 r 的实例。
  • 然后调用 Acceptdetails 给 r 进行赋值。
  • 最后调用 Display 方法打印结果。
  • 而用于计算的 GetArea 方法在在调用 Display 时直接打印出来。

数据类型

类型 描述 范围 默认值
bool 布尔值 True 或 False False
byte 8 位无符号整数 0 到 255 0
char 16 位 Unicode 字符 U +0000 到 U +ffff '\0'
decimal 128 位精确的十进制值,28-29 有效位数 (-7.9 x 1028 到 7.9 x 1028) / 100 到 28 0.0M
double 64 位双精度浮点型 (+/-)5.0 x 10-324 到 (+/-)1.7 x 10308 0.0D
float 32 位单精度浮点型 -3.4 x 1038 到 + 3.4 x 1038 0.0F
int 32 位有符号整数类型 -2,147,483,648 到 2,147,483,647 0
long 64 位有符号整数类型 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 0L
sbyte 8 位有符号整数类型 -128 到 127 0
short 16 位有符号整数类型 -32,768 到 32,767 0
uint 32 位无符号整数类型 0 到 4,294,967,295 0
ulong 64 位无符号整数类型 0 到 18,446,744,073,709,551,615 0
ushort 16 位无符号整数类型 0 到 65,535 0

装箱和拆箱

int val = 8;
object obj = val;//整型数据转换为了对象类型(装箱)

拆箱:之前由值类型转换而来的对象类型再转回值类型, 实例:

int val = 8;
object obj = val;//先装箱
int nval = (int)obj;//再拆箱

只有装过箱的数据才能拆箱

动态(Dynamic)类型

您可以存储任何类型的值在动态数据类型变量中。这些变量的类型检查是在运行时发生的。

声明动态类型的语法:

dynamic <variable_name> = value;

例如:

dynamic d = 20;

动态类型与对象类型相似,但是对象类型变量的类型检查是在编译时发生的,而动态类型变量的类型检查是在运行时发生的。

字符串(String)类型

字符串(String)类型 允许您给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。字符串(String)类型的值可以通过两种形式进行分配:引号和 @引号。

例如:

String str = "runoob.com";

一个 @引号字符串:

@"runoob.com";

C# string 字符串的前面可以加 @(称作"逐字字符串")将转义字符(\)当作普通字符对待,比如:

string str = @"C:\Windows";

等价于:

string str = "C:\\Windows";

@ 字符串中可以任意换行,换行符及缩进空格都计算在字符串长度之内。

string str = @"<script type=""text/javascript"">
    <!--
    -->
</script>";

指针类型(Pointer types)

指针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。

声明指针类型的语法:

type* identifier;

例如:

char* cptr;
int* iptr;

类型转换

类型转换从根本上说是类型铸造,或者说是把数据从一种类型转换为另一种类型。在 C# 中,类型铸造有两种形式:

  • 隐式类型转换 - 这些转换是 C# 默认的以安全方式进行的转换, 不会导致数据丢失。例如,从小的整数类型转换为大的整数类型,从派生类转换为基类。
  • 显式类型转换 - 显式类型转换,即强制类型转换。显式转换需要强制转换运算符,而且强制转换会造成数据丢失。

隐式转换:

C# 默认的以安全方式进行的转换。本质是从小存储容量数据类型自动转换为大存储容量数据类型,从派生类转换为基类。

实例:

namespace TypeConvertion
{   class Class1
    {

    }
    class Class2 : Class1 //类Class2是类Class1的子类
    {

    }
    class Program
    {
        static void Main(string[] args)
        {
            int inum = 100;
            long lnum = inum; // 进行了隐式转换,将 int 型(数据范围小)数据转换为了 long 型(数据范围大)的数据
            Class1 c1 = new Class2(); // 这里也是隐式转换,将一个新建的 Class2 实例转换为了其基类 Class1 类型的实例 C1
        }
    }
}

显式转换:

通过用户使用预定义的函数显式完成的,显式转换需要强制转换运算符。转换类型的范围大小和从属关系和隐式转换相反。显式转换可能会导致数据出错,或者转换失败,甚至无法编译成功。

实例:

double dnum = 100.1;
int ifromd = (int)dnum; //double类型显式转换转为int类型


Class1 c11 = new Class1();
Class2 c22 = c11 as Class2; //使用as进行显式转换
Console.WriteLine(c22 is Class1);
Console.WriteLine(c22 is Class2);

/*运行结果:
FALSE
FALSE
*/

强制转换类型的方法

  1. Convert 和 Parse

    string locstr = 123.ToString();
    
    //如果要将"locstr"转成整型数
    
    //方法一: 用 Convert 
    int i = Convert.ToInt16(locstr);
    
    //方法二: 用 Parse
    int ii = int.Parse(locstr);
    
  2. TryParse

    int.TryParse(string s, out int i)
    

    该方式也是将数字内容的字符串转换为int类型,但是该方式比int.Parse(string s) 好一些,它不会出现异常,最后一个参数result是输出值,如果转换成功则输出相应的值,转换失败则输出0。

    class test
    {
        static void Main(string[] args)
        {
            string s1="abcd";
            string s2="1234";
            int a,b;
            bool bo1 = int.TryParse(s1, out a);
            Console.WriteLine(s1 + " " + bo1 + " " + a);
            bool bo2 = int.TryParse(s2,out b);
            Console.WriteLine(s2+" "+bo2+" "+b);
        }
    }
    

    结果输出:

    abcd False 0
    1234 True 1234
    

变量

接受来自用户的值

System 命名空间中的 Console 类提供了一个函数 ReadLine(),用于接收来自用户的输入,并把它存储到一个变量中。

例如:

int num;
num = Convert.ToInt32(Console.ReadLine());

函数 Convert.ToInt32() 把用户输入的数据转换为 int 数据类型,因为 Console.ReadLine() 只接受字符串格式的数据。

不同类型变量进行运算的问题:

double a = 42.29;
int b = 4229;
int c = a + b;
Console.WriteLine("c = {0}",c);
Console.ReadKey();

上面这种编程方法是错误的,会出现错误提示:

"无法将类型'double'隐式转换为'int'。"

举例说明,当一个精度高的数据类型与一个精度低的数据类型进行运算时,定义运算结果的变量类型必须与精度最高的变量类型相同。这是为了防止在运算过程中造成数据丢失。

下面是正确代码:

double a = 42.29;
int b = 4229;
double c = a + b;
Console.WriteLine("c = {0}",c);
Console.ReadKey();

能输出运算结果:

c = 4271.29

静态变量

  1. 在 C# 中没有全局变量的概念,所有变量必须由该类的实例进行操作,这样做提升了安全性,但是在某些情况下却显得力不从心。因此,我们在保存一些类的公共信息时,就会使用静态变量。
static <data_type> <variable_name> = value;

在变量之前加上 static 关键字,即可声明为静态变量。

  1. 方法的局部变量必须在代码中显式初始化,之后才能在语句中使用它们的值。此时,初始化不是在声明该变量时进行的,但编译器会通过方法检查所有可能的路径,如果检测到局部变量在初始化之前就使用了它的值,就会产生错误。

例如:

public static int Main(){ //返回值类型int类型 正确的做法是初始化它 int d = 0 或者其他值初始化。
    int d;
    Console.WriteLine(d);
}

static void Main(String[],args){ //void 无返回值,参数类型字符串数组
    
}

在这段代码中,演示了如何定义 Main(),使之返回一个 int 类型的数据,而不是 void。但在编译这些代码时,会得到下面的错误消息:

 Use of unassigned local variable 'd' 

常量

定义常量

常量是使用 const 关键字来定义的 。定义一个常量的语法如下:

const <data_type> <constant_name> = value;
const int c1 = 5;

静态常量(编译时常量)const

在编译时就确定了值,必须在声明时就进行初始化且之后不能进行更改,可在类和方法中定义。定义方法如下:

const double a=3.14;// 正确声明常量的方法
const int b;         // 错误,没有初始化

动态常量(运行时常量)readonly

在运行时确定值,只能在声明时或构造函数中初始化,只能在类中定义。定义方法如下:

class Program
{
    readonly int a=1;  // 声明时初始化
    readonly int b;    // 构造函数中初始化
    Program()
    {
        b=2;
    }
    static void Main()
    {
    }
}

Var声明

传统定义变量是已经知道变量的类型,如: int a = 1; string b = “hello”;

用Var类型预先不用知道变量的类型;根据你给变量赋值来判定变量属于什么类型;如

var a =1; 则a是整型,var a = “hello”;则a是字符型,但使用Var类型要注意:

  • 1:必须在定义时初始化,即不能先定义后初始化,如:var a;a = 1;这样是不允许的

  • 2:一旦初始化完成,不能再给变量赋与初始化不同的变量

  • 3:var类型的变量必须是局部变量

封装

封装 被定义为"把一个或多个项目封闭在一个物理的或者逻辑的包中"。在面向对象程序设计方法论中,封装是为了防止对实现细节的访问。

抽象和封装是面向对象程序设计的相关特性。抽象允许相关信息可视化,封装则使开发者实现所需级别的抽象

C# 封装根据具体的需要,设置使用者的访问权限,并通过 访问修饰符 来实现。

一个 访问修饰符 定义了一个类成员的范围和可见性。C# 支持的访问修饰符如下所示:

  • public:所有对象都可以访问;
  • private:对象本身在对象内部可以访问;
  • protected:只有该类对象及其子类对象可以访问
  • internal:同一个程序集的对象可以访问;
  • protected internal:访问限于当前程序集或派生自包含类的类型。

方法

一个方法是把一些相关的语句组织在一起,用来执行一个任务的语句块。每一个 C# 程序至少有一个带有 Main 方法的类。要使用一个方法,您需要:

  • 定义方法
  • 调用方法

实例:

class NumberManipulator
{
   public int FindMax(int num1, int num2)
   {
      /* 局部变量声明 */
      int result;

      if (num1 > num2)
         result = num1;
      else
         result = num2;

      return result;
   } 
   static void Main(string[] args)
   {
      /* 局部变量定义 */
      int a = 100;
      int b = 200;
      int ret;
      NumberManipulator n = new NumberManipulator();

      //调用 FindMax 方法
      ret = n.FindMax(a, b);
      Console.WriteLine("最大值是: {0}", ret );
      Console.ReadLine();
   }
}

您也可以使用类的实例从另一个类中调用其他类的公有方法。例如,方法 FindMax 属于 NumberManipulator 类,您可以从另一个类 Test 中调用它。

using System;

namespace CalculatorApplication
{
    class NumberManipulator
    {
        public int FindMax(int num1, int num2)
        {
            /* 局部变量声明 */
            int result;
            if (num1 > num2)
                result = num1;
            else
                result = num2;
            return result;
        }
    }
    class Test
    {
        static void Main(string[] args)
        {
            /* 局部变量定义 */
            int a = 100;
            int b = 200;
            int ret;
            NumberManipulator n = new NumberManipulator();
            //调用 FindMax 方法
            ret = n.FindMax(a, b);
            Console.WriteLine("最大值是: {0}", ret );
            Console.ReadLine();
        }
    }
}

参数传递

  1. 值传递: 这种方式复制参数的实际值给函数的形式参数,实参和形参使用的是两个不同内存中的值。在这种情况下,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全

  2. 引用参数:这种方式复制参数的内存位置的引用给形式参数。这意味着,当形参的值发生改变时,同时也改变实参的值

  3. 输出参数:这种方式可以返回多个值

值传递类型实例:

class PassingValByVal
{
    private static void Change(int x)
    {
        x = 10;
        System.Console.WriteLine("The value inside the method: {0}", x);
    }
    public static void Execute()
    {
        int n = 5;
        System.Console.WriteLine("The value before calling the method: {0}", n);
        Change(n);
        System.Console.WriteLine("The value after calling the method: {0}", n);
        System.Console.ReadLine();
    }
}
// 输出:
// 5
// 10
// 5

断点执行步骤:

  1. 原变量 n 是 int 值类型,系统为原变量 n 在堆栈Stack上分配的内存地址为:0x088aed2c,该内存地址存储的数据值为5
  2. 当调用 Change 方法时,由于是值传递,系统会为局部参数变量 x 在堆栈Stack上分配一个新的内存区域, 内存地址为:0x088aecd0,并将 n 中的数据值 5 复制到局部参数变量 x 中。这里可以看到局部参数变量 x 和原变量 n 的内存地址是不同的
  3. 对局部参数变量 x 的赋值操作是对变量内存地址中的数据值进行修改,此处是将局部参数变量 x 的内存地址 0x088aecd0 中的数据值由原来的 5 改为 10
  4. 因为原变量 n 和局部参数变量 x 并不是同一块内存区域(内存地址不同),所以 Change 方法中对局部参数变量 x 的赋值操作不会影响到原变量 n。n 的值在调用 Change 方法前后是相同的。实际上,方法内发生的更改只影响局部参数变量 x

传递引用参数类型实例:

class PassingRefByVal
{
    private static void Change(ref int x)
    {
        x = 10;
        System.Console.WriteLine("The value inside the method: {0}", x);
    }
    public static void Execute()
    {
        int n = 5;
        System.Console.WriteLine("The value before calling the method: {0}", n);
        Change(ref n);
        System.Console.WriteLine("The value after calling the method: {0}", n);
        System.Console.ReadLine();
    }
    
    // 输出:
    // 5 
    // 10 
    // 10 

断点执行步骤:

  1. 原变量 n 是 int 值类型,系统为原变量 n 在堆栈Stack上分配的内存地址为:0x0877eb4c,该内存地址存储的数据值为5
  2. 当调用 Change 方法时,由于是引用传递,局部参数变量 x 的内存地址为原变量 n 的内存地址 0x0877eb4c,故局部参数变量 x 的值即是原变量 n 的值。这里可以看到局部参数变量 x 和原变量 n 的内存地址是相同的
  3. 对局部参数变量 x 的赋值操作是对变量内存地址中的数据值进行修改,此处是将局部参数变量 x 的内存地址 0x0877eb4c 中的数据值由原来的 5 改为 10。这里监视窗口中 *&n 的值没有改变是因为在 Change 方法代码块中访问不到原变量 n ,数值没有刷新
  4. 因为原变量 n 和局部参数变量 x 是同一块内存区域(内存地址相同),所以调用 Change 方法后,可以看到变量 n 的内存地址 0x0877eb4c 中的数据值为 10

输出参数类型实例

用法:

  1. 形参与实参之前,都要加out关键字
  2. 输出参数主要是用于函数需要有多个返回值时,作为返回值使用

与引用变量相同的是:

  1. 输出参数也不复制实参在栈中的副本,而是将实参在栈中的地址传给形参。在这点上,输出参数与引用参数相同。
  2. 实参必须是可以赋值的变量,而不能是常量

与引用参数不同的是:

  1. 实参在使用之前不必赋值
  2. 事实上,在使用之前对实参的赋值没有任何意义,因为在调用方法的最开始,便会将其值抹去,使其成为未赋值的变量
  3. 在方法返回之前,必须对out参数进行赋值 

由以上特点所决定的是,输出参数无法向方法中传递信息,其唯一作用便是,当一个参数需要返回多个值时,作为返回值返回。

实例:

 1 class Program
 2     {
 3         static void Main(String[] args)
 4         {
 5             int a;
 6             Add(1, 2, out a);
 7             Console.WriteLine(a);
 8         }
 9         public static bool Add(int x, int y, out int result)
10         {
11             result = x + y;
12             return true;
13         }
14     }
15 //输出:
   // 3

可空类型

可空申明?

单问号用于对 int,double,bool 等无法直接赋值为 null 的数据类型进行 null 的赋值,意思是这个数据类型是 NullAble 类型的。

//实例一:
int? i = 3
int i; //默认值0
int? ii; //默认值null

//实例二:
using System;
namespace CalculatorApplication
{
   class NullablesAtShow
   {
      static void Main(string[] args)
      {
         int? num1 = null;//或使用为new int?()
         int? num2 = 45;
         double? num3 = new double?();
         double? num4 = 3.14157;
         bool? boolval = new bool?();
         // 显示值
         Console.WriteLine("显示可空类型的值: {0}, {1}, {2}, {3}",
                            num1, num2, num3, num4);
         Console.WriteLine("一个可空的布尔值: {0}", boolval);
         Console.ReadLine();
      }
   }
}

//输出:
显示可空类型的值: , 45,  , 3.14157
一个可空的布尔值:

合并运算符??

如果第一个操作数的值为 null,则运算符返回第二个操作数的值,否则返回第一个操作数的值

double? a = null;
double b;
b = a?? 888;

数组

一维数组

初始化

double[] balance = new double[10];

赋值:

double[] balance = new double[10];
balance[0] = 4500.0;
//初始化同时赋值
double[] balance = { 2340.0, 4523.69, 3421.0};
int [] marks = new int[5]  { 99,  98, 92, 97, 95};
//省略大小
int [] marks = new int[]  { 99,  98, 92, 97, 95};
//您也可以赋值一个数组变量到另一个目标数组变量中
int [] marks = new int[]  { 99,  98, 92, 97, 95};
int[] score = marks;

访问数组元素

double salary = balance[9];

多维数组

二维数组类似矩阵

初始化二维数组

int [,] a = new int [3,4] {
 {0, 1, 2, 3} ,   /*  初始化索引号为 0 的行 */
 {4, 5, 6, 7} ,   /*  初始化索引号为 1 的行 */
 {8, 9, 10, 11}   /*  初始化索引号为 2 的行 */
};

循环打印出二维数组:

using System;
namespace ArrayApplication
{
    class MyArray
    {
        static void Main(string[] args)
        {
            /* 一个带有 5 行 2 列的数组 */
            int[,] a = new int[5, 2] {{0,0}, {1,2}, {2,4}, {3,6}, {4,8} };

            int i, j;

            /* 输出数组中每个元素的值 */
            for (i = 0; i < 5; i++)
            {
                for (j = 0; j < 2; j++)
                {
                    Console.WriteLine("a[{0},{1}] = {2}", i, j, a[i,j]);
                }
            }
           Console.ReadKey();
        }
    }
}

交错数组

维数组的元素的类型是一个数组。

声明

int[][] arr = new int[3][];

赋值

方式一:声明时赋值:

1  int[][] arr = new int[3][]{
2                 new int[] { 1, 2, 3, 4, 5 },
3                 new int[] { 6, 7, 8 },
4                 new int[] { 9, 10 }
5             };

方式二:声明后赋值:

int[][] arr = new int[3][];
arr[0] = new int[] { 1, 2, 3, 4, 5 };
arr[1] = new int[] { 6, 7, 8 };
arr[2] = new int[] { 9, 10 };

为交错数组的元素(数组)的元素赋值

int[][] arr = new int[3][] { new int[3], new int[5], new int[7] };
arr[0][0] = 1;//为第0个数组的第0个元素赋值
arr[1][2] = 15;//为第1个数组的第2个元素赋值
arr[2][3] = 23;//为第2个数组的第3个元素赋值

int[] arrResult = arr[0];

注:取值变量也该声明成数组类型

List

基础、常用方法:

(1)、声明:
①、List mList = new List();
T为列表中元素类型,现在以string类型作为例子

List<string> mList = new List<string>();

②、List testList =new List (IEnumerable collection);

以一个集合作为参数创建List:

string[] temArr = { "Ha", "Hunter", "Tom", "Lily", "Jay", "Jim", "Kuku", "Locu" };
List<string> testList = new List<string>(temArr);

(2)、添加元素:

①、 添加一个元素

  语法: List. Add(T item)

List<string> mList = new List<string>();
mList.Add("John");

②、 添加一组元素

  语法: List. AddRange(IEnumerable collection)

List<string> mList = new List<string>();
string[] temArr = { "Ha","Hunter", "Tom", "Lily", "Jay", "Jim", "Kuku",  "Locu" };
mList.AddRange(temArr);

③、在index位置添加一个元素

  语法: Insert(int index, T item);

List<string> mList = new List<string>();
mList.Insert(1, "Hei");

④、遍历List中元素

语法:

foreach (T element in mList)  //T的类型与mList声明时一样
{
    Console.WriteLine(element);
}

例:

List<string> mList = new List<string>();
...//省略部分代码
foreach (string s in mList)
{
    Console.WriteLine(s);
}

(3)、删除元素:

①、删除一个值

  语法:List. Remove(T item)

mList.Remove("Hunter");

②、 删除下标为index的元素

  语法:List. RemoveAt(int index);

mList.RemoveAt(0);

③、 从下标index开始,删除count个元素

  语法:List. RemoveRange(int index, int count);

mList.RemoveRange(3, 2);

(4)、判断某个元素是否在该List中:

语法:List. Contains(T item) 返回值为:true/false

if (mList.Contains("Hunter"))
{
    Console.WriteLine("There is Hunter in the list");
}
else
{
    mList.Add("Hunter");
    Console.WriteLine("Add Hunter successfully.");
}

(5)、给List里面元素排序:

  语法: List. Sort () 默认是元素第一个字母按升序

mList.Sort();

自定义规则排序

/// <summary>
///①通用自定义
/// </summary>
list.Sort((left, right) =>
{
    if (left.n > right.n)//其中 n 是某个你希望以此进行排序的属性
        return 1;
    else if (left.n == right.n)
        return 0;
    else
        return -1;
});

/// <summary>
/// ②针对属性是字符串的排序
/// </summary>
list.Sort((left, right) =>
{
    return left.Date.CompareTo(right.Date);//其中Date是时间字符串             
});

(6)、给List里面元素顺序反转:

  语法: List. Reverse () 可以与List. Sort ()配合使用,达到想要的效果

mList. Reverse();

(7)、List清空:

  语法:List. Clear ()

mList.Clear();

(8)、获得List中元素数目:

  语法: List. Count () 返回int值

int count = mList.Count();
Console.WriteLine("The num of elements in the list: " +count);

2、List的进阶、强大方法:

本段举例用的List:

string[] temArr = { "Ha","Hunter", "Tom", "Lily", "Jay", "Jim", "Kuku", " "Locu" };
mList.AddRange(temArr);

(1)、List.FindAll方法:检索与指定谓词所定义的条件相匹配的所有元素

  语法:public List FindAll(Predicate match);

List<string> subList = mList.FindAll(ListFind); //委托给ListFind函数
foreach (string s in subList)
{
    Console.WriteLine("element in subList: "+s);
}

这时subList存储的就是所有长度大于3的元素。

(2)、List.Find 方法:搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中的第一个匹配元素。

  语法:public T Find(Predicate match);

Predicate是对方法的委托,如果传递给它的对象与委托中定义的条件匹配,则该方法返回 true。当前 List 的元素被逐个传递给Predicate委托,并在 List 中向前移动,从第一个元素开始,到最后一个元素结束。当找到匹配项时处理即停止。

Predicate 可以委托给一个函数或者一个拉姆达表达式:

委托给拉姆达表达式:

string listFind = mList.Find(name =>  //name是变量,代表的是mList中元素,自己设定
{     
   if (name.Length > 3)
   {
      return true;
   }
  return false;
});
Console.WriteLine(listFind);     //输出是Hunter

委托给一个函数:

string listFind1 = mList.Find(ListFind);  //委托给ListFind函数
Console.WriteLine(listFind);    //输出是Hunter

//ListFind函数
public bool ListFind(string name)
{
    if (name.Length > 3)
    {
        return true;
    }
    return false;
}

这两种方法的结果是一样的。

(3)、List.FindLast 方法:搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中的最后一个匹配元素。
  语法:public T FindLast(Predicate match);

用法与List.Find相同。

(4)、List.TrueForAll方法: 确定是否 List 中的每个元素都与指定的谓词所定义的条件相匹配。

  语法:public bool TrueForAll(Predicate match);

委托给拉姆达表达式:

bool flag = mList.TrueForAll(name =>
{
    if (name.Length > 3)
    {
     return true;
    }
    else
    {
     return false;
    }
});
Console.WriteLine("True for all:  "+flag);  //flag值为

委托给一个函数,这里用到上面的ListFind函数:

bool flag = mList.TrueForAll(ListFind);    //委托给ListFind函数
Console.WriteLine("True for all:  "+flag);  //flag值为false

这两种方法的结果是一样的。

(5)List.Take(n)方法: 获得前n行 返回值为IEnumetable,T的类型与List的类型一样

IEnumerable<string> takeList=  mList.Take(5);
foreach (string s in takeList)
{
      Console.WriteLine("element in takeList: " + s);
}

这时takeList存放的元素就是mList中的前5个。

(6)、List.Where方法:检索与指定谓词所定义的条件相匹配的所有元素。跟List.FindAll方法类似。

IEnumerable<string> whereList = mList.Where(name =>
{
   if (name.Length > 3)
   {
      return true;
   }
   else
  {
     return false;
  }
});

foreach (string s in subList)
{
   Console.WriteLine("element in subLis");
}

这时subList存储的就是所有长度大于3的元素。

(7)、List.RemoveAll方法:移除与指定的谓词所定义的条件相匹配的所有元素。

  语法: public int RemoveAll(Predicate match);

mList.RemoveAll(name =>
{
     if (name.Length > 3)
    {
        return true;
    }
    else
    {
        return false;
    }
});

foreach (string s in mList)
{
    Console.WriteLine("element in mList:     " + s);
}  

结构体

在 C# 中,结构体是值类型数据结构。它使得一个单一变量可以存储各种数据类型的相关数据。struct 关键字用于创建结构体。结构体是用来代表一个记录。

定义:

struct Books
{
   public string title;
   public string author;
   public string subject;
   public int book_id;
};  

用法

结构可以不使用 New 操作符即可被实例化。

 //声明时不用初始化
  Books Book1;        /* 声明 Book1,类型为 Book */ 
  Books Book2;        /* 声明 Book2,类型为 Book */

  /* book 1 详述 */
  Book1.title = "C Programming";
  Book1.author = "Nuha Ali";
  Book1.subject = "C Programming Tutorial";
  Book1.book_id = 6495407;

  /* book 2 详述 */
  Book2.title = "Telecom Billing";
  Book2.author = "Zara Ali";
  Book2.subject =  "Telecom Billing Tutorial";
  Book2.book_id = 6495700;

特点

  • 结构可带有方法、字段、索引、属性、运算符方法和事件。
  • 结构可定义构造函数,但不能定义析构函数。但是,您不能为结构定义无参构造函数。无参构造函数(默认)是自动定义的,且不能被改变。
  • 与类不同,结构不能继承其他的结构或类。
  • 结构不能作为其他结构或类的基础结构。
  • 结构可实现一个或多个接口。
  • 结构成员不能指定为 abstract、virtual 或 protected。
  • 当您使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 New 操作符即可被实例化。
  • 如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。

类 vs 结构

类和结构有以下几个基本的不同点:

  • 类是引用类型,结构是值类型。
  • 结构不支持继承。
  • 结构不能声明默认的构造函数。

针对上述讨论,让我们重写前面的实例:

using System;
using System.Text;   
struct Books
{
   private string title;
   private string author;
   private string subject;
   private int book_id;
   public void getValues(string t, string a, string s, int id)
   {
      title = t;
      author = a;
      subject = s;
      book_id =id;
   }
   public void display()
   {
      Console.WriteLine("Title : {0}", title);
      Console.WriteLine("Author : {0}", author);
      Console.WriteLine("Subject : {0}", subject);
      Console.WriteLine("Book_id :{0}", book_id);
   }
};  

public class testStructure
{
   public static void Main(string[] args)
   {
      Books Book1 = new Books(); /* 声明 Book1,类型为 Book */
      Books Book2 = new Books(); /* 声明 Book2,类型为 Book */
      /* book 1 详述 */
      Book1.getValues("C Programming",
      "Nuha Ali", "C Programming Tutorial",6495407);
      /* book 2 详述 */
      Book2.getValues("Telecom Billing",
      "Zara Ali", "Telecom Billing Tutorial", 6495700);
      /* 打印 Book1 信息 */
      Book1.display();
      /* 打印 Book2 信息 */
      Book2.display();
      Console.ReadKey();
   }
}

补充:类与结构体的区别

1、结构体中声明的字段无法赋予初值,类可以:

struct test001
{
    private int aa = 1;
}

执行以上代码将出现“结构中不能实例属性或字段初始值设定”的报错,而类中无此限制,代码如下:

class test002
{
    private int aa = 1;
}

2、结构体的构造函数中,必须为结构体所有字段赋值,类的构造函数无此限制:

补充:类与结构的选择:

首先明确,类的对象是存储在堆空间中,结构存储在栈中。堆空间大,但访问速度较慢,栈空间小,访问速度相对更快。故而,当我们描述一个轻量级对象的时候,结构可提高效率,成本更低。当然,这也得从需求出发,假如我们在传值的时候希望传递的是对象的引用地址而不是对象的拷贝,就应该使用类了。

枚举enum

枚举列表中的每个符号代表一个整数值,一个比它前面的符号大的整数值。默认情况下,第一个枚举符号的值是 0.例如:

using System;

public class EnumTest
{
    enum Day { Sun, Mon, Tue, Wed, Thu, Fri, Sat };

    static void Main()
    {
        int x = (int)Day.Sun;
        int y = (int)Day.Fri;
        Console.WriteLine("Sun = {0}", x);
        Console.WriteLine("Fri = {0}", y);
    }
}

//输出
Sun = 0
Fri = 5

自定义每个符号的值:

using System;
namespace EnumApplication
{
    class EnumProgram
    {
        enum Days {
            Mon=71, 
            tue=61, 
            Wed=51, 
            thu=41, 
            Fri=51, 
            Sat=61, 
            Sun=71
        };

        static void Main(string[] args)
        {
            int WeekdayStart = (int)Days.Mon;
            int WeekdayEnd = (int)Days.Fri;
            Console.WriteLine("Monday: {0}", WeekdayStart);
            Console.WriteLine("Friday: {0}", WeekdayEnd);
            Console.ReadKey();
        }
    }
}

//输出
Monday: 71
Friday: 51

注意:

  • 如果没有指定,则使用默认的访问标识符。类的默认访问标识符是 internal,成员的默认访问标识符是 private
  • 数据类型 指定了变量的类型,返回类型 指定了返回的方法返回的数据类型。
  • 如果要访问类的成员,你要使用点(.)运算符。
  • 点运算符链接了对象的名称和成员的名称。

类中的构造函数

类的 构造函数 是类的一个特殊的成员函数,当创建类的新对象时执行。构造函数的名称与类的名称完全相同,它没有任何返回类型。

using System;
namespace LineApplication
{
   class Line
   {
      private double length;   // 线条的长度
      public Line()
      {
         Console.WriteLine("对象已创建");
      }

      public void setLength( double len )
      {
         length = len;
      }
      public double getLength()
      {
         return length;
      }

      static void Main(string[] args)
      {
         Line line = new Line();    
         // 设置线条长度
         line.setLength(6.0);
         Console.WriteLine("线条的长度: {0}", line.getLength());
         Console.ReadKey();
      }
   }
}
//输出
对象已创建	//类创建实例时构造函数就被调用
线条的长度: 6

默认的构造函数没有任何参数。但是如果你需要一个带有参数的构造函数可以有参数,这种构造函数叫做参数化构造函数。这种技术可以帮助你在创建对象的同时给对象赋初始值,具体请看下面实例:

​ Line line = new Line(10.0)

using System;
namespace LineApplication
{
   class Line
   {
      private double length;   // 线条的长度
      public Line(double len)  // 参数化构造函数
      {
         Console.WriteLine("对象已创建,length = {0}", len);
         length = len;
      }

      public void setLength( double len )
      {
         length = len;
      }
      public double getLength()
      {
         return length;
      }

      static void Main(string[] args)
      {
         Line line = new Line(10.0);
         Console.WriteLine("线条的长度: {0}", line.getLength());
         // 设置线条长度
         line.setLength(6.0);
         Console.WriteLine("线条的长度: {0}", line.getLength());
         Console.ReadKey();
      }
   }
}

类中的析构函数

类的 析构函数 是类的一个特殊的成员函数,当类的对象超出范围时执行。析构函数的名称是在类的名称前加上一个波浪形(~)作为前缀,它不返回值,也不带任何参数。析构函数用于在结束程序(比如关闭文件、释放内存等)之前释放资源。析构函数不能继承或重载。

下面的实例说明了析构函数的概念:

using System;
namespace LineApplication
{
   class Line
   {
      private double length;   // 线条的长度
      public Line()  // 构造函数
      {
         Console.WriteLine("对象已创建");
      }
      ~Line() //析构函数
      {
         Console.WriteLine("对象已删除");
      }

      public void setLength( double len )
      {
         length = len;
      }
      public double getLength()
      {
         return length;
      }

      static void Main(string[] args)
      {
         Line line = new Line();
         // 设置线条长度
         line.setLength(6.0);
         Console.WriteLine("线条的长度: {0}", line.getLength());          
      }
   }
}
//输出
对象已创建
线条的长度: 6
对象已删除

静态成员变量

我们可以使用 static 关键字把类成员定义为静态的。当我们声明一个类成员为静态时,意味着无论有多少个类的对象被创建,只会有一个该静态成员的副本。

关键字 static 意味着类中只有一个该成员的实例。静态变量用于定义常量,因为它们的值可以通过直接调用类而不需要创建类的实例来获取。静态变量可在成员函数或类的定义外部进行初始化。你也可以在类的定义内部初始化静态变量。

using System;
namespace StaticVarApplication
{
    class StaticVar
    {
       public static int num;
        public void count()
        {
            num++;
        }
        public int getNum()
        {
            return num;
        }
    }
    class StaticTester
    {
        static void Main(string[] args)
        {
            StaticVar s1 = new StaticVar();
            StaticVar s2 = new StaticVar();
            s1.count();
            s1.count();
            s1.count();
            s2.count();
            s2.count();
            s2.count();        
            Console.WriteLine("s1 的变量 num: {0}", s1.getNum());
            Console.WriteLine("s2 的变量 num: {0}", s2.getNum());
            Console.ReadKey();
        }
    }
}

//输出
s1 的变量 num: 6
s2 的变量 num: 6

静态成员函数

你也可以把一个成员函数声明为 static。这样的函数只能访问静态变量。静态函数在对象被创建之前就已经存在

using System;
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            int num = AddClass.Add(2, 3);  //编译通过
            Console.WriteLine(num);
        }
    }

    class AddClass
    {
        public static int Add(int x,int y)
        {
            return x + y;
        }
    }
}

反之,如果不声明为static,即使和Main方法从属于同一个类,也必须经过实例化

using System;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            int num = Add(2, 3);  //编译错误,即使改为Program.Add(2, 3);也无法通过编译
            Console.WriteLine(num);
        }

        public int Add(int x, int y)
        {
            return x + y;
        }
    }
}
using System;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Program self = new Program();
            int num = self.Add(2, 3);  //编译通过
            Console.WriteLine(num);
        }

        public int Add(int x, int y)
        {
            return x + y;
        }
    }
}

注意:

倘若在类的声明中没有显式地提供实例构造函数,在这种情况下编译器会提供一个隐式的默认构造函数,它具有以下特点:

①不带参数;

②方法体为空。

但是如果你声明了任何构造函数,那么编译器就不会把该类定义为默认构造函数。

例如:

class Class2
{
    public Class2(int Value)  {...}  //构造函数0
    public Class2(String Value) {...}  //构造函数1
}

class Program
{
    static void Main()
    {
        Class2 a = new Class2();   //错误!没有无参数的构造函数
        ...
    }
}

在以上的代码中至少有一个显式定义的构造函数,编译器不会创建任何额外的构造函数,在 Main() 中如果试图用不带参数的构造函数创建新的实例,因为没有无参数的构造函数,所以编译器就会产生一条错误信息。

访问修饰符

abstract

abstract 修饰符指示被修改内容的实现已丢失或不完整。 abstract 修饰符可用于类、方法、属性、索引和事件。 在类声明中使用 abstract 修饰符来指示某个类仅用作其他类的基类,而不用于自行进行实例化。 标记为抽象的成员必须由派生自抽象类的非抽象类来实现。

示例:在此示例中,类 Square 必须提供 GetArea 的实现,因为它派生自 Shape:

abstract class Shape
{
    public abstract int GetArea();
}

class Square : Shape
{
    int side;

    public Square(int n) => side = n;

    // GetArea method is required to avoid a compile-time error.
    public override int GetArea() => side * side;

    static void Main()
    {
        var sq = new Square(12);
        Console.WriteLine($"Area of the square = {sq.GetArea()}");
    }
}

抽象类具有以下功能:

  • 抽象类不能实例化。
  • 抽象类可能包含抽象方法和访问器。
  • 无法使用 sealed 修饰符来修改抽象类,因为两个修饰符的含义相反。 sealed 修饰符阻止类被继承,而 abstract 修饰符要求类被继承。
  • 派生自抽象类的非抽象类,必须包含全部已继承的抽象方法和访问器的实际实现。

在方法或属性声明中使用 abstract 修饰符,以指示该方法或属性不包含实现。

抽象方法具有以下功能:

  • 抽象方法是隐式的虚拟方法。
  • 只有抽象类中才允许抽象方法声明。
  • 由于抽象方法声明不提供实际的实现,因此没有方法主体;方法声明仅以分号结尾,且签名后没有大括号 ({ })。 例如:
public abstract void MyMethod();  
  • 实现由方法 override 提供,它是非抽象类的成员。
  • 在抽象方法声明中使用 staticvirtual 修饰符是错误的。

除了声明和调用语法方面不同外,抽象属性的行为与抽象方法相似。

  • 在静态属性上使用 abstract 修饰符是错误的。
  • 通过包含使用 override 修饰符的属性声明,可在派生类中重写抽象继承属性。

抽象类必须为所有接口成员提供实现。

实现接口的抽象类有可能将接口方法映射到抽象方法上。 例如:

interface I
{
    void M();
}

abstract class C : I
{
    public abstract void M();
}

在此示例中,类 DerivedClass 派生自抽象类 BaseClass。 抽象类包含抽象方法 AbstractMethod,以及两个抽象属性 XY

abstract class BaseClass   // Abstract class
{
    protected int _x = 100;
    protected int _y = 150;
    public abstract void AbstractMethod();   // Abstract method
    public abstract int X    { get; }
    public abstract int Y    { get; }
}

class DerivedClass : BaseClass
{
    public override void AbstractMethod()
    {
        _x++;
        _y++;
    }

    public override int X   // overriding property
    {
        get
        {
            return _x + 10;
        }
    }

    public override int Y   // overriding property
    {
        get
        {
            return _y + 10;
        }
    }

    static void Main()
    {
        var o = new DerivedClass();
        o.AbstractMethod();
        Console.WriteLine($"x = {o.X}, y = {o.Y}");
    }
}
// Output: x = 111, y = 161

在前面的示例中,如果你尝试通过使用如下语句来实例化抽象类:

BaseClass bc = new BaseClass();   // Error  

继承

当创建一个类时,程序员不需要完全重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可。这个已有的类被称为的基类,这个新的类被称为派生类

继承的思想实现了 属于(IS-A) 关系。例如,哺乳动物 属于(IS-A) 动物,狗 属于(IS-A) 哺乳动物,因此狗 属于(IS-A) 动物。

假设,有一个基类 Shape,它的派生类是 Rectangle:

using System;
namespace InheritanceApplication
{
   class Shape
   {
      public void setWidth(int w)
      {
         width = w;
      }
      public void setHeight(int h)
      {
         height = h;
      }
      protected int width;
      protected int height;
   }

   // 派生类
   class Rectangle: Shape
   {
      public int getArea()
      {
         return (width * height);
      }
   }
   
   class RectangleTester
   {
      static void Main(string[] args)
      {
         Rectangle Rect = new Rectangle();

         Rect.setWidth(5);
         Rect.setHeight(7);

         // 打印对象的面积
         Console.WriteLine("总面积: {0}",  Rect.getArea());
         Console.ReadKey();
      }
   }
}

//输出:总面积:35

基类的初始化

派生类继承了基类的成员变量和成员方法。因此父类对象应在子类对象创建之前被创建。您可以在成员初始化列表中进行父类的初始化。

下面的程序演示了这点:

using System;
namespace RectangleApplication
{
   class Rectangle
   {
      // 成员变量
      protected double length;
      protected double width;
      public Rectangle(double l, double w)
      {
         length = l;
         width = w;
      }
      public double GetArea()
      {
         return length * width;
      }
      public void Display()
      {
         Console.WriteLine("长度: {0}", length);
         Console.WriteLine("宽度: {0}", width);
         Console.WriteLine("面积: {0}", GetArea());
      }
   }//end class Rectangle  
   class Tabletop : Rectangle
   {
      private double cost;
      public Tabletop(double l, double w) : base(l, w) //可以在派生类构造函数中添加初始化基类的代码。
      { }
      public double GetCost()
      {
         double cost;
         cost = GetArea() * 70;
         return cost;
      }
      public void Display()
      {
         base.Display();
         Console.WriteLine("成本: {0}", GetCost());
      }
   }
   class ExecuteRectangle
   {
      static void Main(string[] args)
      {
         Tabletop t = new Tabletop(4.5, 7.5);
         t.Display();
         Console.ReadLine();
      }
   }
}

多重继承

C# 不支持多重继承。但是,您可以使用接口来实现多重继承。下面的程序演示了这点:

using System;
namespace InheritanceApplication
{
   class Shape
   {
      public void setWidth(int w)
      {
         width = w;
      }
      public void setHeight(int h)
      {
         height = h;
      }
      protected int width;
      protected int height;
   }

   // 基类 PaintCost
   public interface PaintCost
   {
      int getCost(int area);

   }
   // 派生类
   class Rectangle : Shape, PaintCost
   {
      public int getArea()
      {
         return (width * height);
      }
      public int getCost(int area)
      {
         return area * 70;
      }
   }
   class RectangleTester
   {
      static void Main(string[] args)
      {
         Rectangle Rect = new Rectangle();
         int area;
         Rect.setWidth(5);
         Rect.setHeight(7);
         area = Rect.getArea();
         // 打印对象的面积
         Console.WriteLine("总面积: {0}",  Rect.getArea());
         Console.WriteLine("油漆总成本: ${0}" , Rect.getCost(area));
         Console.ReadKey();
      }
   }
}

父类声明,子类实例化

这个实例是子类的,但是因为你声明时是用父类声明的,所以你用正常的办法访问不到子类自己的成员,只能访问到从父类继承来的成员。

在子类中用 override 重写父类中用 virtual 申明的虚方法时,实例化父类调用该方法,执行时调用的是子类中重写的方法;

如果子类中用 new 覆盖父类中用 virtual 申明的虚方法时,实例化父类调用该方法,执行时调用的是父类中的虚方法;

/// <summary>  
/// 父类  
/// </summary>  
public class ParentClass  
{  
   public virtual void ParVirMethod()  
   {  
       Console.WriteLine("父类的方法...");  
   }  
}  

/// <summary>  
/// 子类1  
/// </summary>  
public class ChildClass1 : ParentClass  
{  
   public override void ParVirMethod()  
   {  
       Console.WriteLine("子类1的方法...");  
   }  
}  

/// <summary>  
/// 子类2  
/// </summary>  
public class ChildClass2 : ParentClass  
{  
   public new void ParVirMethod()  
   {  
       Console.WriteLine("子类2的方法...");  
   }  

   public void Test()  
   {  
       Console.WriteLine("子类2的其他方法...");  
   }  
} 

执行调用:

ParentClass par = new ChildClass1();  
par.ParVirMethod(); //结果:"子类1的方法",调用子类的方法,实现了多态

par = new ChildClass2();  
par.ParVirMethod(); //结果:"父类的方法",调用父类的方法,没有实现多态  

深究其原因,为何两者不同,是因为原理不同: override是重写,即将基类的方法在派生类里直接抹去重新写,故而调用的方法就是子类方法;而new只是将基类的方法在派生类里隐藏起来,故而调用的仍旧是基类方法。

依赖倒置原则

依赖倒置原则,DIP,Dependency Inverse Principle DIP的表述是:

1、高层模块不应该依赖于低层模块, 二者都应该依赖于抽象。

2、抽象不应该依赖于细节,细节应该依赖于抽象。

这里说的“依赖”是使用的意思,如果你调用了一个类的一个方法,就是依赖这个类,如果你直接调用这个类的方法,就是依赖细节,细节就是具体的类,但如果你调用的是它父类或者接口的方法,就是依赖抽象, 所以 DIP 说白了就是不要直接使用具体的子类,而是用它的父类的引用去调用子类的方法,这样就是依赖于抽象,不依赖具体。

其实简单的说,DIP 的好处就是解除耦合,用了 DIP 之后,调用者就不知道被调用的代码是什么,因为调用者拿到的是父类的引用,它不知道具体指向哪个子类的实例,更不知道要调用的方法具体是什么,所以,被调用代码被偷偷换成另一个子类之后,调用者不需要做任何修改, 这就是解耦了。

重写(override)和覆盖(new)

用关键字 virtual 修饰的方法,叫虚方法。可以在子类中用override 声明同名的方法,这叫“重写”。相应的没有用virtual修饰的方法,我们叫它实方法。
重写会改变父类方法的功能。
看下面演示代码:

public class C1
{
    public virtual string GetName()
    {
        return "叔祥";
    }
}

public class C2 : C1
{
    public override string GetName()
    {
        return "xiangshu";
    }
}

 C1 c1 = new C1();
 Console.WriteLine(c1.GetName());//输出“祥叔”

 C2 c2 = new C2();
 Console.WriteLine(c2.GetName());//输出“xiangshu”

 //重点看这里

 C1 c3 = new C2();
 Console.WriteLine(c3.GetName());//输出“xiangshu” 

覆盖(new)
在子类中用 new 关键字修饰 定义的与父类中同名的方法,叫覆盖。

覆盖不会改变父类方法的功能。

看下面演示代码:

public class C1
{
    public string GetName()
    {
        return "祥叔";
    }
}

public class C2 : C1
{
    public new string GetName()
    {
        return "xiangshu";
    }
}

C1 c1 = new C1();
Console.WriteLine(c1.GetName());//输出“祥叔”

C2 c2 = new C2();
Console.WriteLine(c2.GetName());//输出“xiangshu”

//重点看这里,和上面的重写作比较

C1 c3 = new C2();
Console.WriteLine(c3.GetName());//输出“祥叔” 

总结

1:不管是重写还是覆盖都不会影响父类自身的功能(废话,肯定的嘛,除非代码被改)。

2:当用子类创建父类的时候,如 C1 c3 = new C2(),重写会改变父类的功能,即调用子类的功能;而覆盖不会,仍然调用父类功能。

3:虚方法、实方法都可以被覆盖(new),抽象方法,接口 不可以。

4:抽象方法,接口,标记为virtual的方法可以被重写(override),实方法不可以。

5:重写使用的频率比较高,实现多态;覆盖用的频率比较低,用于对以前无法修改的类进行继承的时候。

关于基类访问(访问隐藏的基类成员)

基类访问的实例:

class SomeClass   //基类
{
    public string Field1 = "Fields -- In the base class";
}

class OtherClass : SomeClass  //继承类OtherClass,继承于SomeClass
{
    new public string Field1 = "Fields -- In the derived class";
    public void PrintField1()
    {
        Console.WriteLine(Field1);    //访问派生类
        Console.WriteLine(base.Field1)  //访问基类
    }
}


class Program
{
   static void Main()
   {
        OtherClass oc = new OtherClass();  //实例化
        oc.PrintFields1();                //执行oc的PrintFields1()方法
   }  
}

// 输出:
// Fields -- In the derived class
// Fields -- In the base class

创建子类对象调用子类的构造函数时,会首先调用父类的无参构造函数。

using System;

namespace ConsoleApp1
{
    class Program
    {
        class father
        {
            public father()
            {
                Console.WriteLine("father");
            }
        }
        class son : father
        {
            public son()
            {
                Console.WriteLine("son");
            }
        }
        static void Main(string[] args)
        {
            son s = new son();
            Console.ReadKey();
        }
    }
}

//输出结果:
//father
//son

多态性

静态多态性

在编译时,函数和对象的连接机制被称为早期绑定,也被称为静态绑定。C# 提供了两种技术来实现静态多态性。分别为:

  • 函数重载
  • 运算符重载

运算符重载将在下一章节讨论,接下来我们将讨论函数重载。

函数重载

using System;
namespace PolymorphismApplication
{
    public class TestData  
    {  
        public int Add(int a, int b, int c)  
        {  
            return a + b + c;  
        }  
        public int Add(int a, int b)  
        {  
            return a + b;  
        }  
    }  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            TestData dataClass = new TestData();
            int add1 = dataClass.Add(1, 2);  
            int add2 = dataClass.Add(1, 2, 3);

            Console.WriteLine("add1 :" + add1);
            Console.WriteLine("add2 :" + add2);  
        }  
    }  
}

动态多态性

C# 允许您使用关键字 abstract 创建抽象类,用于提供接口的部分类的实现。当一个派生类继承自该抽象类时,实现即完成。抽象类包含抽象方法,抽象方法可被派生类实现。派生类具有更专业的功能。

请注意,下面是有关抽象类的一些规则:

  • 您不能创建一个抽象类的实例。
  • 您不能在一个抽象类外部声明一个抽象方法。
  • 通过在类定义前面放置关键字 sealed,可以将类声明为密封类。当一个类被声明为 sealed 时,它不能被继承。抽象类不能被声明为 sealed。

下面的程序演示了一个抽象类:

using System;
namespace PolymorphismApplication
{
   abstract class Shape
   {
       abstract public int area();
   }
   class Rectangle:  Shape
   {
      private int length;
      private int width;
      public Rectangle( int a=0, int b=0)
      {
         length = a;
         width = b;
      }
      public override int area ()
      {
         Console.WriteLine("Rectangle 类的面积:");
         return (width * length);
      }
   }

   class RectangleTester
   {
      static void Main(string[] args)
      {
         Rectangle r = new Rectangle(10, 7);
         double a = r.area();
         Console.WriteLine("面积: {0}",a);
         Console.ReadKey();
      }
   }
}

//输出:
//Rectangle 类的面积:
//面积: 70

当有一个定义在类中的函数需要在继承类中实现时,可以使用虚方法

虚方法是使用关键字 virtual 声明的。

虚方法可以在不同的继承类中有不同的实现。

对虚方法的调用是在运行时发生的。

动态多态性是通过 抽象类虚方法 实现的。

以下实例创建了 Shape 基类,并创建派生类 Circle、 Rectangle、Triangle, Shape 类提供一个名为 Draw 的虚拟方法,在每个派生类中重写该方法以绘制该类的指定形状。

using System;
using System.Collections.Generic;

public class Shape
{
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }
   
    // 虚方法
    public virtual void Draw()
    {
        Console.WriteLine("执行基类的画图任务");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个圆形");
        base.Draw();
    }
}
class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个长方形");
        base.Draw();
    }
}
class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个三角形");
        base.Draw();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle
        var shapes = new List<Shape> 
        {
            new Rectangle(),
            new Triangle(),
            new Circle()
        };
        //方式二:
        /*
         List<Shape> shapes = new List<Shape>;
         Rectangle r1 = new Rectangle();
         Triangle t1 = new Triangle();
         Circle c1 = new Circle();
       	 shapes.Add(r1);
       	 shapes.Add(t1);
       	 shapes.Add(c1);
        */

        // 使用 foreach 循环对该列表的派生类进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法
        foreach (var shape in shapes)
        {
            shape.Draw();
        }

        Console.WriteLine("按下任意键退出。");
        Console.ReadKey();
    }

}

注意:

virtual 和 abstract

virtual和abstract都是用来修饰父类的,通过覆盖父类的定义,让子类重新定义。

  • 1.virtual修饰的方法必须有实现(哪怕是仅仅添加一对大括号),而abstract修饰的方法一定不能实现。
  • 2.virtual可以被子类重写,而abstract必须被子类重写。
  • 3.如果类成员被abstract修饰,则该类前必须添加abstract,因为只有抽象类才可以有抽象方法。
  • 4.无法创建abstract类的实例,只能被继承无法实例化。

重载和重写

重载(overload)是提供了一种机制, 相同函数名通过不同的返回值类型以及参数来表来区分的机制。

重写(override)是用于重写基类的虚方法,这样在派生类中提供一个新的方法。

posted @ 2020-07-31 19:53  garxirapper  阅读(625)  评论(0编辑  收藏  举报