C#笔记

一、 C#入门

1.C#概述

  C#是微软公司2000年推出的一种编程语言,它松散地基于C和C++,在很多地方与java类似。C#程序必须在.NET Framework上运行。

2.C#的编译过程

编译流程图

  1. C#源文件经过CSC.exe把Visual C#程序代码编译成IL中间语言和元数据,编译器编译生成程序集(exe/dll)IL和元数据同时储存在exe或dll 文件中,该伪代码是独立于任何CPU 的,所以只要装有.Net FrameWork的机器均可以运行该伪代码,这就增强了c#代码的可移植性。
  2. 程序执行时,该伪代码被CLR(Commom Language RunTime)捕获,CLR激活内部的 JIT(Just in time)编译器,将伪代码编译成针对特定CPU的机器语言,当然该步还要进行类型检查等,这样程序就能在任意合理的电脑中运行,得出结果。JIT 的意思为“仅在运行时编译”,整个代码的处理过程要编译两次。

3.C#的hello world

using System;
namespace HelloWorld
{
	class HelloWorld
	{
		public void Print()
		{
			Console.WriteLine("hello world");
		}
		static void Main(string[] args) //程序启动的入口
		{
			HelloWorld helloworld = new HelloWorld();
			helloworld.Print();
			Console.ReadLine(); //必须有该行,否则控制台无法持续显示
		}
	}
}

4.C#的类型

C#中的每个类都直接或者间接派生于object类。C#语言的类型可以分为以下3类。

  • 值类型,即基本类型,变量直接包含数据
  • 引用类型,变量只存储对数据的引用(即访问的地址),不包含数据本身
  • 指针类型,与C/C++类似,而且只能用在不安全代码中。C#中很少使用该类型。略。

C#类型关系图

decimal类型:表示128位的数值, 通常用来表示财务计算和货币计算。该类型不支持有符号的零,无穷大和NaN。(浮点数在计算时不会产生异常,而decimal类型会,例如除数为零)

拆箱与装箱
装箱:指将值类型隐式转化为引用类型。

int i = 10;
object o = i; //int类型转化为了引用类型,并将其赋值给object类型变量o

拆箱:将引用类型隐式转换为值类型。包含两个步骤:1)检查该对象是否是给定类型的一个装箱的值。2)将值从实例中赋值出来。

int i= 10;
object o = i;
int j = (int)o; //将引用类型0转换为int类型的值并赋给int类型的变量 

5.变量类型

静态变量:指从程序开始运行,就被分配内存空间的变量,使用static修饰。静态变量都具有初始值,即为默认值。

实例变量:在使用该变量时才进行内存分配。包括类实例变量结构实例变量两类:

class Program
{
	int count;  //声明一个实例变量count
}
struct Program
{
int count; //声明一个实例变量count,初始值为0
}

