随便看看,写得很拉

C#复习提纲

1、C#概述

1.1 CLR的概念和功能

  • CLR(Common Language Runtime公共语言运行库)是一个可由多种编程语言使用的“运行时”。
  • CLR的核心功能内存管理程序集加载安全性异常处理线程同步泛型尾调用指令和基本的公共语言基础结构类型系统等。

1.2 C#的优势和特点

C#是一种安全的、稳定的、简单的、优雅的,由C和C++衍生出来的面向对象的编程语言。它在继承C和C++强大功能的同时去掉了一些它们的复杂特性(例如没有宏以及不允许多重继承)。C#综合了VB简单的可视化操作和C++的高运行效率,以其强大的操作能力、优雅的语法风格创新的语言特性便捷的面向组件编程的支持成为.NET开发的首选语言。C#适用于Windows平台。开发快速上手容易安全性强包含技术全面

1.3 C#开发.NET应用程序的步骤

image-20211224200753653

image-20211224200815014

1.4 C#源程序文件、vs解决方案文件、Web项目文件、C#项目文件的后缀名

扩展名 描述
.cs Visual C#源程序文件 表示属于单个项目的窗体、用户控件、类和模块文件。
.sln Visual Studio解决方案文件 组织解决方案中的项目、项目子项和其他子项。
.aspx Web项目文件 表示 Web 项目子项文件。
.suo 解决方案用户选项文件 记录所有可能和解决方案相关的选项。
.csproj Visual C#项目文件 表示属于多个项目的窗体、用户控件、类和模块文件。

2、C#编程基础

2.1 System.Array的属性与方法

  • Length属性可以获取数组的长度
  • GetLength(n)方法可以得到第n维的数组长度
int[,,] k = new int[1,2,3];
Console.WriteLine(k.GetLength(0));  // 1
Console.WriteLine(k.GetLength(1));  // 2
Console.WriteLine(k.GetLength(2));  // 3
  • Sort方法可以对数组按升序排列
  • Reverse方法把数组中的元素反序

2.2 一维数组的声明、创建、初始化

  • 声明数组:数组元素类型[ ] 数组名;
float[] fs;
  • 创建数组:数组名= new 数组元素类型[数组元素个数];
fs = new float[4];
  • 数组名中存放数组首地址,声明数组和创建数组可同时进行
float[] fs=new float[4];
  • 允许使用int型常量指定数组元素的个数
const int size=4;    
float[] fs = new float[size];
  • 初始化数组:若数组元素是基本数据类型,系统会赋予每个元素一个默认值
// 初始化方式一
for  ( int i = 0; i < fs.Length; i++)
	fs[i] = (float) 2007.0/(i+1);

// 初始化方式二:声明数组的同时初始化
type[] arrayName = new type[size] { val1, val2, … , valn };
float[] fs = new float[4] { 21.34f, 37.58f, 776.46f, 834.23f };

// 初始化方式三:声明数组的同时初始化,省略size
type[] arrayName=new  type[] { val1, val2, … , valn };
float[] fs = new float[] { 21.34f, 37.58f, 776.46f, 834.23f };

// 初始化方式四: 声明数组的同时初始化,省略new
float[] fs = { 21.34f, 37.58f, 776.46f, 834.23f };

// 初始化方式五: 声明与初始化分开
float[] fs;
fs = new float[] { 21.34f, 37.58f, 776.46f, 834.23f };

2.3 规则与锯齿状二维数组的声明、创建、初始化

  • 声明数组
// 数组元素类型[ , ]  数组名;
Point[ , ]  ps;
// 数组元素类型[ ][ ]  数组名;
Point[ ][ ]  ps;
  • 创建数组
// 数组名= new  数组元素类型[一维元素个数][二维元素个数];
ps = new Point[4][];  // 锯齿状二维数组

// 数组名= new  数组元素类型[一维元素个数 , 二维元素个数];
ps = new Point[4, 5]; // 规则二维数组

// 构成二维数组的一维数组不必有相同的长度
int[][] a = new int[3][ ];
a[0] = new int[6];
a[1] = new int[12];
a[2] = new int[8];

  • 初始化数组
// 初始化方式一:    
Point[][] ps = new Point[3][];   // 锯齿状二维数组的初始化                                       
for ( int i = 0; i < ps.GetLength(0); i++)
{  
	ps[i] = new Point[5];
	for ( int j = 0; j < ps[i].Length;  j++)
		ps[i][j] = new  Point(i+1,j+1);
}

// 初始化方式二: 
type[ , ]  arrayName = new  type[size1,size2] {{val11, val12, ……, val1n}, 
                                                 {val21, val22, ……, val2n}, … , 
                                                 { valm1, valm2,……, valmn} };
int[ , ]  a = new int[2,3]{{0,1,2}, {3,4,5}};

// 初始化方式三:
type[ , ] arrayName = new type[ , ] {{ val11, val12, …… , val1n }, { val21, val22, ……, val2n },…, 
                                     { valm1, valm2, ……, valmn } };
int[ , ] a = new int[ , ]{{0,1,2}, {3,4,5}};

// 初始化方式四: 
       type [ , ]   arrayName= { {val11, val12, ……, val1n}, 
        {val21, val22, ……, val2n},…, { valm1, valm2, ……, 
         valmn} };
      int [ , ]  a= {{0,1,2}, {3,4,5}};

// 初始化方式五:
type [ , ]  arrayName;
arrayName  =new  type[size1,size2 ] {{val11, val12, ……, val1n}, {val21, val22, ……, val2n},…,
                                     { valm1, valm2, ……, valmn} };
int [ , ]  a;
a=new  int[2, 3]{{0,1,2}, {3,4,5}};

// 初始化方式6: 不规则数组的初始化
type [ ][ ]   arrayName = new  type[size][];
arrayName[0] = new  type[size0]{ val1, val2, …, valn0 }
arrayName[1] = new  type[size1]{ val1, val2, …, valn1 }
……  …… 
char[][]  st1=new  char[3][];
st1[0] = new char[] { ‘S’,’e’,’p’,’t’,’e’,’m’,’b’,’e’,’r’};
st1[1] = new char[] { ‘O’,’c’,’t’,’o’,’b’,’e’,’r’};
st1[2] = new char[] { ‘N’,’o’,’v’,’e’,’m’,’b’,’e’,’r’};

2.4 数组的Length属性

// 数组名.Length 是规则二维数组含有的总元素的个数
int[,] c = new int [3,4]; // c.Length为12

// 数组名.Length 是锯齿状二维数组含有的一维数组的个数
int[][] c = new int [3][]; // c.Length为3

2.5 foreach语句

  • C#的foreach循环用于遍历集合中的每个元素

  • foreach ( 类型 标识符 in 集合表达式 )
    语句;

  • 被遍历的集合包括C#数组,System.Collection名称空间中的集合类,以及用户定义的集合类。可以是任何支持IEnumerable接口的集合类。

int[] ints={1,2,3};
foreach (int  temp  in  ints)
   Console.WriteLine(temp);

2.6 动态数组ArrayList

  • 需引入System.Collections命名空间
  • Array与ArrayList的区别
    • Array的容量固定,而ArrayList可以动态扩充
    • ArrayList提供添加、插入或移除某一范围元素的方法。在Array中,只能一次获取或设置一个元素的值
    • Array可以是多维的,而ArrayList是一维的

