本博客所总结书籍为《CLR via C#(第4版)》清华大学出版社,2021年11月第11次印刷(如果是旧版书籍或者pdf可能会出现书页对不上的情况) 你可以理解为本博客为该书的精简子集,给正在学习中的人提供一个“glance”,以及对于部分专业术语或知识点给出解释/博客链接。 【本博客有如下定义“Px x”,第一个代表书中的页数,第二个代表大致内容从本页第几段开始。(如果有last+x代表倒数第几段,last代表最后一段)】 电子书可以在博客首页的文档-资源归档中找到,或者点击:传送门 自行查找。如有能力请支持正版。(很推荐放在竖屏上阅读本电子书,这多是一件美事) 欢迎加群学习交流:637959304 进群密码:(CSGO的拆包密码)
第十章 属性
无参属性
封装对类型中的数据字段的访问,好处(P202 1) 1、可以让访问字段来执行一些额外作用(side effect)、缓存某些值或者推迟创建一些内部对象。 2、可以以线程安全的方式访问字段。 3、字段可能是一个逻辑字段,它的值不由内存中的字节表示,而是通过某个算法来计算获得 使用get,set封装数据(P203 1),下述的get访问器方法不接受参数
public sealed class Employee
{
private string m_Name;
private int m_age;
public string Name
{
get{return (m_Name);}
set{m_Name = value;}//关键字value代表新的值
}
}
Employee e;
e.Name = "abc";
string a = e.Name;
自动实现的属性(Automatically Implemented Property,AIP):public string Name{get;set;} 使用AIP即创建了一个属性,访问该属性的任何代码实际都会调用get和set方法。 AIP有如下问题:(P205 2) 1、字段声明语法可能包含初始化部分,所以要在一行代码中声明并初始化字段。但没有简单的语法初始化AIP。所以,必须在每个构造器方法中显式初始化每个AIP。 2、运行时序列化引擎将字段名持久存储到序列化的流中。AIP的支持字段名称由编译器决定,每次重新编译代码都可能更改这个名称。因此,任何类型只要含有一个AIP,就没办法对该类型的实例进行反序列化。在任何想要序列化或反序列化的类型中,都不要使用AIP功能。 3、调试时不能在AIP的get或set方法上添加断点,所以不好检测应用程序在什么时候获取或设置这个属性。相反,手动实现的属性可设置断点,查错更方便。 对象和结合初始化器:即可以在定义时候就使用{}对其公共成员复制,并支持完成一些额外操作。(P208 2)
//如果属性的类型实现了IEnumerable或 IEnumerable<T>接口,属性就被认为是集合,而集合的初始化是一种相加(additive)操作,而非替换(replacement)操作。示例:
className c = new className{stringA = "aaa","bbb","ccc"};
匿名类型:用简洁的语法来自动声明不可变的元组类型。
//方法1
var o1 = new {Name = "abc",year = 7777};
//方法2
string name = "abc";
DateTime dt = DateTime.Now;
var o2 = new{name,dt.year};
System.Tuple类型:从Object派生,也是匿名类型。示例:(P212 last)
有参属性
C#称有参数的get访问器为索引器(P214 1) C#使用数组风格的语法来公开有参属性(索引器)。换句话说,可将索引器看成是C#开发人员对[]操作符的重载。(P214 2)
public sealed class BitArray
{
//容纳了二进制位的私有字节数组
private Byte [] m_byteArray;
private Int32 m_numBits;
//下面的构造器用于分配字节数组,并将所有位设为0
public BitArray ( Int32 numBits)
{
//先验证实参
if(numBits <=0)
throw new ArgumentOutOfRangeException ("numBits must be > 0");
//保存位的个数
m_numBits = numBits;
//为位数组分配字节
m_byteArray = new Byte [ (numBits + 7)/ 8 ];
}
//下面是索引器(有参属性)
public Boolean this [ Int32 bitPos]
{
//下面是索引器的get访问器方法
get
{
//先验证实参
if( (bitPos < 0)ll(bitPos >= m_numBits))
throw new ArgunentOutOfRangeException ( "bitPos");
//返回指定索引处的位的状态
return (n_bytearray [bitPos / 8 ] 6 (1 <<(bitPos % 8) ) ) != 0;
}
//下面是索引器的set访问器方法
set
{
if ( (bitPos < 0)ll(bitPos >= m_numBits))
throw new ArgumentoutOfRangeException ( "bitPos",bitPos.Tostring ( ) );
if (value)
{
//将指定索引处的位设为true
m_byteArray [bitPos i / 8]=(Byte)(m_byteArray [bitPos / 8]l (l <<(bitPos $ 8) ) );
}
else
{
//将指定索引处的位设为false
m_byteArray [bitPos / 8 ]=(Byte)(m_byteArray [bitPos / 8 ] 6~(1 <<(bitPos % 8) ) );
}
}
}
}
//使用:
BitArray ba = new BitArray(14);
ba[1] = 2;
调用属性刚问器方法时的性能
简单的get,set,JIT编译器会将代码内联(inline)。这样就没有性能上的损失。 (P218 last)
属性访问器的可访问性
有时希望为get访问器方法指定一种可访问性,为set访问器方法指定另一种可访问性。最常见的情形是提供公共get 访问器和受保护set访问器。(P219 1)
//Name属性本身声明为public属性,意味着get访问器方法是公共的,所有代码都能调用。而 set访问器方法声明为protected,只能从SomeType 内部定义的代码中调用,或者从SomeType的派生类的代码中调用。
public class a
{
private string m_name;
public string name
{
get{return m_name;}
protected set{m_name = value;}
}
}
第十一章 事件
定义了事件成员的类型允许类型(或类型的实例)通知其他对象发生了特定的事情。定义了事件成员的类型能提供以下功能:(P221 1)类型之所以能提供事件通知功能,是因为类型维护了一个已登记方法的列表。事件发生后,类型将通知列表中所有已登记的方法。 1、方法能等级它对事件的关注 2、方法能注销它对事件的关注 3、事件发生时,登记了的方法将接收到通知 CLR事件模型以委托为基础。委托是调用”回调方法的一种类型安全的方式。对象凭借回调方法接收它们订阅的通知。 设计要公开事件的类型
//第一步:定义类型来容纳所有需要发送给事件通知接受者的附加信息(P222 last)
public class a
{
private string m_name;
public string name
{
get{return m_name;}
protected set{m_name = value;}
}
}
internal class NewMailEventArgs : EventArgs
{
private readonly string m_from,m_to,m_subject;
public NewMailEventArgs(string from,string to,string subject)
{
m_from = from;m_to = to,m_subject = subject;
}
public string From{get{return m_from;}}
public string To{get{return m_to;}}
public string Subject{get{return m_subject;}}
}
//第二步:定义事件成员-event(P223 last)
//每个事件成员都要指定一下内容:可访问性标识符;委托类型,指出要调用的方法的原型;名称(可以是有效标识符)
iinternal class MailManager
{
public event EventHandler<NewMailEventArgs> NewMail;
}
//第三步:定义负责引发事件的方法来通知事件的登记对象(P224 last)
//类需要定义一个受保护的虚方法,引发事件时,类及其派生类中的代码会调用该方法。
internal class MailManager
{
//第三步:定义负责引发事件的方法来通知已登记的对象。
//如果类是密封的,该方法要声明为私有和非虚
protected virtual void OnNewMail(NewMailEventArgs e)
{
//出于线程安全的考虑,现在将对委托字段的引用复制到一个临时变量中(P225 1)
EventHandler<NewMailEventArgs > temp = Volatile.Read(ref NewMail);
//任何方法登记了对事件的关注,就通知它们
if(temp != null) temp(this,e);
}
}
//第四步:定义方法将输入转化为期望事件(P226 4)
internal class MailManager
{
public void SimulateNewMail(string from,string to,string subject)
{
NewMailEventArgs e = new NewMailEventArgs(from,to,subject);
//调用虚方法通知对象事件已经发生
//如果没有类型重写该方法,我们的对象将通知事件的所有登记对象
OnNewMail(e)
}
}
编译器如何实现事件
C#编译器会把event事件转化为3个构造:1、一个被初始化为null的私有委托字段 2、一个公共add_xxx方法 3、一个公共remove_xxx方法(P226 1)
设计侦听事件的类型
定义一个类型来使用另一个类型提供的事件 (P228 last)
internal sealed class Fax
{
public Fax(MailManager mm)
{
//向MailManager的NewMail事件登记回调方法
mm.NewMail +=Faxmsg;
}
//触发事件时将调用这个方法
private void FaxMsg(object sender,NewMailEventArgs e)
{
console.log.....
}
//注销事件侦听
public void Unregister(MailManager mm)
{
mm.NewMail -= FaxMsg;
}
}
显式实现事件
通过显示实现事件来高效率地实现提供大量事件的类 代码:(P231)(注意一下应该是排版问题,代码中的EventSet类是包含之后所有函数到结尾的,但其他函数并没有缩进导致看起来和EventSet类同级了) 委托(delegate)的使用:传送门 博客参考:传送门 (原文代码中用了线程等内容,会在后续章节中讲述,目前看不太明白的可以看这篇博客)
第十二章 泛型
泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。(P233 1)即该算法没有设定要操作什么数据类型,因此可以广泛的应用于不同类型的对象,在使用时指定算法要操作的具体数据类型。(P233 2) CLR允许创建泛型引用类型和泛型值类型,不允许创建泛型枚举类型。 CLR允许创建泛型接口和泛型委托。(P233 3) 泛型的优势:1、源代码保护 2、类型安全 3、更清晰的代码 4、更佳的性能(P235 1) FCL中的泛型
泛型基础结构
开放类型和封闭类型:具有泛型类型参数的类型仍然是类型,CLR同样会为它创建内部的类型对象。这一点适合引用类型(类)、值类型(结构)、接口类型和委托类型。然而,具有泛型类型参数的类型称为开放类型 ,CLR禁止构造开放类型的任何实例。这类似于CLR禁止构造接口类型的实例。代码引用泛型类型时可指定一组泛型类型实参。为所有类型参数都传递了实际的数据类型,类型就成为封闭类型 。CLR允许构造封闭类型的实例。(P239 1) 泛型类型和继承:泛型也是类型,可以从任何类型派生。指定类型的实参不影响继承层次结构(用于判断强制类型是否被允许)。(P240 last) 泛型类型同一性:如果有一类继承自泛型,并分别实例化两个对象,那么泛型和类的对象在使用==符号判等时返回的结果为FALSE。可以使用using指令来化简泛型封闭类型,例
using DateTimeList = system.collections.Generic.List<System.DateTime>;
代码爆炸:使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码(这些代码为操作指定数据类型“量身定制”)。这正是你希望的,也是泛型的重要特点。但这样做有一个缺点:CLR要为每种不同的方法/类型组合生成本机代码。我们将这个现象称为代码爆炸。它可能造成应用程序的工作集显著增大,从而损害性能。(P243 1) 优化:相同类型实参调用只需组合编译一次。(P243 2)CLR判定所有引用类型实参都完全相同则可以代码共享。(P243 3)
泛型接口
没有泛型接口,每次用非泛型接口(如IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。这将严重制约泛型类型的应用范围。因此,CLR提供了对泛型接口的支持。引用类型或值类型可指定类型实参实现泛型接口。也可保持类型实参的未指定状态来实现泛型接口。(P243 last)
泛型委托
CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外,泛型委托允许值类型实例在传给回调方法时不进行任何装箱。(17章会详细讲述)
委托和接口的逆变和协变泛型类型实参
委托的每个泛型类型参数都可标记为协变量(convariant)或逆变量(contravariant)。利用这个功能,可将泛型委托类型的变量转换为相同的委托类型(但泛型参数类型不同)。泛型类型参数可以是:不变量,逆变量(泛型类型参数可以从一个类更改为它的某个派生类,用in标记),协变量(泛型类型参数可以从一个类更改为它的某个基类,用out标记)。
泛型方法
定义泛型类、结构或接口时,类型中定义的任何方法都可引用类型指定的类型参数。类型参数可作为方法参数、方法返回值或方法内部定义的局部变量的类型使用。然而,CLR还允许方法指定它自己的类型参数。这些类型参数也可作为参数、返回值或局部变量的类型使用。(P247 1) 泛型方法和类型推断:C#编译器支持调用泛型方法时进行类型推断。(P248 last3)类型可以定义多个方法,让其中一个方法接受具体数据类型,让另一个接受泛型类型参数。(P249 2)
泛型和其他成员
在C#中,属性、索引器、事件、操作符方法、构造器和终结器本身不能有类型参数。但它们能在泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。(P249 last2)
可验证性和约束
约束的作用:限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型执行更多操作。(P250 last3) 约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数(如下所示)。CLR不允许基于类型参数名称或约束来进行重载,只能基于元数(类型参数个数)对类型或方法进行重载。(P251 3)
//C#的 where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型lComparable 接口。
//有了这个约束﹐就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo。
private static T Min<T>(T o1,T o2) where T : IComparable<T>
{
if(o1.CompareTo(o2) < 0) return o1;
else return o2;
}
主要约束:类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:System.Object ,System.Array,System.Delegate ,System.MulticastDelegate,System.ValueType,System.Enum或者System.Void。(P252 4) 可以这是class和struct为主要约束(P252 last3) 次要约束:类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。这种约束向编译器承诺类型实参实现了接口。由于能指定多个接口约束,所以类型实参必须实现了所有接口约束(以及主要约束,如果有的话)。第十三章将详细讨论接口约束。(P25 3) 构造器约束:类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。注意,如果同时使用构造器约束和 struct约束,C#编译器会认为这是一个错误,因为这是多余的;所有值类型都隐式提供了公共无参构造器。(where T :new())(P254 2) 其他可验证性问题:(P254 last) 1、泛型类型变量的转型:将泛型类型的变量转型为其他类型是非法的,除非转型为与约束兼容的类型。 2、将泛型类型变量设置为默认值:将泛型类型变量设为null是非法的,除非将泛型类型约束成引用类型。 3、将泛型类型变量与null进行比较:无论泛型类型是否被约束,使用–或!=操作符将泛型类型变量与null进行比较都是合法的: 4、两个泛型类型变量相互比较:如果泛型类型参数不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的。 5、泛型类型变量作为操作数使用:会出大量问题,建议不用。