局部变量:是指在一个独立程序块中声明的变量,只在当前程序块中有效。(ps:局部变量创建必须要初始化赋值

void F()
{
	int j = 0; //声明一个局部变量j
	j++;
}

6.函数的参数类型

值参数:将一个值作为参数传递给函数,传递前先复制一份。

引用参数:是使用ref修饰的参数,引用参数不创建新的存储位置,即引用参数和其基础变量操作的是同一个变量。

int F(ref int i,int j)
{
	i--;
	return i+j;
}
int i = 10;
int sum = F(ref i,100); //调用F函数

输出参数:使用out修饰符声明的参数,也不创建新的存储位置。

int F(int j,out int i)
{
	j--;
	i = j;
	return i+j;
}
int i;
int sum = F(100,out i); //调用F函数

参数数组:声明参数时带有params修饰符,如params int[] array

class Program
{
	public static void Print(params int[] args)
	{
		foreach(int i in args)
		{
			Console.WriteLine(i+" ");
		}
	}
	public static void Main(string[] args)
	{
		int[] arr = new int[]{1,2,3}
		Print(arr); //参数为arr
		Print(1,2,3,4); //参数为1,2,3,4,等效于Print(new int[]{1,2,3,4}
		Print(); //无参数,等效于Print(new int[]{}
		Console.ReadLine();
	}
}

7.C#的特殊语句

7.1 foreach语句

for循环的一种特殊形式:

static void Main(string[] args)
{
	int[] array = { 0, 1, 2, 3, 4 };
	foreach(int i in array)
     {
		Console.WriteLine(i);
     }
	Console.ReadLine();
}

7.2 using语句

  用来定义一个范围,并在一个范围之外释放一个或者多个对象。执行using语句时,一般包括:获取资源、使用资源和释放资源。

语法如下:
using (resource-acquisition) embedded-statement;
常见形式:
using (ResorceType resource = expression) statement;

namespace HelloWorld
{
    class C : IDisposable
    {
		public void F()
        {
			Console.WriteLine("函数F()");
        }
        public void Dispose()
        {
			Console.WriteLine("释放资源!");
        }
    }
    class HelloWorld
	{
		static void Main(string[] args)
		{
			using (C c = new C())
            {
				c.F();  //在结束后会释放资源,调用 Dispose()函数
            }
			Console.ReadLine();
		}
	}
}

7.3 lock语句

  lock语句用于首先获取某个给定对象的互斥锁,即对该对象进行加锁,然后执行下一条语句,最后释放该锁。

object o = 2;
lock(o) //o是被加锁对象,必须为引用类型,相当于操作系统中的PV操作
{
	o++;
}

7.4 checked和unchecked语句

  该语句用于控制整型算数运算和转换的溢出检查。如果希望测试是否产生溢出,则使用checked语句,如果希望忽略溢出,则使用unchecked语句。

8.字符串处理

8.1 string类

  String对象是字符(Char对象)的有序集合,它是引用类型,字符串中的内容不可改变。string类包含一个只读字段和两个属性:

  • Empty字段:便是空字符串
  • Chars属性:获取当前对象中位于指定位置的字符
  • Length属性:字符串长度

常用方法:
public string Insert(int startIndex,string value)

public string Remove(int startIndex)
public string Remove(int startIndex,int count)

public string Replace(char oldChar,char newChar)
public string Replace(string oldChar,string newChar)

public static string Concat(object str0)
public static string Concat(object str0,string str1)

public static string Join(string separator,string[] value)
public static string Join(string separator,string[] value,int startIndex,int count)

public string[] Split(params char[] separator)

public string PadLeft(int totalWidth) //填充字符串

public int IndexOf(string value,int startIndex)
public int IndexOf(string value,int startIndex,int count)

public string Substring(int startIndex)
public string Substring(int startIndex,int length)

public string Format(string format,object arg0) //格式化
public string Format(string format,params object[] args)

8.2 stringBuilder类

  需要对字符串执行重复修改时,可以使用stringBuilder类对字符串进行修改。包含四个属性:

  • Length属性:长度
  • Capacity属性:获取或者设置对象的容量
  • MaxCapacity属性:获取或者设置对象最大的容量
  • Chars属性:获取当前对象中位于指定位置的字符

常用方法参考string类,略

二、命名空间

1. 定义

  命名空间是定义了一个声明区域,提供了一组名称和另一组名称进行区分的方法。
  在程序中如果声明的类比较多,类名可能会重复,C#中就采用命名空间的办法进行区分。对于C#的源文件,不管是否声明了命名空间,编译器都会默认生成。

2. 声明命名空间

语法:

namespace qualified-identifier
{
	//主体代码省略
}

ps:命名空间隐式地为public,不能被修改,也不能有任何修饰符。命名空间可以嵌套

3. 点号运算符

  在同一个命名空间中,各个类之间可以相互调用。如果在不同的命名空间中就需要在类之前加点号。

namespace test
{
	class Program
	{
		static void Main(String[] args)
		{
			System.Int32 i; //使用点号运算符
			//省略
		}
	}
}

4. 双冒号运算符

  在写程序时会遇到自定义类和系统封装类重名,在调用时会产生不明确的引用。此时,就需要用双冒号运算符来限制在该命名空间下查找。global命名空间是.NET Framework应用程序的根命名空间。

namespace test1
{
	class class1
	{
		//主体省略
	}
}
namespace test2
{
	class class1
	{
		//主体省略
	}
	class program
	{
		static void main(String[] args)
		{
			test1:class1 c1 = new test1:class1(); //调用test1命名空间中的class1类
			test2:class1 c1 = new test2:class1();//调用test2命名空间中的class1类
		}
	}
}

5. 导入命名空间

5.1. using指令

  在不同的空间中调用类时需要在调用类之前添加命名空间,如果频繁的使用,就可以使用using指令导入这个命名空间,简化代码。

5.2.导入命名空间并定义类型别名

using new_name = old_name;

6 .NETFramework常用命名空间

常用命名空间表

三、C#的类

1. 声明一个简单的类

public class Program
{
	//主体部分省略
}

2. 类的访问权限

  类的修饰符放在class之前,包括newabstractsealedpublicprotectedinternalprotected七个关键字,控制访问权限的为以下几个:

  • public:公开,访问不受限制
  • protected:只能是本身或者其派生类访问
  • internal:只能是在当前程序中访问
  • private:只能是本身访问

2.1 abstract和sealed修饰符

abstract:指定类为抽象类,抽象类不能实例化。抽象类中可以声明抽象方法,方法不包括具体实现。抽象方法只能创建在抽象类中,且默认为虚方法,所以不能再添加virtual修饰符。
sealed:指定类为密封类,即它不能被继承,所以密封类不能作为基类,也不能作为抽象类。sealed修饰方法时也必须同时添加override修饰符,成为密封方法,可以防止派生类对该方法的重写,

2.2 new修饰符

  在继承关系中,如果子类和父类拥有同名的方法,编译器会发出警告(java中子类方法会覆盖重写同名同参父类方法)。为了避免警告,可以使用new关键字,显示地隐藏从父类继承的成员。ps:new关键字仅仅是隐藏了父类成员而创建一个新的同名成员,覆盖重写需要virtual、override声明,修改的是继承的那一个成员

using System;
public class Class1
{
	public static int x = 1;
	public static int y = 2;
}
public class Class2:Class1
{
	new public static int x = 10;
	static void Main()
	{
		Console.WriteLine(x); //输出结果为10
		Console.WriteLine(y);//输出结果为2
		Console.WriteLine(Class1.x); //输出结果为1
		Console,ReadLine();
	}
}

3. 继承类

类的继承有以下5个特点:

  • 继承是可传递的。子类除了父类的构造函数和析构函数,其他的可见成员都可传递继承。
  • 继承是可扩展的。子类能够添加和修改新的成员(包括属性和方法),但不能溢出成员。
  • 基类成员可隐藏。派生类可以通过声明具有相同名称和新成员来隐藏被继承的成员(使用new关键字)
  • 父类和子类类型可以转换。即父类引用可以指向子类对象。
  • 子类可以重载父类的成员。类可以声明虚的方法、属性、索引器,而派生类可以重写这些函数成员的表现。类的【多态性行为】特征

3.1 虚方法和重写方法

  在一个实例方法中声明virtual,则该方法为虚方法,否则称为非虚方法,虚方法可以由派生来实现,在派生类中实现虚方法需要使用override关键字,称为重写方法(ps:注意new关键字和override关键字的区别)

//下面用代码来说明:

    class Father
    {
        protected string firstname;
        protected string lastname;
      
        public Father()
        {
        }
        public Father(string firstname, string lastname)
        {
            this.firstname = firstname;
            this.lastname = lastname;
        }
        public virtual void GetFullName()
        {
            Console.WriteLine("Father:{0}   {1}", firstname, lastname);
        }
    }
    class Son : Father
    {
        public int age;
        public Son() : base() { }
        public Son(string firstname, string lastname,int age):base(firstname,lastname)  
        {
            this.age = age;
        }
        //public new void GetFullName()
        public override void GetFullName()
        {
            Console.WriteLine("Son:{0}  {1}", firstname, lastname);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Father father = new Father("li", "jing");
            Father son1 = new Son("ne ", "zha",12);
            father.GetFullName();
            son1.GetFullName();
            Console.ReadLine();
        }
    }
/*输出
Father:li jing
Son:ne zha

/*若将public override void GetFullName()改为public new void GetFullName()
Father:li jing
Father:ne zha
*?

分析:可以发现两种方式结果并不一样。前者由于子类直接覆盖重写了父类的方法,所以输出第二行改为了son;而后者只是隐藏了父类的方法,创建了一个同名的方法,在子类调用GetFullName()方法时,首先是从父类方法中进行查找调用,而子类的新建的同名方法并没有执行。

3.2 new、override、virtual的区别

virtual :

  • virtual 关键字用于修饰方法、属性、索引器或事件声明,并使它们可以在派生类中被重写。
  • 默认情况下,方法是非虚拟的。不能重写非虚方法。
  • virtual 修饰符不能与 static、abstract, private 或 override 修饰符一起使用。

override :

  • override 方法提供从基类继承的成员的新实现。
  • 由 override 声明重写的方法称为重写基方法。重写的基方法必须与 override 方法具有相同的签名。
  • 重写的基方法必须是vitural,override,abstract类型的 ,不能重写非虚方法或是静态方法。
  • override不能与vitural,new,static同时使用。
  • override 属性,声明必须指定与继承属性完全相同的访问修饰符、类型和名称,并且被重写的属性必须是 virtual、abstract 的。(注意:这里是属性,不是字段,字段是不能被重写的)

new :

  • 用于创建对象和调用构造函数。
  • 使用 new 修饰符可以显式隐藏从基类继承的成员。
  • 若要隐藏继承的成员,请使用相同名称在派生类中声明该成员,并用 new 修饰符修饰它。

4. 属性

  属性(property)是C#语言特有的一种机制,可以用于访问对象或类成员,和字段的语法相同,但是属性不表示存储位置,而是通过访问器的机制来获取或修改其值,分为get访问器和set访问器。

  • get访问器相当于一个无参的方法,返回值的类型和属性的类型相同,且必须包含return语句。
  • set访问器相当于返回值为void的方法,且该方法只有一个参数,参数类型和属性的类型相同。特别的,该方法的参数始终约定为value。
public class Program
{
	private string name;
	public string Name   //创建一个名为Name的属性
	{
		get{return name;}
		set{name = value;}
	}
	static void Main()
	{
		Program p = new Program();
		p.Name = "哪吒";  //调用set访问器,给name赋值
		String name = p.Name; //通过get访问器,获取name的值
		Console.WriteLine(name);
		Console.ReadLine();
	}
}

5. 索引器

  程序开发中有时需要对类的字段批量的进行操作,例如字段赋值和遍历,索引器允许向索引数组一样的方式索引对象,这样就可以使用循环语句,通过下标改变访问对象中的字段。声明索引器时,需要用this关键字。

//定义一个索引器
namespace test
{
	public class Program
	{
		private string[] list;
		public string this[int index]
		{
			get{return list[index];}
			set
			{
				if(index>-1 && index<list.Length)
					list[index] = value; //设置list数组中元素的值
			}
		}
		public Program(int count) //Program的构造函数
		{
			list = new string[count];
			for(int i=0;i<count;i++)
			{
				list[i]=i.ToString();
			}
		}
		public static void Main()
		{
			int count = 10;
			Program p = new Pragram(count);
			for(int i=0;i<count;i++)
			{
				Console.WriteLine(p[i]); //通过索引器访问对象中的list数组
			}
			Console.ReadLine();
		}
	}
}

虽然索引器也是一种属性,但是它和属性存在以下5点区别:

  • 属性存在一个名称,而索引器由this关键字指定
  • 属性可以是静态属性(static修饰),而索引器始终是实例成员(不能使用static修饰)
  • 属性可以通过成员来访问,而索引器必须通过索引访问
  • 属性的get访问器不带参数,而索引器的get访问器需要参数(索引)
  • 属性的set访问器只带value一个参数,而索引器的get访问器还需要索引

注意:虽然索引器和数组类似,但是索引器不属于变量,不能作为ref或out参数传递

6. 构造函数

  构造函数是一种特定的方法,用来初始化新对象的成员,如果没有限时提供构造函数,则编译器会创建一个默认的无参构造函数。默认的无参构造函数会调用父类的无参构造函数,如果父类也没有无参构造函数,则会发生编译时错误(该种情况编译器不会自动生成,所以某类需要被继承时,最好手动添加无参构造函数)
  如果构造函数使用private关键字修饰,则为私有构造函数,通常用于单例模式中。
  如果用static关键字修饰,则为静态构造函数。静态构造函数在创建第一个实例或者引用任何静态成员变量时调用,而且至多执行一次。

class C 
    {
        static string name;
        static C()
        {
            Console.WriteLine("初始化C类");
        }
        public static void show()
        {
            Console.WriteLine("show函数执行");
        }

    }
    class HelloWorld
	{
		static void Main(string[] args)
		{
            C.show();
            Console.ReadLine();
		}
	}
/*结果

初始化C类
show函数执行
*/

静态构造函数具有以下5个特点:

  • 静态构造函数一般用于初始化静态字段、只读字段的值
  • 既没有访问修饰符(默认为private),也没有参数
  • 无法直接调用静态构造函数,也无法控制何时执行
  • 在创建第一个实例或者引用任何静态成员之前将自动调用静态构造函数
  • 如果类中包含Main()方法,那么该类的静态构造函数在Main()方法之前执行。

7. 析构函数

  析构函数就是手动释放该实例占用的资源,并销毁该类的实例。析构函数名称和类一样,并带有~字符。通常情况下编译器会自动释放资源生成一个析构函数。

析构函数具有以下4个特点:

  • 没有修饰符,也没有参数。
  • 无法调用析构函数,他们是被自动调用的,程序员并不知道何时被调用回收资源。
  • 只能对类使用析构函数,且一个类只有一个析构函数。
  • 无法继承或者重构析构函数。

四、结构

  结构和类很相似,但是类是引用类型,结构是一种值类型,且不需要堆分配。

1. 结构的声明

struct-modifiers struct identifier struct-interface struct-body;
示例:

public struct Str
{
	//结构主体部分省略
}

2. 结构修饰符

  可以放在struct前面的关键字有newpublicprotectedinternalprivate,以下几个控制可访问性:

  • public:表示结构是公开的,访问不受限制
  • protected:表示该结构只能被本身和派生结构访问
  • internal:只能在当前应用程序中访问
  • private:只能是本身访问

ps:new 修饰符可以指定接口隐藏同名的继承成员

3. 结构和类的异同

相同点

  结构和类一样包含多种类型成员。包括:常量、字段、方法、属性、事件、索引器、运算符、实例构造函数、静态构造函数。

不同点
在这里插入图片描述
ps:由于结构不支持继承,所以在声明成员时,不能使用protected或者protected internal以及virtual修饰符,同时结构类型也不会是抽象,不能使用abstractsealed修饰符。结构虽然不支持继承,但是可以实现接口。

五、数组

1. 声明和创建数组

声明语法如下:
type[] arrayname

int[] array; //声明一个int类型数组array
string[] array2; //声明string类型数组array2

array = new int[10]; //实例化数组array

2. 初始化数组

2.1 直接赋值

string[] s1 = {"apple","banana"};  

2.2 实例化赋值+指定长度

string[] s1 = new string[2] {"apple","banana"};   //单个数值必须等于数组长度

2.3 实例化赋值+省略长度

string[] s1 = new string[] {"apple","banana"};  

2.4 直接设置每个元素的值

string[] s1 = new string[5];
for(int i=0;i<5;i++)
{
	s1[i] = i.ToString();
}

3. 多维数组

二维数组:
声明语法:
type[,] arrayname
实例化语法:
new type[m,n]

多维数组:
声明语法:
type[,,] arrayname
实例化语法:
new type[m,n,L]

//创建一个三维数组
string[,,] array = new string[1,2,3];

4. 交错数组

交错数组又称“数组的数组”,即数组的每个元素都是一个数组。

string[][] array = new string[10][]; //声明一个交错数组

5. 静态数组常用的属性和方法

常用属性表:
在这里插入图片描述

常用方法表:
在这里插入图片描述

6. 动态数组

  动态数组由System.ArrayList类实现,在保存时仅保存元素的引用而不是具体的值,因此动态数组的类型可以是任何类型,而且各个元素的数据类型也可以不相同。

示例:

using System;
using System.Collections;
namespace test
{
	class Program
	{
		static void Main()
		{
			ArrayList mylist = new ArrayList();
			mylist.add(2021);
			mylist.add("3月4日");
			foreach(object obj in myList)
			{
				Console.WriteLine(obj);
			}
			Console.ReadLine();
		}
	}
}

动态数组常用属性:
在这里插入图片描述

动态数组常用方法:
在这里插入图片描述

六、 接口

  接口定义一种协议,实现该接口的类或者结构必须遵守该协议。C#中接口包含4中成员:方法、属性、事件、索引器。

定义一个简单的接口:

public interface MyInterface
{
	//接口主体部分省略
}

1. 接口的继承

  在程序的开发中,有一些的接口是通用的,接口可以继承于一个或者多个其他接口。被继承的接口成为该接口的显示基接口。基接口之间用逗号分割。
  接口的继承类似与类的继承,详细内容省略。

2. 接口的实现

  如果一个类或结构实现某接口,则它还隐式实现该接口的所有基接口。

2.1 实现属性

    public interface Iinterface
    {
        string Name  //定义一个空的Name属性
        {
            get;
            set;
        }
    }
    class Program:Iinterface
	{
        public string Name //实现属性
        { 
            get
            {
                return "张三";
            }
            set
            {
                Name = value;
            }
        }

        static void Main(string[] args)
        {
            Program p = new Program();
            Console.WriteLine(p.Name);
            Console.ReadLine();
        }
    }

2.2 实现索引器

    public interface Iinterface
    {
        string this [int index]  //定义了一个空索引器
        {
            get;
        }
    }
    class Program:Iinterface
	{
        public string this[int index] // 实现索引器
        {
            get
            {
                return "张三";
            }
        }
        static void Main(string[] args)
        {
            Program p = new Program();
            Console.WriteLine(p[0]);  //输出 张三
            Console.ReadLine();
        }
    }

2.3 实现方法

    public interface Iinterface
    {
        void Print();  //定义一个空方法,默认为抽象方法,不必abstract修饰
    }
    class Program:Iinterface
	{

        public void Print()
        {
            Console.WriteLine("抽象方法被实现了");
        }

        static void Main(string[] args)
        {
            Program p = new Program();
            p.Print();
            Console.ReadLine();
        }
    }

2.4 实现事件

  事件是一种特殊的委托,在实现事件时,需要add访问器和remove访问器。add访问器用来注册一个时间,remove访问器用来移除一个事件。

public interface Iinterface
    {
        event EventHandler Print; //定义接口事件Print
    }
    class Program:Iinterface
	{
        event EventHandler print;
        event EventHandler Iinterface.Print  //实现事件
        {
            add
            {
                print += value;
            }

            remove
            {
                print -= value;
            }
        }

        static void Main(string[] args)
        {
            Program p = new Program();
            Console.ReadLine();
        }  
    }

3. 抽象类和接口

  抽象类使用abstract修饰符,表示类是不完整抽象的,且只能作为基类。
抽象类和非抽象类有以下4个区别:

  • 抽象类不能直接实例化。
  • 允许抽象类包含抽象成员。
  • 抽象类不能被密封。
  • 当从抽象类派生非抽象类时,必须实现所有的抽象成员。

抽象类和接口有以下6个方面的区别:

  • 一个类能实现多个接口,但是只能有一个父类(即:C#为单继承
  • 接口中所有方法都必须是抽象的,但是抽象类中可以有非抽象方法。
  • 抽象类是一个不完整的类,需要进一步细化;而接口是一个行为的规范
  • 接口不属于继承结构,它与继承无关,因此无关的类也可以实现同一个接口。
  • 接口不具备继承的任何基本特点,他只是承诺了能够调用的方法。
  • 接口可以用于支持回调,用于回调的接口仅仅是提供指向方法的指针。

七、 委托和事件

1.使用委托

  委托的含义指的是,建立一个方法链条,让对象依次去执行这个链条上的各个方法,从而简化代码,提高效率, 容易维护。而事件是一种特殊的委托,可以在某些操作发生时自动地发出通知。

1.1 声明委托

声明语法:
delegate-modifiers delegate return-type identifier
示例:

public delegate int MyDelegate(int i,int j); //声明委托Mydelegate

可用的修饰符(delegate-modifiers)有new、public、protected、internal、private,功能和前文一致。

2.2. 向委托注册方法

  委托声明好了需要给委托添加方法列表,方法需要签名必须和委托指定的签名完全匹配。匹配规则如下:

  • 方法的返回值类型必须和委托的返回值类型相同
  • 参数必须相同(参数的个数和参数类型相同),参数的具体名称可以不同。

ps:静态方法满足匹配条件也可以接受委托。

示例:

    class Program
	{
        public delegate void Mydelegate(int i);  //声明一个委托
        public void F1(int i)
        {
            Console.WriteLine(i);
        }
        public void F2(int i)
        {
            Console.WriteLine(i);
        }
        static void Main(string[] args)
        {
            Program p = new Program();
            Mydelegate d1 = new Mydelegate(p.F1);
            d1(1); //通过委托d1执行F1()函数,输出 1
            Mydelegate d2 = new Mydelegate(p.F2);
            d2(1); //通过委托d2执行F2()函数,输出 1
            Mydelegate d3 = d1 + d2;
            d3(2); //通过委托d3执行连续执行F1()、F2()函数,输出 2 2
            Mydelegate d4 = d3 - d1;
            d4(3); //过委托d4执行连续执行F2()函数,输出 3
            Console.ReadLine();
        }  
    }

3. 使用事件

  事件构建在委托的基础上,是一种信号机制,当事件触发后,执行相应代码。

3.1 声明事件

  事件是一种特殊类型的委托,包含两个参数:指示事件源的参数其他相关信息e参数。e类型为System.EventArgs类型或者其派生类类型。声明事件有以下两个步骤:

  1. 声明事件的委托
public delegate void EventHandler(object sender,EventArgs e);
  1. 声明事件本身
public event EventHandler Print; //声明事件Print

3.2 注册、删除、调用事件

    class Program
	{
        public delegate void EventHandler(Object sender, EventArgs e); //1、声明一个委托
        public event EventHandler Print; //2、声明事件
        public void F1(Object sender, EventArgs e)
        {
            Console.WriteLine("调用F1");
        }
        public void F2(Object sender, EventArgs e)
        {
            Console.WriteLine("调用F2");
        }
        static void Main(string[] args)
        {
            Program p = new Program();
            p.Print += new EventHandler(p.F1); //给事件Print注册一个方法F1
            p.Print += new EventHandler(p.F2); //给事件Print注册一个方法F1
            if (p.Print != null)
            {
                p.Print(null, null);  //调用事件
            }
            p.Print -= new EventHandler(p.F1); //给事件Print移除一个方法F
            Console.WriteLine("移除一个事件后:");
            if (p.Print != null)
            {
                p.Print(null, null);  //调用事件
            }
            Console.ReadLine();
        }
    }

八、 异常处理

1. 异常产生

异常产生的原因可以归纳两大类:

  • 在执行某些语句和表达式时,会出现一些例外情况,从而无法完成继而引发一个异常
  • 应用程序通过throw语句特意产生一个异常

2. 捕获异常

try{
	//有可能会发生异常的代码块
}catch{
	//当try中发生异常时,相应的代码
}finally{
	//不管try中代码是否产生异常都一定会执行
}

3. 异常的基类System.Exception

System.Exception类常用的属性表:
在这里插入图片描述

常见的异常类表:
在这里插入图片描述

九、 泛型

  泛型是具有占位符的类、结构、接口和方法。C#中的泛型可以将类型作为参数进行传递。

1. 类型形参

//声明一个名称为Test<T>的泛型类,T为类型参数
public class Test<T,U>
{
	...
}
 //创建泛型类的实例t
Test<int,string> t = new Test<int,string>(); 

2. 设置类型形参的约束

形参类型的约束使用where上下文关键字指定,下面指定常用的6种类型的约束:

  • T:struct,类型参数必须是值类型,但不能是Nullable类型
  • T:class,类型参数必须是引用类型
  • T:new(),类型参数必须具有无参的公共构造函数。如果存在多个约束,new()约束必须放在最后
  • T:<基类名>,类型参数必须是指定的基类或者其派生类
  • T:<接口名称>,类型参数必须是指定的接口或者实现指定的接口
  • T:U,T的类型必须是U提供的参数或者派生自为U提供的参数
public class Test<T> where T:class,Iinterface,new()
//为泛型类添加约束,T的类型参数必须是引用类型,且要实现该接口,以及new构造函数约束
{
	//主体部分省略
}

3. 泛型类的成员

泛型类的成员可以直接使用其所属类的类型形参:

public class Test<T>
{
	private T t;
	public static T t2;  //泛型类静态字段T相同才公用同一个字段
	public T getValue()
	{
		return t;
	}
}

4. 泛型方法

  泛型方法和泛型类性质相同,都是用类型参数,也可以对形参加以约束。

示例:

public class Test
{
	public void Swap<T>(ref T left,ref T right) //声明泛型方式Swap
	{
		T tem;
		tem = left;
		left = right;
		right = tem;
	}
}
Test t = new Test();
int i = 10;
int j = 20;
t.Swap<int>(ref i,ref j);  //调用泛型函数

泛型方法需要注意的两个内容:

  • 泛型方法,也可以用abstract、virtual、override修饰符进行修饰。虚泛型方法在被覆盖重写时,类型参数T也可被覆盖
  • 泛型方法也可以被委托,创建委托时需要确定类型参数,泛型方法必须符合委托的签名

十、 分部类型和可空类型

1. 分部类型

  分部类型允许将一个类、结构或者接口分成几个部分,由不同的人员去编写,由编译器自动完成整合,增加了代码的灵活性。注意:partial修饰符不能用于声明委托或者枚举

1.1 声明分部类

  声明分部类与声明分部结构、接口形式类似,以声明分部类为例:

public partial class People
{
	string name;
}
public partial class People
{
	int age;
}

等同于:

public class People
{
	string name;
	int age;
}

1.2 处理基接口

  一个类的各个部分可以指定不同的基接口,最终类型将实现所有分部声明所列的全部接口。

public partial class Program:Iinterface1 //实现了Iinterface1 接口
{
}
public partial class Program:Iinterface2//实现了Iinterface2 接口
{
}
public partial class Program:Iinterface1,Iinterface2
{
}

分析:该类Program的基接口为Iinterface1Iinterface2

2. 可空类型

  编程时,有的字段未被使用,通常解决的办法是使用一个占位值,或者附加一个字段指示其是否被使用。在C#中可以用可空类型来解决这个问题。可空类型是Syste.Nullable<T>类型的对象,其中的T为非可空类型。

2.1 声明可空类型

  声明可空类型有以下两种形式:

  • System.Nullable<T> variable
  • T? variable
//声明一个可空int类型的变量day
int? day;

2.2 可空类型的属性

可空类型有两个常用的属性HasValueValue

  • HasValue:类型为bool,表示该实例是否有一个确定的值
  • Value:表示实例的一个确定的值

注:任何可空类型都有一个默认值,该默认值是一个实例,其HasValue为false,Value是一个不确定的值。

2.3 bool?类型

  bool?类型为可空bool类型,可以表示3个值:true,false,null。可空bool?类型逻辑运算真值表如下:
在这里插入图片描述

十一、 赋值

1. 使用隐形局部变量

  声明变量都需要指定类型,但是隐形局部变量可以使用var,不必明确它的类型,可以从初始化它的表达式中推导出来。
  声明隐形局部变量时,需要遵循以下4条规则:

  • 必须包含变量的初始化表达式
  • 初始化表达式不能引用变量本身
  • 初始化表达式不能是数组初始值设定
  • 编译时,编译器必须能够确定其类型
var v1;
var v2 = v2 = 1;
var v3 = {1,2,3,4};
var v4 = null;

上述语句都会发生错误,正确的示例如下所示:

var day = 10;
var arr = new []{1,2,3,4,5};
foreach(var i in arr)
{
	...
}

2. 使用对象初始化器

  对象初始化器不显示的调用该对象的构造函数,可以通过一组设定项对对象进行赋值。设定项类似于python中的字典。

class Program
{
	private int name = "张三";
	private int age = 20;

	static void Main()
	{	// 使用对象初始化器创建实例
		Program p = new Program
		{
			name = "李四";
			age = 22;
		};
}

3. 使用匿名类型

  使用匿名类型可以简化代码,避免无意义的变量命名。

创建匿名类:

//创建一个匿名对象person
var person = new{ id = 1, name = "zhang", age = 20};
//等价于下面的代码:
class Program
{
	private int id;
	private string name;
	private int age;
	public Program(int id,string name,int age)
	{
		this.id = id;
		this.name = name;
		this.age = age;
	}
	public static void Main(string[] args)
	{
		Program person = new Program(1,"zhang",20);
	}
}

创建匿名数组:

var array = new[] {"a","b","c"}; //初始化赋值必须是同一种类型

十二、 迭代器、匿名方法和扩展方法

1. 迭代器

  迭代器是一个产生有序值序列的语句块,使用yield关键字,作用是使遍历更加容易。通过创建迭代器,可以在foreach循环中使用类对象。迭代器分为非泛型迭代器泛型迭代器

1.1 非泛型迭代器

最常用的方法是实现IEnumerable接口的getEnumerator()方法,它返回一个循环访问集合的枚举数。

using System.Collections;
using System;

    class Program : IEnumerable
    {
        public IEnumerator GetEnumerator()
        {
            for (int i = 0;i<10;i++)
            {
                yield return i;
            }
        }

        public static void Main()
        {
            Program p = new Program();
            foreach(int i in p)  //实现了IEnumerable接口,可以对实例p使用迭代器
            {
                Console.Write(i + " ");
            }
            Console.ReadLine();
        }
    }

1.2 泛型迭代器

需要实现IEnumerableIEnumerable<T>接口的getEnumerator()方法。

using System;
using System.Collections;
using System.Collections.Generic;
 
    public class Program2<T> : IEnumerable<T>
    {
        Stack stack;
        public Program2()
        {
            stack = new Stack();
        }
        public void push(T item)
        {
            stack.Push(item);
        }
        public T pop()
        {
            return (T)stack.Pop();
        }
        public IEnumerator<T> GetEnumerator() //实现的方法
        {
            while(stack.Count > 0)
            {
                yield return (T)stack.Pop();
            }
        }

        IEnumerator IEnumerable.GetEnumerator() //实现的方法
        {
            return GetEnumerator();
        }    
    }
    public class Test
    {
        public static void Main()
        {
            Program2<int> p = new Program2<int>();
            for (int i = 0; i < 10; i++)
            {
                p.push(i);
            }
            foreach (int m in p)
            {
                Console.WriteLine(m);
            }
            Console.ReadLine();
        }
    }

1.3 使用IEnumeratorIEnumerator<T>接口

IEnumerator接口包含MoveNext()方法,Reset()方法和Current属性,于是迭代器也可以通过以下的方法进行读取。(foreach本质上就是一个迭代器)

    class Program : IEnumerable
    {
        public IEnumerator GetEnumerator()
        {
            for (int i = 0;i<10;i++)
            {
                yield return i;
            }
        }

        public static void Main()
        {
            Program p = new Program();
            IEnumerator ie = p.GetEnumerator();
            while (ie.MoveNext())
            {
                Console.Write(ie.Current+" ");
            }
            /*foreach (int i in p)
            {
                Console.Write(i + " ");
            }*/
            Console.ReadLine();
        }
    }

IEnumerator<T>接口和它的非泛型的接口类似,只是多一个Dispose()方法,用于释放实例占用的资源。

2. 使用匿名方法

  匿名方法是一种隐藏名称的方法,是创建与特定委托实例相关联的命名代码块的一种方式。

2.1 创建匿名方法

向btnAdd(Button的一个实例)的Click事件注册一个匿名方法:
btnAdd.Click += delegate
{
	Console.WriteLine("这是一个匿名方法");
}
等效于下面的代码:
btnAdd.Click += new EventHandler(AddClick);
void Addclick(object sender,EventArgs e)
{
	Console.WriteLine("这是一个匿名方法");
}

2.2 将匿名方法作为参数进行传递

  类似于python中直接将函数作为参数进行传递,C#中如果要将函数作为参数进行传递的话,就可以使用该方法。

    class Program 
    {
        delegate int mydelegate(int i);
        static int[] Array(int[] array, mydelegate d) //定义一个Array函数
        {
            int[] result = new int[array.Length];
            for (int i = 0; i < result.Length; i++)
            {
                result[i] = d(array[i]); //调用d委托对array数组元素进行计算
            }
            return result;
        }
        public static void Main()
        {
            int[] array = { 1, 2, 3, 4 };
            int[] result = Array(array, delegate (int i) { return i * i; });
            foreach(var i in result)
            {
                Console.WriteLine(i);
            }       
            Console.ReadLine();
        }

    }

*3. 扩展方法

  扩展方法可以向现有类型添加新的方法,使用时不需要修改或者编译类型本身的代码。扩展方法必须声明为静态方法,且必须声明在静态类中。

3.1 声明扩展方法

public static class Program
{
	public static int ToInt32(this string str) //第一个参数,表示给该种类型添加扩展方法
	{
		return Int32.Parse(str);
	}
}

一般情况下不建议使用扩展方法。在声明扩展方法时,需要注意下面两点:

  • 如果扩展方法与其相关类型中定义的方法具有相同的签名,则扩展方法永远不会被调用。
  • 扩展方法属于所在的命名空间,所以使用using引入某个命名空间时,其所有的扩展方法都会被导入。

3.2 调用扩展方法

   public static class Test
    {
        public static int ToInt32WithD(this string str,int d)
        {
            return (int)((double)Int32.Parse(str) / d); //将字符串转化为Int32类型,并除以d
        }
    }

    public class TestMain
    {
        static void Main()
        {
            string str = "12345";
            int t = 10;
            int v = str.ToInt32WithD(t); //对字符串类型使用扩展方法
            Console.WriteLine(v);
            Console.ReadLine();
        }
    }

十三、 Lambda表达式和查询表达式

1. Lambda表达式

  Lambda表达式是一种简写匿名函数,可以包含表达式和语句,并且适用于创建委托和事件。

语法构成:
(input parameters) => empression;

int n = (int m) => m * m; //计算参数m的平方,并返回结果
() => Console.WriteLine("this is a Lambda empression"); //空参
m => m * 2 //只有一个参数时,可以省略括号
(m,n) => m * n; //两个参数

(m,n) => {int result = m * n;Console.WriteLine(result);} //语句块

2. 查询表达式

  查询表达式是一种使用查询语法表示的表达式,类似于SQL的子句。必须以from子句开头,以select或group子句结束。中间可以包含其他子句,共包含8个基本子句。

  • from子句:指定数据源和范围变量,指定的数据源的类型必须是IEnumerable或者其派生类
  • select子句:指定查询结果的类型和表现形式
  • where子句:筛选逻辑条件
  • let子句:引入用来临时保存查询表达式中的子表达式结果的范围变量
  • orderby子句:排序,升序或者降序
  • group子句:对查询结果进行分类
  • into子句:提供一个临时标识符
  • join子句:连接多个查询操作的数据源
int[] array = new int[]{1,2,3,4,5,6};
var query1 = form n in array 
			select n;   //查询每一个元素

var query2 = form n in array
			where n > 6
			select n;   //查询大于6的元素

var quer3 = form a in array
			form b in array
			select a + b;   //查询多个数据源数据,并求和

var query3 = form n in array
			let isEven = return n%2 == 0 ? true : false
			where isEven
			select n;   // 查询偶数

var query4 = form n in array
			where n < 6
			orderby n descending
			select n;   // 排序,ascending(升序)

var query5 = form n in array
			where n < 6
			group n by n % 2;   // 按照n%2表达式的值对查询结果进行分类

var query6 = form n in array
			where n < 6
			group n by n % 2 into g
			form sn in g
			select sn;  //使用into保存分组结果
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值