3、面向对象编程基础

3.1 属性及属性的修饰符

属性的种类

  • 可读写属性
  • 只读属性
  • 只写属性
  • static属性:只能访问静态数据

属性修饰符和方法修饰符相同,包括newstaticvirtualoverride

3.2 属性与字段的区别和联系

  • 均可以访问对象中包含的数据
  • 属性不能直接访问数据
  • 属性可以限制数值的取值范围
  • 属性可设置为仅读、仅写、可读写
  • 属性可以象方法一样使用virtual、abstract、override、new、static等修饰符,而这些修饰符不能用于字段

3.3 抽象属性

public abstract class Fruit
{
    public string  vendor { get; set; } //默认为private

    public abstract float Price { get; } //抽象属性必须是公有的 

    public abstract void GrowInArea(); 	//抽象方法必须是公有的
}

3.4 类的修饰符

  • 访问修饰符
访问修饰符 同类 同程序集子类 同程序集非子类 不同程序集子类 不同程序集非子类
public
internal
protected
private
protected internal
  • 其他修饰符
    • abstract 抽象类,含有抽象成员,因此不能被实例化,只能用作基类
    • sealed 密封类,不能从该类派生出其他类

3.5 方法的修饰符

  • 访问修饰符
访问修饰符 同类 同程序集子类 同程序集非子类 不同程序集子类 不同程序集非子类
public
internal
protected
private
protected internal
  • 其他修饰符
    • abstract 抽象类,含有抽象成员,因此不能被实例化,只能用作基类
    • sealed 密封类,不能从该类派生出其他类
    • new
    • static
    • virtual
    • override

3.6 修饰符的无效组合

修饰符 不能和下列选项一起使用
static virtualabstractoverride
virtual staticabstractoverride
override newstaticvirtual
abstract staticvirtual
new override

3.7 集合与foreach循环

  • .NET提供实现集合的接口,包括IEnumerable, ICollection, IList,IDictionary,IDictionaryEnumerator, IComparer等,只需继承实现集合接口也可以直接使用.NET已经定义的集合类,包括Array, ArrayList, Queue, Stack, BitArray, Hashtable等
  • 自定义集合,指实现System.Collections提供的集合接口的集合
  • 实现IEnumerable的同时也要实现IEnumerator接口
public   interface   IEnumerator
{
     object    Current
    { 
        get();
     }
     bool   MoveNext();
     void   Reset();
}
  • foreach循环隐藏调用IEnumerable的GetEnumerator方法得到Iterator,并将集合指向-1的位置
  • foreach循环反复调用MoveNext方法移动集合,使用Current获取集合当前元素

3.8 索引器

  • 使用索引器的目的是为了能够像数组一样访问类中的数组型的对象
  • 通过对对象元素的下标进行索引,可以访问指定的对象
  • 索引器类似于属性,不同的是索引器有索引参数
  • 索引参数可以使用任何数据类型,但常用的是uint、int、ushort、short、string等
  • 不能使用foreach循环访问索引器
  • 索引器的修饰符可以是new、public、protected、internal、private、virtual、sealed、override、abstract

3.9 索引器与数组、属性的不同点

索引器与数组的不同点

  • 索引器的索引值类型不受限为整数
  • 索引器允许重载
  • 索引器不是一个变量
  • 索引器并不直接对应数据存储的地方
  • 索引器有get和set访问器,指明要读取或写入索引器元素时,需要执行的代码
  • 可以认为索引器的[]重载了数组的[]

索引器与属性的不同点

  • 标识方式

    • 属性以名称来标识;索引器则以函数签名来标识
  • 索引器可以被重载,属性不能

  • 索引器不能声明为static,而属性可以

  • 索引器可以使用多个参数访问

public  int  this[string name]
{
    get
    {
        for (int i = 0; i < count; i++)
            if (sset[i].StuName.Equals(name))
                return i;
        return -1;
    }
    set
    {
        for (int i = 0; i < count; i++)
            if (sset[i].StuName.Equals(name))
                sset[i].StuAge = value;
    }
}

3.10 委托

​ 委托是函数的封装,它代表一类函数,它们都符合一定的签名:拥有相同的参数列表和返回值类型。同时,委托也可以看成是对函数的抽象,是函数的”类”。此时,委托的实例将代表一个具体的函数。

3.10.1 委托应用——异步回调

异步回调:由于实例化委托是一个对象,所以可以将其作为参数进行传递,也可以将其赋值给属性。这样,方法便可以将一个委托作为参数来接受,并且以后可以调用该委托。这称为异步回调,是在较长的进程完成后用来通知调用方的常用方法。以这种方式使用委托时,使用委托的代码无需了解有关所用方法的实现方面的任何信息。

// example1
public delegate void Del ( string message );
public void DelegateMethod ( string message )
{ 
    System.Console. WriteLine(message); 
}
Del handler = new Del ( obj.DelegateMethod );
public void MethodWithCallback ( int param1,int param2,Del callback )
{
    callback("The number is:"+(param1+param2).ToString());
}
MethodWithCallback(1,2,handler);

// example2
delegate bool CompareOp(object a, object b);
class BubbleSorter
{
	static public void Sort(object[] sortArray, CompareOp co){ ..... }
    .....
}
class Employee {
    .....
    public static bool Compare(object e1, object e2){ .... }
    .....
}
CompareOp EmployeeCompareOp = new CompareOp(Employee.Compare);
BubbleSorter.Sort(employees, EmployeeCompareOp);

3.10.2 如何委托

  • 委托是面向对象的,是引用类型,对委托的使用要先定义实例化,才能调用
delegate  int  SomeDelegate(int nID, string  sName);
SomeDelegate  d1 = new  SomeDelegate(wr.InstanceMethod);
d1(5, ”aaa”);
  • 委托实例化的实参方法中的参数和返回类型必须和委托的定义一致
  • 委托实例化中的参数可以是非静态方法,也可以是静态方法
  • 可以在类的任何地方定义委托,既可以在一个类的内部定义,也可以在类的外部定义,还可以在命名空间中将委托定义为顶层对象
  • 可以给委托定义添加修饰符public、private和protected
  • 委托派生于System.Delegate,在语法上总是带有一个参数构造函数,这个参数就是委托引用的方法,这个方法必须匹配最初定义委托时的签名

3.10.3 委托与C++函数指针的区别

  • 委托可以指向多个方法
  • 当我们唤起一个搭载了多个方法的delegate,所有方法以其被搭载的顺序被依次唤起
  • 委托搭载的所有方法可以不属于同一个类,但必须具有相同的原型,方法可以是静态的或非静态的

3.11 多播

  • 一次委托可以调用多个方法
  • 通过+和-运算符可以实现多播的增加或减少
  • 委托由System.Delegate类派生而来,多播则派生于System.Delegate类的派生类System.MulticastDelegate
SomeDelegate   d3=d1+d2;
				↑
                ↓
SomeDelegate   d3=(SomeDelegate) Delegate.Combine(d1,d2);
				↑
                ↓
SomeDelegate   d3=(SomeDelegate)MulticastDelegate(d1,d2);

public abstract class MulticastDelegate : Delegate
    
public static Delegate Combine( Delegate a, Delegate b ) 

  • 将多个方法组合到一个委托中,如果方法的返回值非void,则通过委托调用方法仅得到最后一个方法的返回值,其它的返回值都将丢失
  • 将多个方法组合到一个委托中,会顺序调用委托中的每个方法

3.12 事件

  • 定义事件的委托
public delegate void EventHandler( object  from, MyEventArgs  e);
  • System.EventArgs是包含事件数据的类的基类
  • MyEventArgs类派生于EventArgs,实现自定义事件数据的功能
// 定义事件		event  事件的委托名  事件名;
public event EventHandler  TextOut;

// 激活事件
if (TextOut != null)
   TextOut(this, new  EventArgs());

// 订阅和取消事件
evsrc.TextOut += new EventSource.EventHandler(CatchEvent);
evsrc.TextOut -= new EventSource.EventHandler(CatchEvent);

  • 事件模型
public delegate void RoutedEventHandler( Object sender, RoutedEventArgs e );

public event RoutedEventHandler Click;

this.button1.Click += new System.EventHandler(this.button1_Click);

3.13 实例构造函数

[构造函数修饰符] 标识符([参数列表] )
[ : base ( [参数列表] ) ] [ : this ( [参数列表] ) ]
{
构造函数语句块
}

  • 构造函数修饰符有public、protected、internal、private
  • 构造函数可以重载
  • :base表示调用直接基类中的实例构造函数
  • :this调用该类本身所声明的其他构造函数
  • 构造函数语句块既可以对静态字段赋值,也可以对非静态字段进行初始化
  • 实例构造函数不能被继承
  • 如果一个类中没有声明任何实例构造函数,则系统会提供一个默认的实例构造函数
  • 一旦类中提供了自定义的构造函数,系统则不提供默认构造函数
  • 如果变量是方法的局部变量或者是方法的out参数,编译器就会认为在使用该变量前,代码必须给它显式地设置一个值
  • 对于其他情况(包括类的实例和静态成员字段),编译器会在创建变量时,把变量初始化为默认值

3.14 静态构造函数

[静态构造函数修饰符] 标识符()
{
静态构造函数体
}

  • 静态构造函数修饰符:static
  • 静态构造函数仅对静态数据成员进行初始化,不能对非静态数据成员进行初始化
  • 静态构造函数都是私有的
  • 静态构造函数是不可继承的,而且不能被直接调用
  • 如果类中没有声明静态构造函数,而又包含带有初始设定的静态字段,那么编译器会自动生成一个默认的静态构造函数

3.15 析构函数

​ ~标识符()
{
​ 析构函数体
}

  • 析构函数不能被重载,不能被继承

  • 一个类如果没有显式声明析构函数,则编译器将自动产生一个默认的析构函数

  • 析构函数不能由程序显式调用,由系统在释放对象时调用

  • 析构函数是在“垃圾回收器”回收对象的存储空间之前调用的,最终会调用System.Object的Finalize( )

  • 可使用System.GC.Collect()请求运行垃圾回收器,例如代码中有大量的对象刚刚停止使用

  • 析构函数在C#中充当鸡肋的角色,由于有垃圾收集器清理资源,所以不能预计它们会在什么时候运行

  • 什么情况下需要自己写析构函数?

    • 使用析构函数主要是为了释放资源。
    • 如:当该类中打开了一个文件,对象销毁时,应当关闭这个文件。
    • 又如:当该类中打开了数据库,对象销毁时,应当关闭这个连接。
    • 再如:当该类中申请了大量内存,对象销毁时,应当释放这些内存。
  • 调用析构函数的时机由.NET的垃圾回收器来决定

3.16 运算符重载

  • 可以重载的运算符
    一元操作符 +, -, !, ~, ++, --, true, false
    二元操作符 +, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, <=

  • 赋值运算符 +=, -=, *=, /=, >>=, <<=, %=, &=, |=, ^=
    要求同时重载的运算符
    一元操作符 true和false
    二元操作符 ==和!=, >和<, >=和<=

  • C#要求以静态方式声明运算符重载

  • 编译器不能改变二元运算符的参数顺序

  • C#不允许重载=运算符,如果重载+运算符, 编译器会自动使用+运算符的重载计算+=运算符的操作

  • 不能重载的运算符

&& ( ) . ?
?? [ ] || =
=> -> As Checked
default is new Sizeof
typeof unchecked **** ****

3.17 用户定义的数据类型转换

// 隐式类型转换
public   static  implicit  operator  Square(double s)
{
     ……
} 

// 显式类型转换
public   static  explicit  operator  double(Square s)
{
     ……
} 

这种数据类型转换在不同结构或类之间进行是允许的,但有两个限制:

  • 如果某个类直接或间接继承了另一个类,就不能在 两个类之间进行数据类型转换
  • 数据类型转换必须在源或目标数据类型定义的内部定义。一旦在一个类中定义数据类型转换,就不能在另一个类中定义相同的数据类型转换

3.18 方法的参数(参数数组params的使用)

  • 值参数

    • 编译器为值参数分配存储空间,然后将对应的实参的值拷贝到形参中
    • 对形参的改变不会影响到实参
  • 引用参数

    • 引用参数与实参变量共用一个存储单元
    • ref仅对其后的参数有效,不能应用于整个参数表
    • 调用方法时,也用ref修饰实参变量
    • 实参的类型与形参类型必须完全匹配
    • 实参必须是变量,不能是常量或表达式
    • ref参数在调用前必须明确赋值
  • 输出参数

    • 只能从方法中传出值,不能从方法调用处接收实参数据
    • 调用方法时,用out修饰实参变量
    • 和引用参数一样都能修改实参值,区别就是输出参数默认传递进来的是未赋值的,必须赋值了在return
  • 参数数组 params

    • 使用params关键字指定一个参数可变长的参数表
    • 一个方法中只能声明一个params参数,如果还有其他常规参数,则params参数应放在参数表的最后

4、WinForm程序设计

4.1 控件

4.1.1 Button

常用属性

  • Text

    按钮上显示的文本包含在Text属性中。如果文本超出按钮宽度,则换到下一行。可以包含访问键。

  • FlatStyle

    如果按钮样式设置为Popup,则按钮显示为平面,直到用户把鼠标指针移动到它上面为止。此时,按钮弹出,显示为正常的3D外观。

  • Enabled

    设置为false,按钮会灰显,单击它不起任何作用

  • Image
    可以指定在一个按钮上显示的图像(位图,图标等)

  • ImageAlign
    设置按钮的图像在什么地方设置

  • ImageList

  • AcceptButton
    在任何Windows窗体上都可以指定某个Button控件为接受按钮。每当用户按Enter键时,即单击默认按钮,而不管当前窗体上其他哪个控件具有焦点

  • CancelButton
    每当用户按ESC键时,即单击取消按钮,而不管当前窗体上其他哪个控件具有焦点

  • HelpButton
    获取或设置一个值,该值指示是否应在窗体的标题框中显示“帮助”按钮。

常用事件

  • Click
    该控件不支持双击事件
 private void button1_Click(object sender, EventArgs e)
 {
     selected="宝马";
 }

4.1.2 TextBox

用于获取用户输入或显示文本,只能对显示或输入的文本提供单个格式化样式

常用属性

  • CausesValidation
    该属性设置为true,且该控件获得了焦点时,会引发两个事件:validating和validated。可以处理这些事件,以便验证失去焦点的控件中数据的有效性。这可能使控件永远都不能获得焦点。
  • CharacterCasing 表示文本框是否会改变输入文本的大小写Lower、Normal、Upper
  • MaxLength 指定输入到文本框中的最大字符长度
  • Multiline 表示该控件是一个多行控件
  • PasswordChar 指定是否用密码字符替换在单行文本框中输入的字符
  • ReadOnly 表示文本是否为只读
  • ScrollBars 指定多行文本框是否显示滚动条
  • SelectedText 在文本框中显示的文本
  • SelectionLength 在文本中选择的字符数
  • SelectionStart 文本框中被选中文本的开头
  • WordWrap 指定在多行文本框中,如果一行的宽度超出了控件的宽度,其文本是否应自动换行
  • AcceptsReturn 获取或设置一个值,该值指示在多行 TextBox控件中按 Enter 键时,是在控件中创建一行新文本还是激活窗体的默认按钮。
  • AcceptsTab

常用方法

  • Clear
  • AppendText
  • Copy
  • Cut
  • Paste
  • Select
  • SelectAll

常用事件

  • 焦点事件
    • Enter
    • Leave
    • Validating
    • Validated
  • 键事件
  • KeyDown
  • KeyPress
  • KeyUp
  • TextChanged事件 文本框的内容发生变化时触发该事件

4.1.3 RadioButton

4.1.4 CheckBox

4.1.5 RichTextBox

4.1.6 ListBox

4.1.7 ComboBox

4.1.8 TreeView

4.1.9 ListView

4.1.10 PictureBox

4.1.11 菜单栏

4.1.12 工具栏

4.1.13 状态栏

4.1.14 OpenFileDialog

4.1.15 SaveFileDialog

4.1.16 FontDialog

4.1.17 ColorDialog

4.2 事件处理

5、流

流是串行化设备的抽象表示

  • 输出流

    向某些外部目标写入数据

  • 输入流

    用于将数据读到程序可以访问的内存或变量中

5.1 FileStream

  • 表示在磁盘或网络路径上指向文件的流,提供了在文件中读写字节的方法
  • 构造方法
    • public FileStream ( string path, FileMode mode )
    • public FileStream ( string path, FileMode mode, FileAccess access )
      • Read 用于只读
      • Write 用于只写
      • ReadWrite 用于读写
FileStream  fs=new FileStream(“D:\\1.txt”,FileMode.Open,FileAccess.Read,FileShare.None);
FileStream  fs=File.Open(“D:\\1.txt”,FileMode.Open,FileAccess.Read,FileShare.None);

FileStream  fs=new FileStream(“D:\\1.txt”,FileMode.OpenOrCreate,FileAccess.Write,FileShare.None);
FileStream  fs=File.OpenWrite(“D:\\1.txt”);

FileStream aFile = new FileStream("E:/程序/f2.java", FileMode.Open);
aFile.Seek(90, SeekOrigin.Begin);
aFile.Read(byData, 0, 200);

5.2 StreamWriter与StreamReader

5.2.1 StreamWriter的构造方法

public StreamWriter ( string path ) 
public StreamWriter ( string path, bool append )
//	append 为false,则创建一个新文件,或截取现有文件并打开它
//	append 为true,则打开文件,保留原有数据。如果找不到文件,则创建一个新文件
public StreamWriter ( Stream  stream ) 

5.2.2 StreamWriter的Write和WriteLine方法

//	将字符串写入流,参数除字符串外可以是任何基本数据类型
public override void Write ( string value )

//	将字符串写入流并换行,参数除字符串外可以是任何基本数据类型
public virtual void WriteLine ( string value )

5.2.3 StreamReader的构造方法

public StreamReader ( Stream  stream )  
public StreamReader ( string path ) 

5.2.4 StreamReader的Read、ReadLine、ReadToEnd方法

// 从当前流中读取一行字符,并将数据作为字符串返回
public override string ReadLine () 
// 从index开始,将当前流中的最多count个字符读入buffer中
int  Read ( char[] buffer, int  index, int  count ) 
// 从流的当前位置到末尾读取流
string  ReadToEnd()

5.3 BinaryReader和BinaryWriter

  • BinaryReader
//	常用函数
public virtual XXX readXXX( );
//	对各种基本数据类型的读取
public virtual int Read (); 
public virtual int Read ( byte[] buffer, int index, int count ); 
public virtual int Read ( char[] buffer, int index, int count ); 
//	构造函数
public BinaryReader ( Stream input );
public BinaryReader( Stream input, Encoding encoding );
  • BinaryWriter
//	常用函数
public virtual void Write(XXX  xx);
public virtual void Write ( byte[] buffer, int  index, int count );
public virtual void Write ( char[] chars, int index, int count ); 
//	构造函数
protected BinaryWriter (); 
public BinaryWriter ( Stream output ); 
public BinaryWriter ( Stream output, Encoding encoding ); 

5.4 对象的串行化

  • .NET Framework在System.Runtime.Serialization和System.Runtime.Serialization.Formatters命名空间中提供了串行化对象的基础架构
  • System.Runtime.Serialization.Formatters.Binary在该命名空间中包含的BinaryFormatter类能把对象串行化为二进制数据,把二进制数据串行化为对象
  • System.Runtime.Serialization.Formatters.Soap在该命名空间中包含的SoupFormatter类能把对象串行化为SOAP格式的XML数据,把SOAP格式的XML数据串行化为对象

5.5 监控文件结构FileSystemWatcher

  • 使用FileSystemWatcher的基本过程

    • 首先设置一些属性,指定监控的位置、内容以及引发应用程序要处理的事件的时间

    • 然后给FileSystemWatcher提供定制事件处理程序的地址,当发生重要事件时,FileSystemWatcher就调用这些属性

    • 打开FileSystemWatcher,等待事件

    • 启用FileSystemWatcher对象之前必须设置的属性

      • Path 设置要监控的文件位置或目录

      • NotifyFilter NotifyFilters枚举值规定在被监控的文件内要监控哪些内容

        可能的枚举值是Attributes、CreateTime、DirectoryName、FileName
        LastAccess、LastWrite、Security、Size

      • Filter 监控文件的过滤器

    • 必须为Changed、Created、Deleted和Renamed编写事件处理程序。当修改与Path、NotifyFilter和Filter属性匹配的文件或目录时,就引发每一个事件

    • 将EnableRaisingEvents属性设置为true,就可以开始监控工作

6、多线程技术

6.1、 线程的建立与启动

  • 假定需要编写一个文件压缩软件,用户点击压缩按钮后开始压缩指定的文件。因为整个压缩过程需要一定的时间才能完成,而用户此时还可能需要移动或缩放程序的窗口,甚至暂停或终止当前文件的压缩。因此需要一个单独线程来处理压缩过程
  • 在C#应用程序中,Main()方法所在线程是.NET运行库开始执行的第一个线程,称为主线程
  • 在一个应用程序中创建的用于执行一些工作任务的线程称为工作线程
  • Thread的构造方法需要一个参数,用于指定线程的入口—即线程开始执行的方法
  • 线程创建后,并未获得系统资源。启动线程,即给线程分配除处理器之外的系统资源并执行各种安全性检查。
Thread  compressThread = new  Thread(entryPoint);
compressThread.Start( );

public delegate void ParameterizedThreadStart ( Object obj ) 
public delegate void ThreadStart( );
static void DoCompress( )
{
   //压缩代码
}
ThreadStart  entryPoint = new  ThreadStart( DoCompress );

6.2、 线程的常用方法

  • public void start()
    • 在调用该方法后,新线程并不是处于Running状态,而是处于Unstarted状态
  • public void Join ()
    • 当前线程进入阻塞状态,等待调用该方法的线程执行完毕
  • public static void Sleep ( int millisecondsTimeout )
    • 使当前线程睡眠指定的时间,休眠完后线程变为就绪状态

6.3、 Thread类的常用属性

  • CurrentThread
    • 获取当前正在运行的线程
    • Thread myOwnThread = Thread.CurrentThread;
  • Name
    • 线程的名称
  • Priority 指示线程的调度优先级
    • ThreadPriority.Highest 将线程安排在具有任何其他优先级的线程之前
    • ThreadPriority.AboveNormal 将线程安排在具有Highest优先级的线程之后,在具有Normal优先级的线程之前。
    • ThreadPriority.Normal 将线程安排AboveNormal线程之后,BelowNormal线程之前。为线程默认优先级。
    • ThreadPriority.BelowNormal 将线程安排在具有Normal优先级的线程之后,在具有Lowest优先级的线程之前
    • ThreadPriority.Lowest 将线程安排在具有任何其他优先级的线程之后

6.4、 给线程传递数据

  • 使用带ParameterizedThreadStart委托参数的Thread构造函数
  • 创建一个定制类,把线程的方法定义为实例方法,这样就可以初始化实例的数据,然后启动线程
//	给线程传递数据的第一种方式
var d = new Data { Message = "Info" };
var t2 = new Thread(ThreadMainWithParameters);
t2.Start(d);

//	给线程传递数据的第二种方式
var obj = new MyThread("info");
var t3 = new Thread(obj.ThreadMain);
t3.Start();

6.5、后台线程

  • 前台与后台线程
    • 只要有一个前台线程在运行,应用程序的进程就在运行
    • 在默认情况下,用Thread类创建的线程是前台线程,线程池中的线程总是后台线程
    • 通过设置线程的属性IsBackground可以确定该线程是前台线程还是后台线程
    • 后台线程非常适合完成后台任务。例如,如果关闭word应用程序,拼写检查器继续运行其进程就没有意义了

6.6、线程池ThreadPool

  • 该类会在需要时增减池中线程的个数,直到最大的线程数。
  • 池中的最大线程数是可配置的
  • 如果有更多的工作要处理,而线程池中线程的使用也到了极限,最新的工作就要排队,必须等待线程完成其任务
  • 线程池中的每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中
  • 如果线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间之后创建另一个辅助线程
  • 线程池适合于执行一些需要多个线程的任务。使用线程池可以优化任务的执行过程,提高吞吐量;如果应用程序需要对线程进行特定的控制,则不适合使用线程池,需要创建并管理自己的线程
  • 每个进程都有且仅有一个线程池。当进程启动时,线程池不会自动创建。只有第一次将回调方法排入队列时才会创建线程池。
  • 线程池通常用于服务器应用程序,每个传入请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理

线程池常用方法:

  • void GetMaxThreads(out int workerThreads,out int completionPortThreads)

    检索可以同时处于活动状态的线程池请求的数目,所有大于此数目的请求将保持排队状态,直到线程池线程变为可用

  • bool QueueUserWorkItem ( WaitCallback callback )
    public delegate void WaitCallback(Object state)

    将方法排入队列以便执行,此方法在有线程池线程变得可用时执行 。线程池收到请求后会从池中选择一个线程来调用该方法。如果线程池还没有运行,就会创建一个线程池,并启动第一个线程。如果线程池已经在运行,且有一个空闲线程来完成该任务就把该作业传递给这个线程

  • bool SetMinThreads(int workerThreads, int completionPortThreads)

    设置最小并发数

  • bool SetMaxThreads(int workerThreads, int completionPortThreads)

    设置最大并发数

使用线程池的限制

  • 线程池中所有线程都是后台线程。如果进程中的所有前台线程都结束了,所有的后台线程就会停止。不能把线程池中的线程改为前台线程。
  • 不能给线程池中的线程设置优先级或名称
  • 线程池中的线程只能用于时间较短的任务,如果线程要一直运行(如word的拼写检查器线程),就应使用Thread类创建一个线程

6.7、线程同步

  • 所谓同步,是指在某一时刻只有一个线程可以访问变量
  • 当一个线程写入一个变量,同时有其他线程读取或写入这个变量时,就应同步变量
  • 高级语言程序中的一条语句在最后编译好的汇编语言机器码中可能被翻译为多条语句,在操作系统调度时被划分到不同时间片中
  • 只要一条C#语句被翻译为多个本地代码命令,线程的时间片就有可能在执行该语句的进程中终止
    • 读“垃圾”数据
    • 不正确写入
  • 在C#中处理同步
    • 通过对指定对象的加锁和解锁可以同步代码段的访问
    • System.Threading.Monitor类提供的静态方法
      • public static void Enter ( Object obj ) 在指定对象上获取排它锁
      • public static void Exit ( Object obj ) 释放指定对象上的排它锁
      • public static bool Wait ( Object obj ) 释放对象上的锁并阻塞当前线程,直到它重新获取该锁。
      • public static void Pulse ( Object obj ) 通知等待队列中的线程锁定对象状态的更改。
      • public static void PulseAll ( Object obj ) 通知所有的等待线程对象状态的更改。

线程同步的几种方法:

  • 使用Lock(锁定对象) { 互斥代码块 } 实现互斥
  • 设Mutex类对象为mut,在互斥代码块前使用mut.WaitOne( ),在互斥代码块后使用mut.ReleaseMutex( )
  • 在互斥代码块前使用Monitor.Enter(锁定对象),在互斥代码块后使用Monitor.Exit(锁定对象)
public void some_method ( ) 
{
    //获取锁
    Monitor.Enter (this);
    
    //处理需要同步的代码
    
    //释放锁
    Monitor.Exit (this);
}
  • 使用Interlocked
//Interlocked类的常用方法:

//以原子操作的形式,相加两个64位整数并用两者的和替换第一个整数
long  Add ( ref  long  location1, long  value );

//以原子操作的形式,递增指定的变量的值并存储结果
long  Increment( ref  long  location );

//以原子操作的形式,递减指定的变量的值并存储结果 
long  Decrement ( ref  long  location );

//以原子操作的形式,将一个变量设置为指定的值并返回变量的初始值
long  Exchange ( ref  long  location1,  long  value );
  • 等待句柄

    • WaitHandle是一个抽象基类

    • WaitHandle类定义的执行等待的方法

    • WaitOne

      bool WaitOne (int millisecondsTimeout, bool exitContext)

      是一个实例方法,利用它可以等待一个信号的发生。也可以为最大等待时间指定一个超时值

      exitContext是true,则等待之前先退出上下文的同步域,然后稍后重新获取它,否则为false。

      public static bool WaitAll(WaitHandle[] waitHandles)

      是一个静态方法,用于传送WaitHandle对象的数组,并等待所有的句柄发出信号

  • 使用Semphore 见程序SemphoreSample.见程序示例SynchronizationSamples

  • ReaderWriterLockSlim 见程序ReaderWriterSample

6.8、任务Task

  • 任务在后台使用ThreadPool
  • 在安排需要完成的工作时,任务提供了非常大的灵活性。
    • 可以定义连续的工作,在一个任务完成后需要执行什么工作
    • 可以在层次结构中安排任务,例如父任务可以创建新的子任务,这可以创建一种依赖关系
  • 启动任务
    • 可以使用TaskFactory类或Task类的构造函数和Start()方法
static void TaskMethod()
{
    Console.WriteLine("running in a task");
    Console.WriteLine("Task id: {0} {1}", Task.CurrentId, 
                      Thread.CurrentThread.ManagedThreadId);
}
// using task factory
TaskFactory tf = new TaskFactory();
Task t1 = tf.StartNew(TaskMethod);

// using the task factory via a task
Task t2 = Task.Factory.StartNew(TaskMethod);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

// using Task constructor
Task t3 = new Task(TaskMethod);
// t3.Start();
t3.RunSynchronously();//任务也会启动,但是在调用者的当前线程中运行,调用者需要一直等待该任务结束

Task t4 = new Task(TaskMethod, TaskCreationOptions.PreferFairness);
t4.Start();

  • TaskCreationOptions选项

    设置LongRunning选项,可以通知任务调度器,该任务需要较长时间运行,这样调度器更可能使用新线程
    如果该任务关联到父任务上,而父任务取消了,该任务也要取消,此时应设置AttachToParent选项
    如果任务应以公平的方式与所有其他任务一起处理,应设置该选项为PreferFairness

6.9、死锁的产生和避免

  • 死锁的产生:两个线程需要访问互锁的资源
lock (a)
{
    lock (b)
    {
        // do  something
    }
}

lock (b)
{
    lock (a)
    {
        // do  something
    }
}
  • 避免死锁:让两个线程以相同的顺序在对象上声明加锁
lock (a)
{
   lock (b)
   {
      // do  something
    }
}

lock (a)
{
   lock (b)
   {
      // do  something
    }
}

6.10、异步委托

委托使用线程池完成异步任务

  • 轮询

  • 等待句柄

    • 使用与IAsyncResult相关的等待句柄
    • 使用AsyncWaitHandle属性可以访问等待句柄,这个属性返回一个WaitHandle类型的对象,可以等待委托线程完成任务
  • 异步回调

6.11、屏障Barrier

static void Main(string[] args)
{
    Barrier barrier = new Barrier(4, it => {
        Console.WriteLine("再次集结,友谊万岁,再次开跑");
    });
    string[] names = { "张三", "李四", "王五", "赵六" };
    Random random = new Random();
    foreach(string name in names)
    {
        Task.Run(() => {
            Console.WriteLine($"{name}开始跑");
            int t = random.Next(1, 10);
            Thread.Sleep(t * 1000);
            Console.WriteLine($"{name}用时{t}秒,跑到友谊集结点");
            barrier.SignalAndWait();
            Console.WriteLine($"友谊万岁,{name}重新开始跑");
        });
    }
    Console.ReadKey();
}

7、数据库应用开发

7.1、 ADO.NET的主要特点

  • 不依赖于连续的活动连接
  • 使用数据命令执行数据库交互
  • 使用数据集缓存数据
  • 数据集独立于数据源
  • 数据保持为XML
  • 通过架构定义数据结构

7.2、 ADO.NET对象模型的结构

image-20211226121212229

  • Connection对象表示与一个数据源的物理连接
  • DataAdapter是Connection与数据集之间的桥梁
  • DataAdapter管理了4个Command对象来处理后端数据集和数据源的通信,即SelectCommand、UpdateCommand、InsertCommand、DeleteCommand
  • DataReader对象用于从数据源中获取仅前向、只读的数据流
  • Dataset
    • 数据集中的数据可能来自多个数据源
    • ADO.NET支持外键约束和唯一键约束

7.3、 连接对象的创建

System.Data.OleDb.OleDbConnection  conn=
    new System.Data.OleDb.OleDbConnection(“Provider=Microsoft.Jet.OLEDB.4.0; User ID=Admin;
  Data  Source=c:\\data.mdb; ”); 


System.Data.OleDb.OleDbConnection  conn = new System.Data.OleDb.OleDbConnection( );
conn.ConnectionString="Provider=Microsoft.Jet.OLEDB.4.0; User ID=Admin; Data  Source=c:\\data.mdb"; 

//打开和关闭连接
try
{
    conn.Open( );
    ……
}
catch  ( Exception  ex )
{
    ……
}
finally
{
    conn.Close( );
}

7.4、 SqlCommand执行数据命令的方法

string ss="Data Source=.\\MYSQLSERVER;Initial Catalog=MyDB;Integrated Security=True";
SqlConnection conn = new SqlConnection(ss);
con.Open();

7.4.1 ExecuteReader

//使用SqlCommand和SqlDataReader读表的内容
SqlCommand cmd = new SqlCommand("select * from Student", conn);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
    Console.WriteLine(reader[0] + "  " + reader[1] + "  " + reader[2] + "  " + reader[3]);
reader.Close();
con.Close();

7.4.2 ExecuteNonQuery

SqlCommand cmd = new SqlCommand("update Student set 年龄=28 where 姓名='李明'", con);
int n = cmd.ExecuteNonQuery();
Console.WriteLine("{0}条记录已更新", n);
con.Close();

7.4.3 ExecuteScalar

SqlCommand cmd = new SqlCommand("select count(*) from Student", con);
int cnt = Convert.ToInt32(cmd.ExecuteScalar());
Console.WriteLine("学生人数:" + cnt);

7.5、 DataSet

类似于一个在客户机内存中的数据库,可以在这个数据库中增删表、定义表结构和关系、增删行

DataSet不考虑其中的表结构和数据是来自数据库、XML文件还是程序代码

7.5.1 Tables

数据集中数据表的集合,是一个DataTableCollection集合类

7.5.2 Relations

数据集中数据关系的集合,是一个DataRelationCollection集合类

7.6、 DataTable

  • DataTable的构造方法
    • public DataTable ()
    • public DataTable ( string tableName )
  • DataTable的属性
    • Columns 属于该表的列的集合
    • Rows 该表的行的集合
    • PrimaryKey 表主键的列数组

7.7、 SqlDataAdapter

//	查
SqlDataAdapter sda = new SqlDataAdapter("select * from Student", con);
DataSet ds = new DataSet();
sda.Fill(ds, "student");
foreach (DataRow dr in ds.Tables["student"].Rows)
{
    Console.WriteLine(dr[0] + " " + dr[1] + " " + dr[2]+"  "+dr[3]);
} 

//	改
dr[1] = 23;
sda.Update(ds,"student");

//	增
SqlDataAdapter sda = new SqlDataAdapter("select * from Student", con);
DataSet ds = new DataSet();
sda.Fill(ds, "student");
DataRow dr = ds.Tables["student"].NewRow();
dr["姓名"] = "李欣";
dr["年龄"] = 23;
dr["性别"] = "女";
dr["职业"] = "研究生";
ds.Tables["student"].Rows.Add(dr);
sda.Update(ds, "student");
con.Close(); 

//	删
DataRow dr = ds.Tables["student"].Rows.Find("李强");
if (dr != null)
{
    //Console.WriteLine(dr[0] + " " + dr[1] + " " + dr[2]);
    // ds.Tables["student"].Rows.Remove(dr);
    dr.Delete();

    Console.WriteLine(dr.RowState);
    sda.Update(ds, "student");
}

7.8、DataRowState和AcceptChanges、Update方法

SqlDataAdapter sda = new SqlDataAdapter("select * from Student", con);
DataSet ds = new DataSet();
sda.Fill(ds, "student");

//sda.Update(ds, "student");
// 更新数据源可以二选一
//ds.Tables["student"].AcceptChanges();

7.9、在ADO.NET中访问多个表

  • DataRelation

    DataRelation对象用于描述DataSet中的多个DataTables对象之间的关系

DataRelation   custOrderRel=thisDataSet.Relations.
Add(“CustOrders”,thisDataSet.Tables[“Customers”].
Columns[“CustomerID”],thisDataSet.Tables[“Orders”].
Columns[“CustomerID”]);

7.10、DataColumnVersion

7.11、UpdatedRowSource的含义和示例

  • UpdateRowSource值
    • Both 存储过程可以返回输出参数和一个完整的数据库记录。这两个数据源都用于更新源数据行
    • FirstReturnedRecord 该命令返回一个记录,该记录的内容应合并到最初的源DataRow中,当给定的表有许多默认(或计算)列时,使用这个值很有用,因为在执行insert语句之后,这些行需要与客户端上的DataRow同步。
    • None 丢弃从该命令返回的所有数据
    • OutputParameters 命令的任何输出参数都映射到DataRow的对应列上

8、泛型

8.1 .net 泛型与C++模板的不同

  • 对于C++模板,用特定的类型实例化模板时,是需要模板的源代码的
  • 泛型不仅是C#语言的一种结构,而且是CLR定义的
  • 即使泛型类是在C#中定义的,也可以在Visual Basic中用一个特定的类型实例化该泛型
  • 在C++中,编译器可以检测出在哪里使用了模板的某个特定类型,例如,模板B的A类型,然后编译需要的代码,来创建这个类型。而在C#中,所有操作都在运行期间进行

8.2 泛型在性能、类型安全、二进制代码的重用、代码的扩展方面的表现

  • 性能
var list = new ArrayList(); //类型推理,自动类型
list.Add(44); //装箱  int对应的封箱类型Integer
int  i1 = (int) list[0]; //拆箱 object
foreach (int i2 in list)
  Console.WriteLine(i2);  //拆箱
// 频繁的装拆箱对性能损失较大,遍历许多项时尤其如此
var list = new List<int>();  
list.Add(44); 
int  i1 = list[0]; 
foreach (int i2 in list)
  Console.WriteLine(i2); 
//List<T>的泛型类型定义为int,所以int类型在JIT编译器动态生成的类中使用,不再进行装箱和拆箱操作
  • 类型安全
var list = new ArrayList();
list.Add(44); //装箱
list.Add("mystring"); //string   String
list.Add(new MyClass());
// 以上代码在编译期间就会出现错误

for  (int i in list)
  Console.WriteLine(i);
/*
    可以往集合中添加任意类型,在迭代时并不是所有类型
    都可以转换成int,所以触发运行异常
*/
  • 二进制代码的重用

泛型类可以定义一次,并且可以用许多不同的类型实例化,而不需要像C++模板那样访问源代码

var list = new List<int>();
list.Add(44); 
var stringList = new List<string>();
stringList.Add(“mystring”);
var myClassList = new List<MyClass>();
myClassList.Add(new MyClass());
  • 代码的扩展

因为泛型类的定义放在程序集中,所以用特定类型实例化泛型类不会在IL代码中复制这些类。但是在JIT编译器把泛型类编译为本地代码时,会给每个值类型创建一个新类

引用类型共享同一个本地类的所有相同的实现代码

因为引用类型在实例化的泛型类中只需要4个字节的内存地址,就可以引用一个引用类型

值类型包含在实例化的泛型类的内存中,同时因为每个值类型对内存的要求都不同,所以要为每个值类型实例化一个新类

8.3 泛型类型的命名约定

  • 泛型类型的命名规则
//	泛型类型的名称用字母T作为前缀
//	如果没有特殊的要求,泛型类型允许用任意类替代,且只使用了一个泛型类型,就可以使用T作为泛型类型的名称
public  class  List<T>  {   }
public  class  LinkedList<T>  {   }

//	如果泛型类型有特定的要求(例如它必须实现一个接口或派生自基类),
//	或者使用了两个或多个泛型类型,就应给泛型类型使用描述性的名称
public  delegate  void  EventHandler<TEventArgs>(object sender, TEventArgs e);
public  delegate  TOutput  Converter<TInput,TOutput>(TInput  from);
public  class  SortedList<TKey,TValue>  {  }

8.4 创建泛型类

public class LinkedList<T> : IEnumerable<T>
{
    public LinkedListNode<T> First { get; private set; }
    public LinkedListNode<T> Last { get; private set; }

    public LinkedListNode<T> AddLast(T node)
    {
        var newNode = new LinkedListNode<T>(node);
        if (First == null)
        {
            First = newNode;
            Last = First;
        }
        else
        {
            newNode.Prev = Last;
            Last.Next = newNode;
            Last = newNode;
        }
        return newNode;
    }

    public IEnumerator<T> GetEnumerator()
    {
        LinkedListNode<T> current = First;

        while (current != null)
        {
            yield return current.Value;
            current = current.Next;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

8.5 泛型类的功能

泛型文档管理器示例。文档管理器用于从队列中读写文档

using System;
using System.Collections.Generic;
namespace Wrox.ProCSharp.Generics
{
    public class DocumentManager<T>        
    {
        private readonly Queue<T> documentQueue = new Queue<T>();
        
		public void AddDocument(T doc)
        {
            lock (this)
            {
                documentQueue.Enqueue(doc);
            }
        }
		public bool IsDocumentAvailable
        {
            get { return documentQueue.Count > 0; }
        }
    }
}

8.6 泛型类型使用时需注意的地方

// 	不能假定类提供了什么类型
//	我们不知道T1是什么,也就不能使用它的构造函数,它甚至
//	可能没有构造函数,或者没有可公共访问的默认构造函数
Class  MyGenericClass<T1,T2,T3>
{
     private  T1  innerT1Object;
     public  MyGenericClass(T1  item)
     {  
         //innerT1Object=item;  
         innerT1Object = new T1();  //编译错误
     }
     public   T1  InnerT1Object
     {
         get
         {
             return  innerT1Object; 
         }
      }
}

//	当比较为泛型类型提供的类型值和null时
//	只能使用运算符==和!=
public  bool  Compare(T1  op1, T1  op2)
{
    if  (op1 != null && op2 != null)
        return  true;
    else
        return  false;
    
    if  (op1 == op2)   //编译错误
        return  true;
    else
        return  false;
}

8.7 默认值

  • 不能将null赋予泛型类型,原因是泛型类型可以实例化为值类型,而null只能用于引用类型
  • 为了解决这个问题,可以使用default关键字,将null赋予引用类型,0赋予值类型
  • 在泛型中,default关键字根据泛型类型是值类型还是引用类型,default用于将泛型类型初始化为null或0

8.8 约束

//	如果没有使用约束,代码如下所示
public void DisplayAllDocuments()
{
    foreach (T doc in documentQueue)
    {
        Console.WriteLine(((IDocument)doc).Title);
    }
}
/*
	问题是如果类型T没有实现IDocument接口,这个
	类型强制转换就会导致一个运行异常
*/

/*
	最好给DocumentManager<T>添加一个约束,T必须实现IDocument接口,同时将T改为TDocument	
*/
class  MyGenericClass<T>  where  T: constraint1,constraint2
{
     ……
}
//	约束必须出现在继承说明符的后面
class  MyGenericClass<T1,T2>: MyBaseClass, IMyInterface
where T1:constraint1  where  T2: constraint2
{
     ……
}

8.9 继承

  • 泛型类型可以实现泛型接口,也可以派生自一个类
  • 泛型类可以派生自泛型基类
public  class  LinkedList<T> : IEnumerable<out T>
{
    ……
}
public  class  Base<T>
{
    ……
}
public  class  Derived<T>: Base<T>
{
    ……
}

// 其要求是必须重复接口的泛型类型,或者必须指定基类的类型
public  class  Base<T>
{
    ……
}
public  class  Derived<T>: Base<string>
{
    ……
}

//	派生类可以是泛型类或非泛型类
public  abstract  class  Calc<T>
{
    public  abstract  T  add(T x, T  y);
    public  abstract  T  sub(T x, T  y);
}
public  class  IntCalc: Calc<int>
{
    public  override  int  Add(int  x, int y)
    {   return  x+y;  }
    public  override  int  Sub(int  x, int  y)
    {  return  x-y;  }
}

8.10 静态成员

//	泛型类的静态成员只能在类的一个实例中共享
public    class   StaticDemo<T>

    public  static  int  x;
}

StaticDemo<string>.x=4;
StaticDemo<int>.x=5;
Console.WriteLine(StaticDemo<string>.x); //输出4

8.11 泛型运算符

public static implicit operator List<Animal>(Farm<T> farm)
{
    List<Animal> result = new List<Animal>();
    foreach (T animal in farm)
    {
        result.Add(animal);
    }
    return result;
}
public static Farm<T> operator+(Farm<T> farm1, List<T> farm2)
{
    Farm<T> result = new Farm<T>();
    foreach (T animal in farm1)
        result.Animals.Add(animal);
    foreach (T animal in farm2)
    {
        if (!result.Animals.Contains(animal))
            result.Animals.Add(animal);
    }
    return result;
}

public static Farm<T> operator +(List<T> farm1, Farm<T> farm2)
{
    return farm2 + farm1;
}

8.12 泛型接口的协变与抗变

  • 协变和抗变指对参数和返回值的类型进行转换

  • .NET中,参数类型是协变的。假定有Shape和Rectangle类,Rectangle派生自Shape

public  void  Display(Shape o)  {   }
  • 现在可以传递派生自Shape基类的任意对象
Rectangle  r=new Rectangle { Width=5, Height=2.5 }
Display(r);
  • 方法的返回类型是抗变的。当方法返回一个Shape时,不能把它赋予Rectangle,因为Shape不一定总是Rectangle。反过来可行。
public  Rectangle  GetRectangle() {    …… }
Shape   s=GetRectangle();
  • 在C# 4中,扩展后的语言支持泛型接口和泛型委托的协变和抗变

  • 多态性不适用于接口

  • 如果泛型类型用out关键字标注,泛型接口就是协变的

  • 对于接口定义,协变类型参数只能用作方法的返回值或属性get访问器

  • 如果泛型类型用in关键字标注,泛型接口就是抗变的。这样接口只能把泛型类型T用作其方法的输入,不能用作返回类型

public  interface  IIndex<out T>
{
    T   this [int  index]   {  get;  }
    int   Count   {  get;  }         
}

public  interface  IDisplay<in T>
{
    void  Show (T  item);
}

8.13 可空类

类的对象可以为空,所以对类使用Nullable类型是没有意义的

因为可空类型使用得非常频繁,C#有一种特殊的语法

Nullable<int>  x1;
x1=null;  等价于  x1=new Nullable<int>();
int?  x2;

可空类型可以与null和数字比较

int? x3 = null;
if (x3 == null)
    Console.WriteLine("x is null");
else if (x3 < 0)
    Console.WriteLine("x is smaller than 0");

下面的代码不会被编译:

int?  op1 = 5;
int   result = op1*2;

为了使上面的代码正常工作,必须进行显式转换:

int?  op1 = 5;
int   result = (int)op1*2;

或者通过Value属性访问值:如果op1为null,会引发异常

int?  op1 = 5;
int   result = op1.Value*2;

8.14 泛型方法

void  Swap<T>(ref  T  x,  ref  T  y)
{
    T  temp;
    temp=x;
    x=y;
    y=temp;
}
int   i=4, j=5;
Swap<int>(ref  i, ref  j);

8.15 泛型方法的规范

  • 泛型方法可以重载,为特定的类型定义规范
public class MethodOverloads  {
    public void Foo<T>(T obj)   {
        Console.WriteLine("Foo<T>(T obj), obj type: {0}", obj.GetType().Name);
    }
    public void Foo(int x)  {
        Console.WriteLine("Foo(int x)");
    }
    public void Bar<T>(T obj)  {
        Foo(obj);
    }
}
  • 在编译期间,会使用最佳匹配。如果传递了一个int,就选择带int参数的方法;对于其他参数类型,编译器会选择方法的泛型版本
  • 所调用的方法是在编译期间定义的,而不是运行期间
  • Bar()方法选择了泛型Foo()方法,而不是用int参数重载的Foo()方法,原因是编译器在编译期间选择Bar()方法调用的Foo()方法。
  • 由于Bar()方法定义了一个泛型参数,而且泛型Foo()方法匹配这个类型,所以调用了Foo()方法,在运行期间给Bar()传递一个int值不会改变这一点

9、C#语言的改进

9.1 自动属性

9.2 对象初始化器

9.3 类型推理

9.4 Lambda表达式与匿名方法

  • 匿名方法

    匿名方法纯粹是为了委托目的而创建的

    delegate (parameters)
    {
    ……
    };

  • Lambda表达式的组成

    • 放在括号中的参数列表
    • =>运算符
    • C#语句或表达式
  • 编译器会提取这个Lambda表达式,创建一个匿名方法,Lambda表达式和匿名方法会被编译为相同或相似的CIL代码

  • Lambda表达式的参数

    • 隐式参数

      (a,b)=>a+b

    • 显式参数

      (int a,int b)=>a+b

  • Lambda表达式的语句体

PerformOperations((a,b)=>a+b); //可改写为

PerformOperations(delegate(a,b)=>{
                      return a+b;
                  });

PerformOperations((a,b)=>{
                      return a+b;
                  }); 

posted @   Doooong  阅读(354)  评论(8编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示